// 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.
| name | julien-dev-hook-creator |
| description | 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. |
| license | Apache-2.0 |
| metadata | {"author":"Julien","version":"1.0.0","category":"development"} |
| triggers | ["create hook","new hook","add hook","hook template","write hook","build hook","crรฉer hook","nouveau hook","ajouter hook","รฉcrire hook","SessionStart","SessionEnd","PreToolUse","PostToolUse","UserPromptSubmit","claude code hook","automation hook"] |
This 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.log