Hooks execute custom commands or prompts in response to Claude Code events. Use for automation, validation, formatting, and security.
All Hook Events
| Event | When Fired | Matcher Applies | Common Uses |
|---|---|---|---|
PreToolUse |
Before tool execution | Yes | Validation, blocking |
PermissionRequest |
When Claude requests permission | Yes | Auto-approval policies |
PostToolUse |
After successful tool execution | Yes | Formatting, linting |
Notification |
When Claude wants attention | Yes | Custom notifications |
UserPromptSubmit |
User submits prompt | No | Input validation |
Stop |
Claude finishes response | No | Cleanup, final checks |
SubagentStop |
Subagent completes | No | Result validation |
PreCompact |
Before context compaction | No | State backup |
SessionStart |
Session begins | No | Environment setup |
SessionEnd |
Session ends | No | Cleanup, persistence |
Configuration Locations
Precedence (highest to lowest)
- Managed -
managed-settings.json(enterprise) - Local -
.claude/settings.local.json(gitignored) - Project -
.claude/settings.json(shared via git) - User -
~/.claude/settings.json(personal) - Plugin -
hooks.jsonor frontmatter - Capability - Skill/Command/Agent frontmatter
Settings.json Format
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./scripts/validate.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "./scripts/cleanup.sh"
}
]
}
]
}
}
Frontmatter Format
---
hooks:
PreToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "./scripts/check.sh"
once: true
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: "./scripts/lint.sh"
---
Hook Structure
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./scripts/validate.sh",
"timeout": 60,
"once": true
}
]
}
Fields
| Field | Type | Required | Description |
|---|---|---|---|
matcher |
string | For tool events | Regex pattern (case-sensitive) |
type |
string | Yes | command or prompt |
command |
string | For command type | Bash command |
prompt |
string | For prompt type | LLM prompt |
timeout |
number | No | Seconds (default: 60) |
once |
boolean | No | Run once per session (Skills/Commands only) |
Matcher Syntax
| Pattern | Matches |
|---|---|
Write |
Exact match (case-sensitive) |
Edit|Write |
Either Edit or Write |
Notebook.* |
NotebookEdit, NotebookRead, etc. |
mcp__.*__write.* |
MCP write tools |
* or "" |
All tools |
Not applicable to: UserPromptSubmit, Stop, SubagentStop, PreCompact, SessionStart, SessionEnd
Hook I/O
Input (JSON via stdin)
{
"session_id": "abc123",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.py",
"content": "..."
},
"tool_use_id": "toolu_123",
"permission_mode": "default"
}
Exit Codes
| Code | Behavior |
|---|---|
| 0 | Success - execution continues |
| 2 | Blocking error - prevents action, shows stderr |
| Other | Non-blocking error - stderr in verbose mode |
JSON Output Control
For advanced control, output JSON to stdout:
PreToolUse Response
{
"decision": "allow",
"reason": "Validation passed",
"updatedInput": { "modified": "input" }
}
| Field | Values | Effect |
|---|---|---|
decision |
allow, block, ask |
Controls tool execution |
reason |
string | Shown to user if blocked |
updatedInput |
object | Modifies tool input |
Stop/SubagentStop Response
{
"decision": "continue",
"reason": "Task incomplete",
"additionalContext": "More details..."
}
| Field | Values | Effect |
|---|---|---|
decision |
continue, stop |
Continue or stop agent |
reason |
string | Shown if continuing |
additionalContext |
string | Added to context |
Override Field
{
"continue": false
}
continue: false forces stop regardless of other fields.
Prompt-Based Hooks
LLM-evaluated decisions (Stop/SubagentStop only).
{
"type": "prompt",
"prompt": "Verify the task was completed correctly. Check for edge cases. Return {\"ok\": true} if satisfied, {\"ok\": false, \"reason\": \"...\"} otherwise."
}
Use for: Context-aware validation, complex decision logic
Environment Variables
| Variable | Description | Available In |
|---|---|---|
CLAUDE_PROJECT_DIR |
Project root (absolute path) | All hooks |
CLAUDE_CODE_REMOTE |
"true" if remote, empty if local |
All hooks |
CLAUDE_ENV_FILE |
Path for persisting env vars | SessionStart only |
CLAUDE_PLUGIN_ROOT |
Plugin directory (absolute) | Plugin hooks |
Using in Commands
#!/bin/bash
"$CLAUDE_PROJECT_DIR/scripts/validate.sh"
Execution Details
| Aspect | Behavior |
|---|---|
| Parallelization | All matching hooks run in parallel |
| Deduplication | Identical commands deduplicated |
| Timeout | 60 seconds default per command |
| Hook order | Hooks from all sources execute together |
SessionStart for Environment
Persist environment variables across Bash commands:
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "echo 'conda activate myenv' >> \"$CLAUDE_ENV_FILE\""
}
]
}
]
}
}
The file at $CLAUDE_ENV_FILE is sourced before each Bash command.
Best Practices
- Use
$CLAUDE_PROJECT_DIRfor project-relative paths - Quote shell variables to handle spaces
- Validate/sanitize JSON input in scripts
- Use JSON output for complex control logic
- Implement logging for debugging
- Test in safe environments before production
- Use prompt-based hooks for context-aware decisions
- Use bash hooks for deterministic rules
- Keep hooks fast - timeout is 60 seconds
- Use
once: truefor one-time setup in Skills/Commands
Examples
Code Formatting
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "prettier --write \"$CLAUDE_PROJECT_DIR\"/**/*.{js,ts,json}"
}
]
}
]
}
}
File Protection
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "./scripts/check-protected-files.sh"
}
]
}
]
}
}
Custom Notifications
{
"hooks": {
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "osascript -e 'display notification \"Claude needs attention\"'"
}
]
}
]
}
}
Task Verification
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Verify task completion. Check edge cases. Return {\"ok\": true} or {\"ok\": false, \"reason\": \"...\"}."
}
]
}
]
}
}
Plugin Hook
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PLUGIN_ROOT}/scripts/format.sh"
}
]
}
]
}
}