{"slug": "secret-scanning-hooks-md", "title": "secret-scanning-hooks.md", "summary": "A developer documented a secret scanning setup that prevents credentials from entering the context window of Claude Code, an AI coding assistant. The system uses trufflehog with 700+ detectors to intercept secrets at four entry points—user prompts, file reads, tmux panes, and bash commands—before they reach Anthropic's servers. The hooks are designed to fail-closed, blocking access if trufflehog is missing, and use exit code 2 to enforce blocks or notifications.", "body_md": "This documents the secret scanning setup that prevents credentials from entering Claude's context window during a Claude Code session.\n\nClaude Code reads files and receives user-typed prompts, both of which can contain secrets (API keys, tokens, passwords). Once a secret enters the context window, it is sent to Anthropic's servers as part of the model inference request. The goal is to intercept secrets at the boundary — before they reach Claude — rather than relying on the user to never make a mistake.\n\n[trufflehog](https://github.com/trufflesecurity/trufflehog) scans text for secrets using 700+ detectors covering structured credential formats: GitHub PATs, AWS keys, Slack tokens, GCP service account keys, and many more. It reads from stdin via `trufflehog stdin`\n\nand outputs one JSON object per finding.\n\nIt does **not** catch generic passwords (e.g. `DB_PASSWORD=\"SuperSecret123\"`\n\n), since those have no recognizable structure. Only format-specific, verifiable credential patterns are detected.\n\n`--no-verification`\n\nskips live API calls to check whether a token is still valid — scans run entirely offline.\n\nFour hooks intercept secrets at different entry points:\n\n```\nUser types a prompt       ──►  UserPromptSubmit hook        ──►  blocked or passed to Claude\nClaude reads a file       ──►  PreToolUse/Read hook         ──►  blocked or passed to Claude\nClaude reads a tmux pane  ──►  PreToolUse/mcp__tmux__ hook  ──►  blocked or passed to Claude (optional, see below)\nClaude runs a Bash cmd    ──►  PostToolUse/Bash hook        ──►  cannot block, but notifies user and Claude to rotate\n```\n\nAll hooks call the same script: `.claude/hooks/scan-secrets.sh`\n\n.\n\n```\n\"hooks\": {\n  \"PreToolUse\": [\n    {\n      \"matcher\": \"Read\",\n      \"hooks\": [\n        {\n          \"type\": \"command\",\n          \"command\": \"/path/to/.claude/hooks/scan-secrets.sh read\",\n          \"timeout\": 60,\n          \"statusMessage\": \"Scanning for secrets...\"\n        }\n      ]\n    },\n    {\n      \"matcher\": \"mcp__tmux__capture-pane|mcp__tmux__show-buffer|mcp__tmux__search-buffer|mcp__tmux__subsearch-buffer\",\n      \"hooks\": [\n        {\n          \"type\": \"command\",\n          \"command\": \"/path/to/.claude/hooks/scan-secrets.sh tmux\",\n          \"timeout\": 60,\n          \"statusMessage\": \"Scanning tmux pane for secrets...\"\n        }\n      ]\n    }\n  ],\n  \"PostToolUse\": [\n    {\n      \"matcher\": \"Bash\",\n      \"hooks\": [\n        {\n          \"type\": \"command\",\n          \"command\": \"/path/to/.claude/hooks/scan-secrets.sh bash\",\n          \"timeout\": 60,\n          \"statusMessage\": \"Scanning bash output for secrets...\"\n        }\n      ]\n    }\n  ],\n  \"UserPromptSubmit\": [\n    {\n      \"hooks\": [\n        {\n          \"type\": \"command\",\n          \"command\": \"/path/to/.claude/hooks/scan-secrets.sh prompt\",\n          \"timeout\": 60,\n          \"statusMessage\": \"Scanning prompt for secrets...\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nA `PreToolUse`\n\nhook exits with code 2 to block the tool from running. A `PostToolUse`\n\nhook exits with code 2 to notify — the tool has already run and the output is in Claude's context, but the hook's reason is surfaced to both the user (terminal) and Claude (error notice), prompting key rotation.\n\nExit code semantics— counterintuitively, exit code 1 doesnotblock in Claude Code hooks; it is treated as a non-blocking error and the tool proceeds. Only exit code 2 enforces a block (for`PreToolUse`\n\n) or surfaces a notification (for`PostToolUse`\n\n). A script that errors (e.g. a failed`jq`\n\nparse producing an empty`summary`\n\n) must therefore still exit 2, not rely on a non-zero exit from`${var:?}`\n\nor similar guards.\n\nFail-closed design— if`trufflehog`\n\nis not installed or not on`PATH`\n\n, the script blocks with exit 2 rather than passing through silently. This prevents the hooks from providing a false sense of security on machines where the binary is missing. Install trufflehog with`brew install trufflehog`\n\n.\n\n``` bash\n#!/bin/bash\n# Scans for secrets using trufflehog.\n# Reads the Claude Code hook event JSON from stdin.\n# Usage: scan-secrets.sh prompt|read|tmux|bash\n# Exits 2 with JSON blocking output if any secret is found.\n\nmode=\"${1:?usage: scan-secrets.sh prompt|read|tmux|bash}\"\n\nif ! command -v trufflehog &>/dev/null; then\n    jq -n '{\"decision\":\"block\",\"reason\":\"trufflehog not found — install it to enable secret scanning\"}'\n    exit 2\nfi\n\nevent=$(cat)\n\ncase \"$mode\" in\n    prompt)\n        content=$(echo \"$event\" | jq -r '.prompt')\n        label=\"prompt\"\n        ;;\n    bash)\n        content=$(echo \"$event\" | jq -r '.tool_output')\n        label=\"bash output\"\n        ;;\n    read)\n        label=$(echo \"$event\" | jq -r '.tool_input.file_path')\n        { [ -z \"$label\" ] || [ \"$label\" = \"null\" ] || [ ! -f \"$label\" ]; } && exit 0\n        content=$(cat \"$label\")\n        ;;\n    tmux)\n        tool=$(echo \"$event\" | jq -r '.tool_name')\n        socket=$(echo \"$event\" | jq -r '.tool_input.socket // empty')\n        tmux_cmd=(tmux)\n        [ -n \"$socket\" ] && tmux_cmd+=(-S \"$socket\")\n\n        case \"$tool\" in\n            mcp__tmux__capture-pane)\n                pane_id=$(echo \"$event\" | jq -r '.tool_input.paneId')\n                label=\"tmux pane $pane_id\"\n                content=$(\"${tmux_cmd[@]}\" capture-pane -p -t \"$pane_id\" 2>/dev/null)\n                ;;\n            mcp__tmux__show-buffer)\n                buf_name=$(echo \"$event\" | jq -r '.tool_input.name // empty')\n                label=\"tmux buffer ${buf_name:-latest}\"\n                if [ -n \"$buf_name\" ]; then\n                    content=$(\"${tmux_cmd[@]}\" show-buffer -b \"$buf_name\" 2>/dev/null)\n                else\n                    content=$(\"${tmux_cmd[@]}\" show-buffer 2>/dev/null)\n                fi\n                ;;\n            mcp__tmux__search-buffer|mcp__tmux__subsearch-buffer)\n                buf_name=$(echo \"$event\" | jq -r '.tool_input.buffer // empty')\n                label=\"tmux buffer ${buf_name:-latest}\"\n                if [ -n \"$buf_name\" ]; then\n                    content=$(\"${tmux_cmd[@]}\" show-buffer -b \"$buf_name\" 2>/dev/null)\n                else\n                    content=$(\"${tmux_cmd[@]}\" show-buffer 2>/dev/null)\n                fi\n                ;;\n            *)\n                exit 0\n                ;;\n        esac\n        [ -z \"$content\" ] && exit 0\n        ;;\n    *)\n        echo \"Unknown mode: $mode\" >&2\n        exit 1\n        ;;\nesac\n\nth_out=$(echo \"$content\" | trufflehog stdin --no-update --no-verification --json 2>/dev/null)\n\nif [ -n \"$th_out\" ]; then\n    if [ \"$mode\" = \"prompt\" ] || [ \"$mode\" = \"bash\" ]; then\n        # prompt: blocked before Claude sees it — show full secret so user knows what to rotate\n        # bash: already in Claude's context — include full secret so Claude knows to warn user to rotate\n        summary=$(echo \"$th_out\" | jq -r '\"[\" + .DetectorName + \"] \" + .Raw' 2>/dev/null | paste -sd ', ' -)\n    else\n        # read/tmux: tool result sent back to Claude — omit secret value to avoid leaking it via the reason\n        summary=$(echo \"$th_out\" | jq -r '.DetectorName' 2>/dev/null | sort -u | paste -sd ', ' -)\n    fi\n    [ -z \"$summary\" ] && summary=\"unknown (jq parse failed)\"\n    jq -n --arg summary \"$summary\" --arg label \"$label\" \\\n        '{\"decision\":\"block\",\"reason\":(\"Secret detected in \" + $label + \" — \" + $summary)}'\n    exit 2\nfi\n```\n\nThe block reason is handled differently depending on whether the secret has already reached Claude:\n\n| Mode | Hook type | Secret in context? | Reason includes secret? | Why |\n|---|---|---|---|---|\n`prompt` |\nPreToolUse | No — blocked before Claude sees it | Yes | User needs it to rotate; only shown in terminal |\n`read` |\nPreToolUse | No — blocked before Claude sees it | No | Reason is returned to Claude as the tool result |\n`tmux` |\nPreToolUse | No — blocked before Claude sees it | No | Reason is returned to Claude as the tool result |\n`bash` |\nPostToolUse | Yes — already in Claude's context | Yes | Claude needs it to warn user to rotate |\n\nThe tmux hook requires the [bnomei/tmux-mcp](https://github.com/bnomei/tmux-mcp) MCP server. This is not part of a standard Claude Code setup — most users won't have it. The tool names in the hook matcher (`mcp__tmux__capture-pane`\n\n, `mcp__tmux__show-buffer`\n\n, etc.) are specific to that implementation.\n\n**If you are not using tmux-mcp, you have two options — both safe:**\n\n-\n**Leave everything as-is (recommended).** The hook matcher only fires when Claude calls a`mcp__tmux__*`\n\ntool, which never happens without the MCP server configured. The`tmux)`\n\ncase in the script handles a missing`tmux`\n\nbinary gracefully — commands fail silently,`content`\n\nis empty, and the script exits 0. No effect on anything. -\n**Remove both the matcher and the** Fully stripped down. Also update the usage comment and`tmux)`\n\ncase from the script.`mode`\n\nvalidation from`prompt|read|tmux|bash`\n\nto`prompt|read|bash`\n\n.\n\nThe remaining three hooks (`prompt`\n\n, `read`\n\n, `bash`\n\n) work without any MCP or tmux dependency.\n\nThe `!`\n\nprefix in Claude Code runs a shell command and injects its stdout directly into the conversation. This bypasses all hooks — there is no `UserPromptSubmit`\n\nevent for local command output, and Claude never issues a `Read`\n\nor MCP tool call for it.\n\n```\n! cat /path/to/file-with-secret.txt   ← output goes straight to Claude's context\n```\n\nThis is a platform limitation. As a workaround, avoid using `!`\n\nor bash blocks to print files that might contain credentials. Use Claude's `Read`\n\ntool instead (which is protected by the `PreToolUse`\n\nhook).\n\n- Install trufflehog:\n`brew install trufflehog`\n\n- Copy\n`scan-secrets.sh`\n\nto`.claude/hooks/`\n\nand make it executable:`chmod +x scan-secrets.sh`\n\n- Add the hook entries to\n`.claude/settings.local.json`\n\n(use absolute paths in`command`\n\n) - If using the tmux hook: install\n[bnomei/tmux-mcp](https://github.com/bnomei/tmux-mcp)and configure it as an MCP server in Claude Code - Restart the Claude Code CLI\n\n```\n# Should pass (no secret)\necho '{\"prompt\":\"hello world\"}' | .claude/hooks/scan-secrets.sh prompt\n\n# Should block (prompt with secret) — shows full token in terminal\necho '{\"prompt\":\"TOKEN=ghp_REPLACE_WITH_A_REAL_FORMAT_TOKEN\"}' | .claude/hooks/scan-secrets.sh prompt\n\n# Should pass (non-existent file)\necho '{\"tool_input\":{\"file_path\":\"/tmp/nonexistent\"}}' | .claude/hooks/scan-secrets.sh read\n\n# Should block (file with secret)\necho 'REPLACE_WITH_REAL_TOKEN' > /tmp/test-secret.txt\necho '{\"tool_input\":{\"file_path\":\"/tmp/test-secret.txt\"}}' | .claude/hooks/scan-secrets.sh read\n\n# Should notify (bash output with secret) — cannot block, warns user and Claude to rotate\necho '{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"cat /tmp/test-secret.txt\"},\"tool_output\":\"REPLACE_WITH_REAL_TOKEN\\n\"}' \\\n    | .claude/hooks/scan-secrets.sh bash\n\n# Should block (tmux buffer with secret) — requires tmux-mcp\ntmux new-session -d -s test\ntmux set-buffer 'REPLACE_WITH_REAL_TOKEN'\nbuf=$(tmux list-buffers -F '#{buffer_name}' | head -1)\necho \"{\\\"tool_name\\\":\\\"mcp__tmux__show-buffer\\\",\\\"tool_input\\\":{\\\"name\\\":\\\"$buf\\\"}}\" \\\n    | .claude/hooks/scan-secrets.sh tmux\ntmux kill-session -t test\n```\n\n", "url": "https://wpnews.pro/news/secret-scanning-hooks-md", "canonical_source": "https://gist.github.com/oajara/3a4d1e71fa727b893a4ffaedd6dd5064", "published_at": "2026-06-18 15:22:39+00:00", "updated_at": "2026-06-24 15:39:15.122313+00:00", "lang": "en", "topics": ["ai-safety", "developer-tools", "large-language-models"], "entities": ["Claude Code", "trufflehog", "Anthropic", "GitHub", "AWS", "Slack", "GCP"], "alternates": {"html": "https://wpnews.pro/news/secret-scanning-hooks-md", "markdown": "https://wpnews.pro/news/secret-scanning-hooks-md.md", "text": "https://wpnews.pro/news/secret-scanning-hooks-md.txt", "jsonld": "https://wpnews.pro/news/secret-scanning-hooks-md.jsonld"}}