Hooks are user-defined shell commands that execute at specific points in Claude Code’s lifecycle. They provide deterministic control: certain actions always happen rather than relying on the LLM to choose.
Hook Events
Ten events available:
| Event | Trigger | Can Block |
|---|---|---|
| PreToolUse | Before tool execution | Yes |
| PostToolUse | After tool completes | Yes |
| PermissionRequest | Permission dialog shown | Yes |
| UserPromptSubmit | User submits prompt | Yes |
| Stop | Main agent finishes | Yes |
| SubagentStop | Subagent finishes | Yes |
| Notification | Notifications sent | No |
| PreCompact | Before compacting | No |
| SessionStart | Session begins | No |
| SessionEnd | Session ends | No |
Best practice: PreToolUse for policy guards, PostToolUse for cleanup and feedback.
Configuration
Hooks live in settings files:
~/.claude/settings.json(user, applies everywhere).claude/settings.json(project, shared via git).claude/settings.local.json(local, gitignored)
Basic structure:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "your-script.sh",
"timeout": 60
}
]
}
]
}
}Use /hooks command for interactive configuration.
Matchers
Filter which tools trigger hooks:
| Pattern | Matches |
|---|---|
Write | Exact match only |
Edit|Write | Either Edit or Write |
Notebook.* | Regex pattern |
* or "" | All tools |
mcp__memory__.* | MCP server tools |
Matchers are case-sensitive. bash won’t match Bash.
Exit Codes
| Code | Behavior |
|---|---|
| 0 | Success, continue execution |
| 2 | Block tool execution (PreToolUse/PermissionRequest only) |
| Other | Non-blocking error, logged |
Input Data (stdin)
Hooks receive JSON via stdin:
{
"session_id": "abc123",
"cwd": "/project/path",
"tool_name": "Bash",
"tool_input": {
"command": "npm test",
"description": "Run tests"
},
"tool_response": {}
}Extract with jq:
jq -r '.tool_input.command'
jq -r '.tool_input.file_path'Environment Variables
Available in all hooks:
CLAUDE_PROJECT_DIR: Absolute project root pathCLAUDE_CODE_REMOTE:"true"for web, empty for CLI
SessionStart only:
CLAUDE_ENV_FILE: Path to persist environment variables
Output Control (JSON)
Return JSON to stdout for advanced control:
{
"continue": true,
"stopReason": "Message when continue=false",
"suppressOutput": false,
"systemMessage": "Warning shown to user",
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"additionalContext": "Context for Claude"
}
}PreToolUse Decision Control
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "Auto-approved by policy",
"updatedInput": {
"command": "modified-command"
}
}
}Permission decisions:
allow: Auto-approve, bypass permission systemdeny: Block tool callask: Request user confirmation
Stop Hook Control
Used for Ralph Wiggum loops:
{
"decision": "block",
"reason": "Tasks incomplete, continue working"
}Practical Examples
Block Dangerous Commands
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "if [[ \"$CLAUDE_TOOL_INPUT\" == *\"rm -rf\"* ]]; then echo 'Blocked!' >&2 && exit 2; fi"
}]
}]
}
}Auto-Format TypeScript
{
"hooks": {
"PostToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "jq -r '.tool_input.file_path' | { read f; [[ \"$f\" == *.ts ]] && npx prettier --write \"$f\"; }"
}]
}]
}
}Protect Sensitive Files
{
"hooks": {
"PreToolUse": [{
"matcher": "Edit|Write",
"hooks": [{
"type": "command",
"command": "python3 -c \"import json,sys; d=json.load(sys.stdin); p=d.get('tool_input',{}).get('file_path',''); sys.exit(2 if any(x in p for x in ['.env','package-lock.json','.git/']) else 0)\""
}]
}]
}
}Log All Bash Commands
{
"hooks": {
"PreToolUse": [{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"No desc\")\"' >> ~/.claude/bash-log.txt"
}]
}]
}
}Desktop Notifications
{
"hooks": {
"Notification": [{
"matcher": "",
"hooks": [{
"type": "command",
"command": "notify-send 'Claude Code' 'Awaiting input'"
}]
}]
}
}Persist Environment Variables (SessionStart)
#!/bin/bash
if [ -n "$CLAUDE_ENV_FILE" ]; then
echo 'export NODE_ENV=production' >> "$CLAUDE_ENV_FILE"
fi
exit 0Modify Tool Inputs
PreToolUse hooks can modify inputs before execution:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"updatedInput": {
"command": "npm run lint -- --fix"
}
}
}Prompt-Based Hooks
For Stop/SubagentStop, use LLM evaluation:
{
"hooks": {
"Stop": [{
"hooks": [{
"type": "prompt",
"prompt": "Check if all tasks complete. Return {\"ok\": true} or {\"ok\": false, \"reason\": \"why\"}",
"timeout": 30
}]
}]
}
}This powers the Ralph Wiggum Loop technique for autonomous loops.
Security Considerations
Hooks run with your terminal’s permissions. Risks:
- Malicious hooks can exfiltrate data
- User settings apply to all projects
Best practices:
- Review hook implementations before adding
- Validate and quote all variables:
"$VAR"not$VAR - Avoid touching
.env,.git/, secrets
Debugging
claude --debug # Detailed hook execution logs
/hooks # Interactive configurationRelated
- Claude Code
- Slash commands can define hooks in frontmatter
- Stop hooks enable Ralph Wiggum loops
- MCP tools can be matched with mcp__server__tool pattern