Claude Code Hook Security — Architecture, Limitations, and Coverage

Written after incident #2620 (2026-07-02): monitoring.env.sops full dump in whatsup-android-chat-puller session. Root cause chain and prevention controls documented here.


1. What hooks can and cannot do

PreToolUse hooks — the only real prevention

PreToolUse hooks run before the tool executes. Exit code 1 blocks the tool call entirely. This is the only mechanism that can prevent secrets from reaching stdout.

What PreToolUse CAN do:

  • Block SOPS decrypt commands that lack a safe output sink (| Out-Null, | Select-String, etc.)
  • Block cat/Read on credential files
  • Modify the command before it runs (exit 0 with modified tool_input JSON on stdout)

Implementation: C:\Users\konar\.claude\hooks\global-sops-safety.py Called from ~/.claude/settings.json PreToolUse[PowerShell] — applies to every session globally.

PostToolUse hooks — warning only, NOT prevention

PostToolUse hooks run after the tool output is already:

  1. Rendered in the VS Code UI / chat
  2. Added to Claude’s full context (tokens consumed)

Exit code 2 injects a warning string into Claude’s context, telling Claude not to repeat the values in its text response. It does NOT suppress the tool output from the UI or reduce tokens.

What PostToolUse CANNOT do:

  • Suppress display of tool output in VS Code chat
  • Reduce token usage (output is already in context before hook fires)
  • Prevent the values from being visible to the human reading the chat

Implementation: C:\Users\konar\.claude\hooks\redact-credentials.py Matches JWTs, Supabase keys, GitHub PATs, Wasabi keys, OpenAI keys, dotenv dump patterns, etc. Tells Claude to reference key NAMES only in its response, not to repeat values.

”No show output in chat” — this feature does NOT exist

There is no global setting to suppress tool output from the Claude Code UI. The only way to avoid secrets in the UI is to not run commands that produce them (PreToolUse blocking).

”Only show errors / fewer comments” — this feature does NOT exist globally

No global filter exists for command output. Output volume can only be reduced by:

  • Piping commands to Select-String, grep, | Out-Null, etc. in the command itself
  • Using Grep / Read tools with targeted patterns instead of broad shell commands

2. SOPS 3.x flag-order pitfall (root technical cause of #2620)

SOPS 3.x silently ignores flags placed AFTER the positional file argument.

# WRONG — --output is ignored, secrets go to stdout
sops --decrypt --input-type dotenv --output-type dotenv secrets/x.env.sops --output /tmp/file
 
# CORRECT — --output before the file argument
sops --decrypt --input-type dotenv --output-type dotenv --output /tmp/file secrets/x.env.sops
 
# SAFEST — pipe to suppressor (no temp file)
sops --decrypt --input-type dotenv --output-type dotenv secrets/x.env.sops | Out-Null
sops --decrypt --input-type dotenv --output-type dotenv secrets/x.env.sops |
  ForEach-Object { $_.Split("=")[0] }  # key names only
 
# Single-key safe extraction
$env:MY_SECRET = (sops --decrypt --input-type dotenv --output-type dotenv secrets/x.env.sops |
  Select-String "^KEY_NAME=").ToString().Split("=",2)[1]

The global-sops-safety.py PreToolUse hook blocks any SOPS decrypt without a safe sink.


3. Incident #2620 root cause chain

Primary root cause: role boundary violation

The whatsup session needed a CF Workers API token for wrangler. When infra-task-request #2609 failed (worker lacked wrangler), the session entered “solve-it-myself” mode instead of:

Correct escalation path:
  Identify blocker: no CF_WORKERS_API_TOKEN with Workers Edit scope
         ↓
  Create GH issue: "secret-manager: add CF_WORKERS_API_TOKEN to monitoring.env.sops"
         ↓
  Open p24-infra session → /role-secret-manager handles SOPS
         ↓
  Callback on original issue → whatsup session continues

The whatsup session should never have known what is in monitoring.env.sops.

Secondary root causes:

LayerWhy it failed
CLAUDE.md role boundarySoft rule — not enforced by hook
p24-infra PreToolUse hooksScoped to p24-infra sessions only; didn’t apply in whatsup session
redact-credentials.py PostToolUseFires too late (output already in UI); pattern list too narrow (4 patterns, missed monitoring.env.sops key formats)
SOPS flag ordersops ... file.env.sops --output $tmp writes to stdout, not $tmp

Not a bug in monitoring.env.sops access control — the session had the age key available (shared dev machine). The failure was in judgment, not access control.


4. Hook coverage matrix (as of 2026-07-03)

RepoPreToolUse PowerShellPreToolUse BashNotes
p24-infrapre-bash-safety-windows.ps1pre-bash-safety.shFull SOPS + server op protection
whatsup-android-chat-puller✅ global global-sops-safety.py❌ (no Bash PreToolUse)Fixed 2026-07-03
Art-Agency✅ global global-sops-safety.pyFixed 2026-07-03
brandpilot✅ global (no local hook)pre-bash-safety.shGlobal covers PowerShell
radekkonarski-personal-brand✅ global (no local hook)Global covers PowerShell
et-operational-platform✅ global (no local hook)pre-bash-safety.shGlobal covers PowerShell
amazon-kdp-tango✅ global (no local hook)Global covers PowerShell

Key gap: Bash tool PreToolUse is not globally covered. Bash is rarely used on this Windows machine (PowerShell is default), but remains an uncovered vector. Mitigation: CLAUDE.md rules + the PostToolUse redact-credentials.py hook (coverage layer 3).

Global hook path: C:\Users\konar\.claude\settings.json → PreToolUse[PowerShell] Global hook script: C:\Users\konar\.claude\hooks\global-sops-safety.py PostToolUse hook: C:\Users\konar\.claude\hooks\redact-credentials.py (expanded 2026-07-03: 9 patterns + dotenv dump detection)


5. Protocol: what to do if a secret appears in chat

  1. Stop immediately — do not quote, repeat, or summarize the value
  2. Reference only the KEY NAME in all subsequent communication
  3. Open docs/playbooks/static-api-key-incident-rotation.md and follow it
  4. Create a GH issue in radieu/p24-infra with label security BEFORE any workarounds
  5. Identify the root cause layer (role violation / missing hook / SOPS flag order) and patch it

6. Session start checklist for any cross-boundary SOPS need

If a non-p24-infra session discovers it needs a secret from a SOPS file:

1. Stop. What SOPS file contains the key?
2. Create a GH issue: "secret-manager: provide <KEY_NAME> for <purpose>"
3. Open p24-infra session → /role-secret-manager → issue handles the SOPS operation
4. Wait for callback comment on your original issue
5. Never open or decrypt a SOPS file from a non-p24-infra session

The role boundary is: only /role-secret-manager in p24-infra sessions touches *.env.sops.