Claude Code Hooks — Auto-run Shell Commands on Session and Tool Events
settings.json hooks insert shell commands at session start / pre & post tool use / stop. Five examples: security block, auto-format, logging, external alerts.
Claude Code's hooks — defined under the hooks key in settings.json — let you auto-run shell commands on session start, before/after tool calls, and on session end. Intercept what the AI is about to do, fix things up afterward, clean up at the end. A powerful extension point.
This guide targets Claude Code 1.x / the official hooks spec. It tightens automation + security one notch beyond Claude Code setup.
TL;DR
| Hook event | Fires | Typical use |
|---|---|---|
SessionStart | Right after session begins | Memory sync notice, env validation |
UserPromptSubmit | Right after user submits a prompt | Slash command recognition, logging, post-processing |
PreToolUse | Right before a tool call | Block dangerous commands, protect secret files |
PostToolUse | Right after a tool call | Auto-format (prettier), lint, side-effect logging |
Stop | Right before session end | Change summary, backup, notification |
Key rules:
- Exit code 2 blocks the tool call (PreToolUse).
- JSON arrives on hook stdin — parse with
jq. - Hooks are shell commands — mind OS differences (macOS/Linux/Windows WSL).
Prerequisites
- Claude Code 1.x installed — Claude Code setup
jqinstalled (brew install jq/apt install jq/winget install jqlang.jq)- Edit access to
settings.json(usually~/.claude/settings.json)
Download the Baseline
The devAlice recommended five-hook baseline + permission deny/allow list — grab it and customize.
settings.json# 1. Download
curl -fsSL https://devalice.jaceclub.com/assets/ai-agents/claude-code-hooks/settings.example.json -o settings.json
# 2. Verify SHA-256
shasum -a 256 settings.json
# Expected: 30c1d4117a935cac03b601c71125c11b71733c8c59517ab129fd8913cfb8a69e
# 3. Inspect (especially PreToolUse hooks)
less settings.json
# 4. Copy to its location (macOS/Linux)
mkdir -p ~/.claude
cp settings.json ~/.claude/settings.json⚠️ Caution: hooks run arbitrary shell commands. Always inspect anyone else's hooks before applying. This sample is a safe five-hook baseline, but review and adapt for your environment.
1. Hooks Structure — 5 min
The hooks key in ~/.claude/settings.json:
{
"hooks": {
"EventName": [
{
"matcher": "regex or tool name", // optional
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'actual shell command'"
}
]
}
]
}
}Matching rules:
- No
matcher→ fire on every event matcher= tool name (Bash,Write,Edit,Read…) → only that toolmatcher= regex (^/(?:ej|euijinpro)\\b) → forUserPromptSubmit, matches the prompt text
JSON arriving on stdin:
{
"session_id": "...",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/Users/...",
"tool_name": "Bash",
"tool_input": { "command": "..." },
"tool_response": { /* PostToolUse only */ }
}Extract with jq -r .tool_input.command.
2. SessionStart — Load Context
Simplest hook. Print a line when the session starts:
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'printf \"[session start] %s\\nmemory sync: latest (skip)\\n\" \"$(date \"+%Y-%m-%d %H:%M:%S\")\"'"
}
]
}
]Stdout is exposed as a system message. Use it for:
- Triggering memory file auto-load
- Project-specific context output
- Git status summary at start
# Richer example
command='/bin/bash -lc "echo [session start]; git -C \"$PWD\" status -s; git -C \"$PWD\" log --oneline -3"'3. PreToolUse — Block Dangerous Commands (highest-value hook)
exit 2 blocks. Even if the AI tries a dangerous shell command, you stop it.
3.1 Dangerous Patterns
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'cmd=$(jq -r .tool_input.command); case \"$cmd\" in *\"rm -rf /\"*|*\"sudo rm\"*|*\":(){\"*) echo \"blocked dangerous command: $cmd\" >&2; exit 2;; esac'"
}
]
}
]Blocked patterns:
rm -rf /— system destructionsudo rm— broad delete (allow via whitelist if you need it):(){— fork bomb (:(){ :|: & };:)
3.2 Block Edits to Secret Files
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'path=$(jq -r .tool_input.file_path); case \"$path\" in */.env|*/credentials.json|*/.aws/credentials) echo \"blocked secret-file edit: $path\" >&2; exit 2;; esac'"
}
]
}Prevents accidental edits to .env, credentials.json, ~/.aws/credentials, etc.
3.3 Protect Specific Directories
command='/bin/bash -lc "path=\$(jq -r .tool_input.file_path); case \"\$path\" in /etc/*|/System/*) echo \"blocked system path: \$path\" >&2; exit 2;; esac"'4. PostToolUse — Auto-format / Lint / Log
Cleanup after the tool call.
4.1 Prettier Auto-format
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'path=$(jq -r .tool_input.file_path); case \"$path\" in *.ts|*.tsx|*.js|*.jsx) prettier --write \"$path\" 2>/dev/null || true ;; esac'"
}
]
}
]After an AI Write/Edit, prettier auto-cleans. Adds ~0.5s per edit.
4.2 Log Changed Files
command='/bin/bash -lc "path=\$(jq -r .tool_input.file_path); echo \"[changed] \$(date +%T) \$path\" >> ~/.claude/change.log"'Time-ordered record of every file change in the session.
4.3 Type-check / Lint (move slow work to Stop)
tsc --noEmit on every edit is expensive. Better in the Stop hook (§6).
5. UserPromptSubmit — Slash Detection / Post-processing Trigger
"UserPromptSubmit": [
{
"matcher": "^/(?:ej|euijinpro)\\b",
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'echo \"[ej skill] prompt earmarked for euijin.pro logging\" >&2'"
}
]
}
]- Detect slash commands → trigger external systems (Slack, separate logging)
- Auto-label specific keywords (
urgent,bug) - Token budget threshold checks
6. Stop — Cleanup at Session End
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'printf \"\\n[session end] %s\\n\" \"$(date \"+%Y-%m-%d %H:%M:%S\")\"'"
}
]
}
]Uses:
- Changed-file git status summary
- Backup (
rsyncorgit stash) - External shutdown notification
- One round of type-check / build
command='/bin/bash -lc "if git diff --quiet; then echo \"[clean]\"; else echo \"[changes]\"; git diff --stat; fi"'7. permissions — Deny / Allow
Separate from hooks but the most important security key in settings.json.
"permissions": {
"deny": [
"Bash(rm -rf /:*)",
"Bash(sudo rm:*)",
"Read(.env)",
"Read(.env.local)",
"Read(.aws/credentials)",
"Read(*/credentials.json)"
],
"allow": [
"Bash(git status:*)",
"Bash(git log:*)",
"Bash(git diff:*)",
"Bash(pnpm:*)",
"Bash(npm:*)",
"Bash(node:*)"
]
}denywins unconditionally — complements hook blocksallowauto-approves without user prompt (frequent safe commands)- Everything else asks the user every time
This guide's hooks overlap with deny — defense in depth.
8. settings.json Locations and Priority
| Location | Scope | Notes |
|---|---|---|
~/.claude/settings.json | User-global | All projects |
<project>/.claude/settings.json | Project | Merges with user on project entry |
<project>/.claude/settings.local.json | Project + user-local | Recommended gitignored |
Merge order: user → project → project.local. Later overrides earlier.
Hooks append rather than merge — 5 user hooks + 3 project hooks = all 8 run.
9. Debugging — When the Hook Doesn't Fire
9.1 Inspect stdin Payload
Temporary debug hook:
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/bin/bash -lc 'cat > /tmp/claude-hook-debug.json'"
}
]
}→ cat /tmp/claude-hook-debug.json | jq .
9.2 Test Exit Codes
command='/bin/bash -lc "echo dbg >&2; exit 2"'→ Verify blocking (PreToolUse) works.
9.3 Confirm Hook Fires
command='/bin/bash -lc "echo \"[hook fired $$ $(date +%T)]\" >> ~/.claude/hook-trace.log"'Check the log after a session.
9.4 JSON Syntax Errors
settings.json is unforgiving — trailing commas and missed escapes are common. Validate via jq . ~/.claude/settings.json.
10. Troubleshooting
"command not found: jq"
brew install jq (macOS) / sudo apt install jq (Linux) / winget install jqlang.jq (Windows).
Hook Fires But Does Nothing
commandmust be single-line. For multiline, use\\nor call a separate script.- Watch shell escapes —
\"is double-escaped inside JSON.
PreToolUse exit 2 Ignored
- Confirm
exit 2actually reaches the last line of the shell command. - A
set -eenvironment can letcase ;;exit 0 — directlyexit 2inside the matched branch.
Prettier Doesn't Run
- Confirm prettier is installed in the project root (
pnpm prettier --version). - Hook PATH may not include
node_modules/.bin→ usenpx prettieror an absolute path.
settings.json Changes Don't Apply
- Session restart required. Or
/config reload(if supported). - Could be silently ignored due to JSON syntax error. Validate with
jq ..
Next Steps
- Claude Code setup — prerequisite
- Agent runner — autonomous execution + hooks combo
- MCP servers — role differences vs hooks
- Multi-agent workflow — integrating subagents
References
Changelog
- 2026-05-12 — Initial draft (devAlice M2 seed expansion)