Your First Hook
A hook runs a script automatically when something happens in Claude Code — like before Claude runs a bash command, or after Claude edits a file. Hooks let you enforce policies, log activity, validate changes, or integrate with external systems.
The most common use case
Section titled “The most common use case”Block Claude from running dangerous shell commands, even if you accidentally approve them:
-
Create the hook script
Create
.claude/hooks/check-bash.sh:#!/bin/bash# Read the hook input (JSON with tool name and command details)INPUT=$(cat)# Extract the command being runCOMMAND=$(echo "$INPUT" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('tool_input', {}).get('command', ''))")# Block dangerous patternsif echo "$COMMAND" | grep -qE '(rm -rf /|DROP TABLE|> /dev/sda)'; thenecho "Blocked: dangerous command detected" >&2exit 2fi# Allow everything elseexit 0 -
Register the hook in settings
Add to
.claude/settings.json:{"hooks": {"PreToolUse": [{"matcher": "Bash","handler": {"type": "command","command": ".claude/hooks/check-bash.sh"}}]}} -
Claude now checks every bash command
Every time Claude wants to run a bash command, your script runs first. If it exits with code
2, Claude stops. If it exits with code0, Claude proceeds.
What “blocking” means — exit codes
Section titled “What “blocking” means — exit codes”The exit code from your script is the decision:
| Exit code | Meaning |
|---|---|
0 | Proceed normally — Claude runs the tool |
2 | Block — Claude stops and does not run the tool |
| Any other | Ignored — Claude proceeds (treat as 0) |
This is how you’d prevent Claude from running rm -rf commands, accidentally overwriting production configs, or making network requests to internal services.
How hook events flow
Section titled “How hook events flow”-
Claude decides to run a tool — e.g.
rm -rf old_files/ -
Claude Code finds matching hooks — checks
PreToolUsehandlers insettings.jsonfor matchers that match"Bash". -
Your script runs with JSON on stdin — Claude Code runs
check-bash.shand pipes the tool call JSON to its stdin. Your script parses the command and checks it against your deny list. -
Your script exits with a decision:
- Exit 0 (safe): Claude Code proceeds and runs the Bash command. The output is returned to Claude.
- Exit 2 (dangerous): Claude Code stops. Whatever your script wrote to stderr becomes the block message shown to Claude.
The JSON input your script receives
Section titled “The JSON input your script receives”When Claude Code calls your hook, it passes JSON to your script’s stdin:
{ "event": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "rm -rf old_files/", "description": "Remove old files" }, "session_id": "abc123"}For PostToolUse, you also get tool_result with the output.
Parse it with python3 -c "import json,sys; d=json.load(sys.stdin); ..." or jq if you prefer.
Hook event types
Section titled “Hook event types”The most useful events for beginners:
| Event | Fires when | Blockable? |
|---|---|---|
PreToolUse | Before any tool runs | Yes — exit 2 to block |
PostToolUse | After a tool succeeds | Yes — exit 2 to undo/flag |
SessionStart | When a session begins | No |
Stop | When Claude finishes a response | Yes — exit 2 to continue |
FileChanged | When a watched file changes | No |
For the full list of 26 events, see Hooks/event-reference.md.
A complete walkthrough: logging all file edits
Section titled “A complete walkthrough: logging all file edits”Here’s a hook that logs every file Claude edits to a file, which you can review later:
.claude/hooks/log-edits.sh
#!/bin/bashINPUT=$(cat)FILE=$(echo "$INPUT" | python3 -c "import json, sysd = json.load(sys.stdin)print(d.get('tool_input', {}).get('file_path', 'unknown'))")echo "$(date '+%Y-%m-%d %H:%M:%S') Claude edited: $FILE" >> .claude/edit-log.txtexit 0.claude/settings.json
{ "hooks": { "PostToolUse": [ { "matcher": "Write", "handler": { "type": "command", "command": ".claude/hooks/log-edits.sh" } } ] }}This hook runs after every Write tool call (not before, so it can’t block — it just logs).
Hook configuration fields
Section titled “Hook configuration fields”{ "hooks": { "PreToolUse": [ { "matcher": "Bash", "handler": { "type": "command", "command": "path/to/script.sh", "timeout": 5000 } } ] }}| Field | Required | What it does |
|---|---|---|
matcher | No | Filter by tool name (e.g., "Bash", "Write"). Omit to match all tools for this event. |
handler.type | Yes | "command" (shell script), "http" (webhook), "prompt" (ask Claude), "agent" (subagent) |
handler.command | Yes (for type: command) | Path to your script. Relative to project root. |
handler.timeout | No | Milliseconds before the hook is killed (default: 60000) |
Troubleshooting
Section titled “Troubleshooting”Hook never fires
- Check JSON syntax in settings.json — a typo breaks all hooks
- Make sure the event name is spelled correctly (
PreToolUse, notpre-tool-use) - Check that
matchermatches the tool name exactly ("Bash"not"bash")
Hook fires but doesn’t block
- Exit code 2 blocks; anything else (including exit 1) does not block
- Make sure your script actually exits — infinite loops hang the session
Script can’t find my file
- Hooks run with the project root as the working directory
- Use relative paths from the project root, or absolute paths
Next steps
Section titled “Next steps”- Hooks/event-reference.md — all 26 events with matcher support
- Hooks/handler-types.md —
command,http,prompt,agenthandlers - Hooks/security-model.md — SSRF protection and env var allowlists for http hooks
- Hooks/how-event-hooks-work.md — scope precedence and async execution