From d2f864916fd30bcace9cbcc5737607d07375c543 Mon Sep 17 00:00:00 2001 From: Anton Sauchyk Date: Mon, 9 Nov 2026 11:09:15 +0100 Subject: [PATCH] fix(hooks): make bash-guard's docs block and warnings take effect Claude Code treats exit 1 as a non-blocking error and ignores stderr on exit 0, so the docs/*.md block let the command run and Claude never saw the transaction warnings. The block now exits 2 and the warnings go back as additionalContext. Co-Authored-By: Claude Opus 5.5 --- .claude/hooks/bash-guard.py | 43 ++++++++++++++++++------------------- .claude/hooks/test-hooks.sh | 25 ++++++++++++++------- CLAUDE.md | 2 +- 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/.claude/hooks/bash-guard.py b/.claude/hooks/bash-guard.py index e40c16a..f865ef3 100755 --- a/.claude/hooks/bash-guard.py +++ b/.claude/hooks/bash-guard.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 """Pre-bash guard: block dangerous git operations, warn on transactions. -BLOCKS (exit 1): git add of docs/*.md (gitignored files). +BLOCKS (exit 2): git add of docs/*.md (gitignored files). ADVISORY (exit 0): transaction commands, mainnet tests, sensitive file deletion. +The note goes back as JSON `additionalContext`, the only channel Claude reads +on exit 0; the command still runs. """ import json @@ -12,8 +14,6 @@ data = json.load(sys.stdin) command = data.get("tool_input", {}).get("command", "") -# --- Advisory warnings (exit 0 — never block) --- - # Transaction-sending CLI commands TX_COMMANDS = re.compile( r"\bpumpfun\b.*\b(" @@ -44,34 +44,33 @@ "Remove docs/ paths from your git add command.", file=sys.stderr, ) - sys.exit(1) - -elif TX_COMMANDS.search(command): - print( - "NOTE: This command sends a Solana transaction. On mainnet this costs real SOL.", - file=sys.stderr, - ) + sys.exit(2) +note = None +if TX_COMMANDS.search(command): + note = "NOTE: This command sends a Solana transaction. On mainnet this costs real SOL." elif MAINNET_PATTERNS.search(command): - print( + note = ( "NOTE: This runs mainnet end-to-end tests (costs real SOL). " - "Make sure the user has confirmed.", - file=sys.stderr, + "Make sure the user has confirmed." ) - elif SENSITIVE_DELETE.search(command): - print( + note = ( "NOTE: This deletes a sensitive file (.env, wallet, or credentials). " - "Make sure the user intended this.", - file=sys.stderr, + "Make sure the user intended this." ) - elif IDL_DELETE.search(command): - print( + note = ( "NOTE: This deletes IDL files (source-of-truth from on-chain programs). " - "These cannot be regenerated without fetching from chain.", - file=sys.stderr, + "These cannot be regenerated without fetching from chain." + ) + +if note: + print( + json.dumps( + {"hookSpecificOutput": {"hookEventName": "PreToolUse", "additionalContext": note}} + ) ) -# Always allow — this is advisory only +# Advisory only — never block sys.exit(0) diff --git a/.claude/hooks/test-hooks.sh b/.claude/hooks/test-hooks.sh index 155bd5e..3978ed3 100755 --- a/.claude/hooks/test-hooks.sh +++ b/.claude/hooks/test-hooks.sh @@ -9,9 +9,10 @@ PASS=0 FAIL=0 run_test() { - local name="$1" hook="$2" input="$3" expect_exit="$4" expect_stderr="$5" + local name="$1" hook="$2" input="$3" expect_exit="$4" expect_output="$5" - actual_stderr=$(echo "$input" | CLAUDE_PROJECT_DIR="$PROJ" uv run python "$PROJ/.claude/hooks/$hook" 2>&1 1>/dev/null || true) + # stdout carries the JSON advisory, stderr the block reason; check both. + actual_output=$(echo "$input" | CLAUDE_PROJECT_DIR="$PROJ" uv run python "$PROJ/.claude/hooks/$hook" 2>&1 || true) actual_exit=$(echo "$input" | CLAUDE_PROJECT_DIR="$PROJ" uv run python "$PROJ/.claude/hooks/$hook" >/dev/null 2>/dev/null; echo $?) local ok=true @@ -21,14 +22,14 @@ run_test() { ok=false fi - if [[ -n "$expect_stderr" && "$actual_stderr" != *"$expect_stderr"* ]]; then - echo "FAIL: $name — stderr missing '$expect_stderr'" - echo " got: $actual_stderr" + if [[ -n "$expect_output" && "$actual_output" != *"$expect_output"* ]]; then + echo "FAIL: $name — output missing '$expect_output'" + echo " got: $actual_output" ok=false fi - if [[ -z "$expect_stderr" && -n "$actual_stderr" ]]; then - echo "FAIL: $name — expected no stderr, got: $actual_stderr" + if [[ -z "$expect_output" && -n "$actual_output" ]]; then + echo "FAIL: $name — expected no output, got: $actual_output" ok=false fi @@ -79,7 +80,15 @@ run_test "allows README.md" "guard.py" \ "0" "" echo "" -echo "=== bash-guard.py (PreToolUse Bash) — advisory only ===" +echo "=== bash-guard.py (PreToolUse Bash) ===" + +run_test "blocks git add docs/*.md (exit 2)" "bash-guard.py" \ + '{"tool_input":{"command":"git add docs/notes.md"}}' \ + "2" "BLOCKED: docs/*.md files are in .gitignore" + +run_test "advisory is JSON additionalContext" "bash-guard.py" \ + '{"tool_input":{"command":"pumpfun buy ABC123 0.1"}}' \ + "0" '"additionalContext": "NOTE: This command sends a Solana transaction' run_test "warns on pumpfun buy (exit 0)" "bash-guard.py" \ '{"tool_input":{"command":"uv run pumpfun buy ABC123 0.1"}}' \ diff --git a/CLAUDE.md b/CLAUDE.md index 62402f6..fe2243d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -172,7 +172,7 @@ Three hooks run automatically during Claude Code sessions (see `.claude/settings - **guard.py** (PreToolUse → Write|Edit): Blocks edits to `.env`, `wallet.enc`, `idl/`, and credential files (`.pem`, `.key`, `.secret`). Edit these files manually. - **lint.py** (PostToolUse → Write|Edit): Runs `ruff format` + `ruff check --fix` on every edited `.py` file. If unfixable errors remain, you will see them as errors — fix them before moving on. -- **bash-guard.py** (PreToolUse → Bash): Advisory only — prints warnings (never blocks) when commands send Solana transactions, run mainnet tests, or delete sensitive files. The CLI must work unimpeded when run via assistants or automated workflows. +- **bash-guard.py** (PreToolUse → Bash): Blocks `git add` of `docs/*.md` (exit 2). Otherwise advisory: adds a note to Claude's context (JSON `additionalContext`, never blocks) when commands send Solana transactions, run mainnet tests, or delete sensitive files. The CLI must work unimpeded when run via assistants or automated workflows. ## Boundaries