Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 21 additions & 22 deletions .claude/hooks/bash-guard.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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("
Expand Down Expand Up @@ -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)
25 changes: 17 additions & 8 deletions .claude/hooks/test-hooks.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

Expand Down Expand Up @@ -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"}}' \
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
Loading