Guide for creating Claude Code hooks - shell commands that execute at specific lifecycle events (SessionStart, SessionEnd, PreToolUse, PostToolUse, etc.). Use when users want to automate actions, add validation, logging, or integrate external tools into Claude Code workflows.
Guide creating deterministic shell hooks that execute at lifecycle events (SessionStart, PreToolUse, PostToolUse, etc.). Use when users want to automate actions, validate tool inputs, log commands, or integrate external tools into Claude workflows.
/plugin marketplace add theflysurfer/claude-skills-marketplace/plugin install theflysurfer-claude-skills-marketplace@theflysurfer/claude-skills-marketplaceThis skill inherits all available tools. When active, it can use any tool Claude has access to.
references/hook-templates.mdreferences/troubleshooting.mdThis skill guides the creation of Claude Code hooks - deterministic shell commands or LLM prompts that execute at specific points in Claude's lifecycle.
Hooks provide deterministic control over Claude's behavior. Unlike skills (which Claude chooses to use), hooks always execute at their designated lifecycle event.
┌─────────────────────────────────────────────────────────────────┐
│ HOOKS vs SKILLS │
├─────────────────────────────────────────────────────────────────┤
│ HOOKS: Deterministic, always run at lifecycle events │
│ SKILLS: Model-invoked, Claude decides when to use │
└─────────────────────────────────────────────────────────────────┘
| Event | When It Runs | Common Use Cases |
|---|---|---|
SessionStart | Session begins/resumes | Load context, sync data, set env vars |
SessionEnd | Session ends | Cleanup, save state, push changes |
PreToolUse | Before tool execution | Validate, block, modify tool input |
PostToolUse | After tool completes | Format output, log, trigger actions |
PermissionRequest | Permission dialog shown | Auto-approve or deny permissions |
UserPromptSubmit | User submits prompt | Add context, validate requests |
Notification | Claude sends notification | Custom alerts |
Stop | Claude finishes responding | Decide if Claude should continue |
SubagentStop | Subagent completes | Evaluate task completion |
Hooks are configured in ~/.claude/settings.json (global) or .claude/settings.json (project).
{
"hooks": {
"EventName": [
{
"matcher": "ToolPattern",
"hooks": [
{
"type": "command",
"command": "your-command-here",
"timeout": 60
}
]
}
]
}
}
| Field | Required | Description |
|---|---|---|
matcher | For tool events | Pattern to match tool names (regex supported) |
type | Yes | "command" (shell) or "prompt" (LLM) |
command | For type:command | Shell command to execute |
prompt | For type:prompt | LLM prompt for evaluation |
timeout | No | Seconds before timeout (default: 60, max: 300) |
"matcher": "Write" // Exact match
"matcher": "Edit|Write" // OR pattern (regex)
"matcher": "Notebook.*" // Wildcard pattern
"matcher": "*" // All tools (or omit matcher)
Hooks receive JSON via stdin with context about the event:
{
"session_id": "abc123",
"transcript_path": "/path/to/transcript.jsonl",
"cwd": "/current/working/directory",
"hook_event_name": "PreToolUse",
"tool_name": "Write",
"tool_input": {
"file_path": "/path/to/file.txt",
"content": "file content"
}
}
| Exit Code | Behavior |
|---|---|
0 | Success - continue normally |
2 | Block - stderr fed to Claude, action blocked |
| Other | Non-blocking error (shown in verbose mode) |
{
"continue": true,
"stopReason": "message if continue=false",
"suppressOutput": true,
"systemMessage": "warning shown to user"
}
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow|deny|ask",
"permissionDecisionReason": "Reason here",
"updatedInput": {
"field": "modified value"
}
}
}
Ask:
~/.claude/settings.json) or project (.claude/settings.json)?Create script in ~/.claude/scripts/ or .claude/scripts/:
#!/bin/bash
# ~/.claude/scripts/my-hook.sh
# Read input from stdin
INPUT=$(cat)
# Parse with jq
TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // empty')
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
# Your logic here
if [[ "$FILE_PATH" == *".env"* ]]; then
echo "Blocked: Cannot modify .env files" >&2
exit 2 # Block the action
fi
exit 0 # Allow the action
Important: Make executable with chmod +x
Add to settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash ~/.claude/scripts/my-hook.sh",
"timeout": 10
}
]
}
]
}
}
# Test script directly
echo '{"tool_name":"Write","tool_input":{"file_path":"/test/.env"}}' | bash ~/.claude/scripts/my-hook.sh
echo "Exit code: $?"
#!/bin/bash
INPUT=$(cat)
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
PROTECTED=(".env" "package-lock.json" ".git/" "credentials")
for pattern in "${PROTECTED[@]}"; do
if [[ "$FILE_PATH" == *"$pattern"* ]]; then
echo "Protected file: $pattern" >&2
exit 2
fi
done
exit 0
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "FILE=$(cat | jq -r '.tool_input.file_path') && npx prettier --write \"$FILE\" 2>/dev/null || true"
}
]
}
]
}
}
#!/bin/bash
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
DESC=$(echo "$INPUT" | jq -r '.tool_input.description // "No description"')
echo "$(date +%Y-%m-%d_%H:%M:%S) | $CMD | $DESC" >> ~/.claude/logs/bash-commands.log
exit 0
{
"hooks": {
"SessionStart": [
{
"hooks": [{
"type": "command",
"command": "bash ~/.claude/scripts/sync-marketplace.sh",
"timeout": 30
}]
}
],
"SessionEnd": [
{
"hooks": [{
"type": "command",
"command": "bash ~/.claude/scripts/push-marketplace.sh",
"timeout": 30
}]
}
]
}
}
#!/bin/bash
# stdout is added as context to the prompt
echo "Current git branch: $(git branch --show-current 2>/dev/null || echo 'not a git repo')"
echo "Node version: $(node -v 2>/dev/null || echo 'not installed')"
exit 0
{
"hooks": {
"Stop": [
{
"hooks": [{
"type": "prompt",
"prompt": "Review if all tasks are complete. Check: 1) All todos marked done 2) Tests passing 3) No pending questions. Respond with decision: approve (stop) or block (continue).",
"timeout": 30
}]
}
]
}
}
"$VAR" not $VARtr -d '\r' for Windows CRLF compatibility# Run with debug output
bash -x ~/.claude/scripts/my-hook.sh
# Test with sample input
echo '{"tool_name":"Write","tool_input":{"file_path":"/test/file.txt"}}' | bash ~/.claude/scripts/my-hook.sh
# Check hook errors in Claude Code
# Look for "hook error" messages in the UI
For detailed troubleshooting of common errors (timeout, CRLF, jq not found, etc.), see references/troubleshooting.md.
Available in hooks:
CLAUDE_PROJECT_DIR - Current project directoryCLAUDE_CODE_REMOTE - Remote mode indicatorCLAUDE_ENV_FILE - (SessionStart only) File path for persisting env vars| Location | Scope |
|---|---|
~/.claude/settings.json | Global (all projects) |
.claude/settings.json | Project-specific |
.claude/settings.local.json | Local overrides (not committed) |
~/.claude/scripts/ | Global scripts |
.claude/scripts/ | Project scripts |
Event Flow:
SessionStart → UserPromptSubmit → PreToolUse → [Tool] → PostToolUse → Stop → SessionEnd
Exit Codes:
0 = Success (continue)
2 = Block (stop action, feed stderr to Claude)
* = Non-blocking error
Matcher:
"Write" = exact match
"Edit|Write" = OR
"Notebook.*" = regex
"*" or omit = all tools
~/.claude/settings.json) ou project (.claude/settings.json)jq installé pour parsing JSON~/.claude/scripts/ ou .claude/scripts/settings.jsonRecommandés:
Optionnels:
Read (lecture settings.json existant)Write (création scripts bash)Edit (modification settings.json)Bash (test du hook, chmod +x)User: "Je veux protéger les fichiers .env"
↓
hook-creator (this skill)
├─► Step 1: Identify event (PreToolUse)
├─► Step 2: Write script (protect-files.sh)
├─► Step 3: chmod +x script
├─► Step 4: Configure settings.json
└─► Step 5: Test with sample input
↓
Hook active ✅
↓
[Next: Test in real session]
Scenario: Créer un hook de logging des commandes bash
Input: "Log toutes les commandes bash exécutées"
Process:
PostToolUse avec matcher Bash~/.claude/scripts/log-bash.shResult:
~/.claude/logs/bash-commands.logThis skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.