#!/usr/bin/env bash # PreToolUse hook (mcp__.* matcher) — makes a human approve irreversible MCP # verbs instead of letting the model run them unattended. # # Why (infra#508, infra ADR-0027): gitea-mcp authenticates by PAT pass-through, # so an agent holds the full scope of the caller's PAT. repo_delete's own guard # (confirm == repo) is filled in by the model on both sides: it stops typos, # not an injected or mistaken agent. The infra-mcp README claimed client-side # deny lists covered this; on 2026-10-01 none existed on any settings file. # # Policy: match the VERB, not the server. The same tool is reachable as # mcp__plugin_skills_gitea__*, mcp__claude_ai_gitea__* (connector) or under # any name a host registers it with, so keying on the server name would let a # rename bypass the gate. # # Emits "ask" or nothing. Never "allow": a hook allow would override the # user's own deny/ask rules. Malformed input fails closed (ask). # # Scope limit, recorded in the ADR: this gates MCP calls only. A session with # the PAT in its environment can still reach the Gitea REST API via Bash. set -uo pipefail DESTRUCTIVE='repo_delete|file_delete|branch_delete|pr_merge|tbd_ship|repo_update|repo_mirror_push' ask() { jq -n --arg reason "$1" \ '{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: $reason}}' exit 0 } INPUT="$(cat)" if ! TOOL_NAME="$(printf '%s' "$INPUT" | jq -er '.tool_name' 2>/dev/null)"; then ask "mcp-destructive-guard: could not parse the tool call, so it cannot be shown safe. Approve only if you expected it." fi verb="${TOOL_NAME##*__}" case "$TOOL_NAME" in mcp__*__*) ;; *) exit 0 ;; esac if printf '%s' "$verb" | grep -Eqix "($DESTRUCTIVE)"; then ask "Irreversible MCP verb '$verb' (infra ADR-0027): needs your approval. The model filled in every argument, including any confirm field." fi exit 0