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/Readon credential files - Modify the command before it runs (exit 0 with modified
tool_inputJSON 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:
- Rendered in the VS Code UI / chat
- 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/Readtools 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:
| Layer | Why it failed |
|---|---|
| CLAUDE.md role boundary | Soft rule — not enforced by hook |
| p24-infra PreToolUse hooks | Scoped to p24-infra sessions only; didn’t apply in whatsup session |
redact-credentials.py PostToolUse | Fires too late (output already in UI); pattern list too narrow (4 patterns, missed monitoring.env.sops key formats) |
| SOPS flag order | sops ... 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)
| Repo | PreToolUse PowerShell | PreToolUse Bash | Notes |
|---|---|---|---|
p24-infra | ✅ pre-bash-safety-windows.ps1 | ✅ pre-bash-safety.sh | Full 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.py | ❌ | Fixed 2026-07-03 |
brandpilot | ✅ global (no local hook) | ✅ pre-bash-safety.sh | Global covers PowerShell |
radekkonarski-personal-brand | ✅ global (no local hook) | ❌ | Global covers PowerShell |
et-operational-platform | ✅ global (no local hook) | ✅ pre-bash-safety.sh | Global 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
- Stop immediately — do not quote, repeat, or summarize the value
- Reference only the KEY NAME in all subsequent communication
- Open
docs/playbooks/static-api-key-incident-rotation.mdand follow it - Create a GH issue in
radieu/p24-infrawith labelsecurityBEFORE any workarounds - 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.