Hooks System
Master Claude Code Hooks — 17 lifecycle events, 3 handler types, 10+ practical recipes and enterprise configuration
On This Page
- 1. Core Concepts
- 2. Complete Event List
- 3. Handler Types
- 4. Configuration
- 5. Practical Recipes
- Recipe 1: format automatically after a file is saved
- Recipe 2: block dangerous shell commands
- Recipe 3: protect sensitive files
- Recipe 4: system notification when a task completes
- Recipe 5: Slack notification
- Recipe 6: ESLint auto-fix
- Recipe 7: audit log
- Recipe 8: run tests automatically
- Recipe 9: protect migration files
- Recipe 10: inject context at session start
- 6. Enterprise Hooks
- 7. Debugging
- 8. Compared with Codex Hooks
- Next Steps
Hooks let you insert your own logic at key points in the Claude Code lifecycle — format code automatically, block dangerous commands, send notifications, write audit logs. Hooks are one of Claude Code's most powerful extension mechanisms.
1. Core Concepts¶
Every hook has three parts:
Event (When) → Matcher (Which) → Handler (What)
PreToolUse matcher: "Bash" command: "check_safety.sh"
- Event: when it fires (for example
PreToolUse— before a tool runs) - Matcher: an optional tool-name filter (only
Bash, or onlyWrite) - Handler: a shell command, a prompt injection, or a subagent
2. Complete Event List¶
Core events¶
| Event | Fires | Can block | Typical use |
|---|---|---|---|
| PreToolUse | Before a tool runs | Yes (exit 2) | Security interception, argument validation |
| PostToolUse | After a tool runs | No | Auto formatting, logging |
| Notification | Claude sends a notification | No | Slack / Feishu / DingTalk alerts |
| Stop | Claude finishes a response | No | Quality checks, auto commit |
| UserPromptSubmit | User submits a prompt | Yes | Context injection, policy checks |
| SessionStart | Session starts | No | Environment setup, welcome message |
Extended events (new in 2026)¶
| Event | Fires | Can block | Typical use |
|---|---|---|---|
| SubagentStop | A subagent finishes | No | Collect subagent results |
| SubagentToolUse | A subagent uses a tool | Yes | Restrict subagent permissions |
| FileChanged | A file is modified | No | Auto lint, trigger a build |
| CwdChanged | Working directory changes | No | Load directory-specific config |
| ModelChange | Model is switched | No | Track model usage |
| CompactComplete | /compact finishes | No | Post-compaction processing |
| ToolError | A tool errors out | No | Error collection, retry logic |
3. Handler Types¶
Type 1: Command (shell command)¶
The most common type — runs a shell command:
{
"type": "command",
"command": "npx prettier --write $FILEPATH"
}
Environment variables:
| Variable | Meaning | Available in |
|------|------|---------|
| $FILEPATH | Path of the file being operated on | PreToolUse/PostToolUse |
| $TOOL_INPUT | Tool input as JSON | PreToolUse |
| $TOOL_NAME | Tool name | PreToolUse/PostToolUse |
| $SESSION_ID | Session id | All |
| $NOTIFICATION_MESSAGE | Notification body | Notification |
stdin input: a hook script also receives a JSON payload on stdin containing session_id, tool_name and tool_input. Read it when you need structured data — it is more reliable than parsing the environment variables.
Exit codes:
0: continue2: block the operation (PreToolUse/UserPromptSubmit only)- anything else: treated as an error but does not block
Type 2: Prompt (prompt injection)¶
Injects text into Claude's context:
{
"type": "prompt",
"prompt": "Remember: every database operation must use a transaction"
}
Useful for: injecting extra project rules at SessionStart.
Type 3: Subagent¶
Spawns a subagent to handle the event:
{
"type": "subagent",
"prompt": "Review the code that was just modified and check for security vulnerabilities"
}
Useful for: automatic code-quality review after PostToolUse.
4. Configuration¶
Configure in .claude/settings.json (project level) or ~/.claude/settings.json (user level):
{
"hooks": {
"EventName": [
{
"matcher": "tool name (optional, | separates several)",
"hooks": [
{
"type": "command|prompt|subagent",
"command": "..."
}
]
}
]
}
}
5. Practical Recipes¶
Recipe 1: format automatically after a file is saved¶
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write $FILEPATH 2>/dev/null || true"
}
]
}
]
}
}
Recipe 2: block dangerous shell commands¶
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "echo $TOOL_INPUT | grep -qE 'rm -rf /|sudo rm|git push --force|DROP TABLE|DROP DATABASE' && echo 'dangerous command blocked' && exit 2 || exit 0"
}
]
}
]
}
}
Recipe 3: protect sensitive files¶
{
"hooks": {
"PreToolUse": [
{
"matcher": "Read|Write|Edit",
"hooks": [
{
"type": "command",
"command": "echo $TOOL_INPUT | grep -qE '\\.env|\\.env\\.|credentials|secrets?\\.ya?ml|private.key' && echo 'access to a sensitive file blocked' && exit 2 || exit 0"
}
]
}
]
}
}
Recipe 4: system notification when a task completes¶
macOS:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"$NOTIFICATION_MESSAGE\" with title \"Claude Code\"'"
}
]
}
]
}
}
Linux:
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "notify-send 'Claude Code' \"$NOTIFICATION_MESSAGE\""
}
]
}
]
}
}
Recipe 5: Slack notification¶
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "curl -s -X POST https://hooks.slack.com/services/xxx/yyy/zzz -H 'Content-Type: application/json' -d '{\"text\": \"Claude Code task finished\"}'"
}
]
}
]
}
}
Recipe 6: ESLint auto-fix¶
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx eslint --fix $FILEPATH 2>/dev/null; npx prettier --write $FILEPATH 2>/dev/null; exit 0"
}
]
}
]
}
}
Recipe 7: audit log¶
{
"hooks": {
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "echo \"$(date -u +%Y-%m-%dT%H:%M:%SZ) | session=$SESSION_ID | tool=$TOOL_NAME | file=$FILEPATH\" >> ~/.claude/audit.log"
}
]
}
]
}
}
Recipe 8: run tests automatically¶
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "echo $FILEPATH | grep -qE '\\.(ts|tsx|js|jsx)$' && echo $FILEPATH | grep -qvE '\\.test\\.|\\.spec\\.' && npx vitest related $FILEPATH --run 2>/dev/null || exit 0"
}
]
}
]
}
}
Recipe 9: protect migration files¶
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "echo $FILEPATH | grep -qE 'migrations/|alembic/versions/' && echo 'editing an existing migration is not allowed, create a new one' && exit 2 || exit 0"
}
]
}
]
}
}
Recipe 10: inject context at session start¶
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Important: this project is mid-way through a v3.0 rewrite. All new code must use React Server Components; the pages/ directory is no longer supported."
}
]
}
]
}
}
6. Enterprise Hooks¶
managed-settings.d/ configuration¶
Enterprise administrators can enforce an organisation-wide security policy through the managed-settings.d/ directory:
# Create a policy file in the enterprise management directory
mkdir -p /etc/claude-code/managed-settings.d/
// /etc/claude-code/managed-settings.d/security.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "/opt/claude-policy/check_command.sh"
}
]
}
],
"PostToolUse": [
{
"hooks": [
{
"type": "command",
"command": "/opt/claude-policy/audit_log.sh"
}
]
}
]
}
}
An enterprise policy cannot be overridden by user- or project-level configuration.
7. Debugging¶
Verbose mode¶
Press Ctrl+O to turn on verbose mode, which shows:
- when each hook fires
- the stdout/stderr of the hook script
- the hook's exit code
Common problems¶
| Problem | Cause | Fix |
|---|---|---|
| The hook never fires | Typo in the matcher | Check tool-name casing: Bash, Write, Edit, Read |
| The hook blocks normal work | Exit-code logic is wrong | Make sure the normal path is exit 0 and only block with exit 2 |
| The hook is slow | The script takes too long | A hook should finish in 1–2 seconds; run long jobs in the background |
| An environment variable is empty | That event does not provide it | Check the table above to see which variables the event supplies |
Test the hook script¶
Test it by hand in a terminal before putting it in the configuration:
# Simulate the PreToolUse environment variables
FILEPATH="src/main.ts" TOOL_INPUT="rm -rf /" bash -c 'echo $TOOL_INPUT | grep -qE "rm -rf" && echo "blocked" && exit 2 || exit 0'
8. Compared with Codex Hooks¶
| Claude Code Hooks | Codex Hooks | |
|---|---|---|
| Number of events | 17 | Fewer |
| Configuration | settings.json | config.toml |
| Handler types | command/prompt/subagent | command |
| Enterprise management | managed-settings.d/ | None |
| Blocking | exit code 2 | Limited |
Next Steps¶
- Skills — a more advanced extension mechanism
- MCP Servers — connect external tools
- Automation & CI/CD — using headless mode
- CLI Tips — command-line techniques