norboten · cheat sheet

A hook is a policy only if it fires, reads its input and exits 2 — cheat sheet

claude-code, bash
# what hooks are configured, per event and matcher
jq -c '.hooks | to_entries[] | {event: .key, matchers: [.value[].matcher]}' .claude/settings.json
ls -l .claude/hooks                      # executable?

# see what a hook receives: a first line that saves its stdin
cat > /tmp/hook-input.json

# test a hook by hand, exactly as Claude Code runs it
printf '%s' '{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}' \
  | CLAUDE_PROJECT_DIR=$PWD "$PWD/.claude/hooks/no-push.sh"; echo "exit $?"

# reading input inside a hook
cmd=$(jq -r '.tool_input.command // ""')
file=$(jq -r '.tool_input.file_path // ""')

# blocking a PreToolUse call: either
echo "Blocked: reason for the model" >&2; exit 2
# or
printf '%s\n' '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"reason"}}'; exit 0

# after a headless run: what the hooks (and rules) refused
jq -c '.permission_denials[] | {tool_name, tool_input}' run.json

# matchers: exact names are case-sensitive
"matcher": "Edit|Write"        # yes
"matcher": "edit|write"        # matches nothing
"matcher": "mcp__github__.*"   # regex: every tool of one MCP server

# undo one file from a pushed commit, forward
git log --oneline -- path/to/file
git restore --source=<good-commit> --staged --worktree -- path/to/file
git commit -m "Restore …" && git push