The hook system — shipped scripts that Claude Code runs on session and tool lifecycle events
Tasks
Read the event payload safely
import json
import sys
try:
payload = json.load(sys.stdin)
except (json.JSONDecodeError, ValueError):
sys.exit(0) # fail open — never block on bad input
if not isinstance(payload, dict):
sys.exit(0)
tool_name = payload.get("tool_name", "")
Verify: the script exits 0 on non-JSON or non-dict stdin, and
reads fields with .get() (never a raising index), so a hook bug can
never block a real tool call.
Block a tool from a PreToolUse hook
import json
import sys
payload = json.load(sys.stdin)
if payload.get("tool_name") == "Write" and "/etc/" in str(
payload.get("tool_input", {}).get("file_path", "")
):
print("refusing to write under /etc", file=sys.stderr)
sys.exit(2)
sys.exit(0)
Verify: exit 2 blocks the tool and Claude Code surfaces the
stderr message; exit 0 lets it proceed.