7. Hooks
📌 Section 7 TL;DR (60 seconds)
Section titled “📌 Section 7 TL;DR (60 seconds)”What are Hooks: Scripts that run automatically on events (like git hooks)
Event types:
PreToolUse→ Before Claude runs a tool (e.g., block dangerous commands)PostToolUse→ After Claude runs a tool (e.g., auto-format code)UserPromptSubmit→ When you send a message (e.g., inject context)
Common use cases:
- 🛡️ Security: Block file deletions, prevent secrets in commits
- 🎨 Quality: Auto-format, lint, run tests
- 📊 Logging: Track commands, audit changes
Quick Start: See 7.3 Hook Templates for copy-paste examples
Read this section if: You want automation or need safety guardrails Skip if: Manual control is sufficient for your workflow
Reading time: 20 minutes Skill level: Week 2-3 Goal: Automate Claude Code with event-driven scripts
7.1 The Event System
Section titled “7.1 The Event System”Hooks are scripts that run automatically when specific events occur.
Event Types
Section titled “Event Types”| Event | When It Fires | Can Block? | Use Case |
|---|---|---|---|
SessionStart | Session begins or resumes | No | Initialization, load dev context |
UserPromptSubmit | User submits prompt, before Claude processes it | Yes | Context enrichment, prompt validation |
PreToolUse | Before a tool call executes | Yes | Security validation, input modification |
PermissionRequest | Permission dialog appears | Yes | Custom approval logic |
PostToolUse | After a tool completes successfully | No | Formatting, logging |
PostToolUseFailure | After a tool call fails | No | Error logging, recovery actions |
Notification | Claude sends notification | No | Sound alerts, custom notifications |
SubagentStart | Sub-agent spawned | No | Subagent initialization |
SubagentStop | Sub-agent finishes | Yes | Subagent cleanup |
Stop | Claude finishes responding | Yes | Post-response actions, continue loops |
TeammateIdle | Agent team member about to go idle | Yes | Team coordination, quality gates |
TaskCompleted | Task being marked as completed | Yes | Enforce completion criteria |
ConfigChange | Config file changes during session | Yes (except policy) | Enterprise audit, block unauthorized changes |
WorktreeCreate | Worktree being created | Yes (non-zero exit) | Custom VCS setup |
WorktreeRemove | Worktree being removed | No | Clean up VCS state |
PreCompact | Before context compaction | No | Save state before compaction |
SessionEnd | Session terminates | No | Cleanup, logging |
StopandSubagentStop—last_assistant_messagefield (v2.1.47+): These events now include alast_assistant_messagefield in their JSON input, giving direct access to Claude’s final response without parsing transcript files. Useful for orchestration pipelines that need to inspect or log the last output.Terminal window # In your Stop hook scriptLAST_MSG=$(cat | jq -r '.last_assistant_message // ""')echo "$LAST_MSG" >> ~/.claude/logs/session-outputs.log
Event Flow
Section titled “Event Flow”┌─────────────────────────────────────────────────────────┐│ EVENT FLOW │├─────────────────────────────────────────────────────────┤│ ││ User types message ││ │ ││ ▼ ││ ┌────────────────────┐ ││ │ UserPromptSubmit │ ← Add context (git status) ││ └────────────────────┘ ││ │ ││ ▼ ││ Claude decides to run tool (e.g., Edit) ││ │ ││ ▼ ││ ┌────────────────────┐ ││ │ PreToolUse │ ← Security check ││ └────────────────────┘ ││ │ ││ ▼ (if allowed) ││ Tool executes ││ │ ││ ▼ ││ ┌────────────────────┐ ││ │ PostToolUse │ ← Auto-format ││ └────────────────────┘ ││ │└─────────────────────────────────────────────────────────┘Hook Execution Model (v2.1.0+)
Section titled “Hook Execution Model (v2.1.0+)”Claude Code supports two execution models for hooks:
Synchronous (Default)
Section titled “Synchronous (Default)”- Claude blocks until the hook completes
- Exit code and stdout available immediately for feedback
- Use case: Critical validation (security, type checking, blocking operations)
- Configuration: Omit
asyncor setasync: false
Asynchronous (Optional)
Section titled “Asynchronous (Optional)”- Claude continues immediately, hook runs in background
- Exit code/stdout NOT available to Claude (no feedback loop)
- Use case: Non-critical operations (logging, notifications, formatting, metrics)
- Configuration: Add
async: trueto hook definition
Configuration Example
Section titled “Configuration Example”{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/auto-format.sh", "timeout": 10000, "async": true // ← Non-blocking execution }, { "type": "command", "command": ".claude/hooks/typecheck.sh" // Sync by default - blocks on completion } ] } ] }}Decision Matrix
Section titled “Decision Matrix”| Hook Purpose | Execution Mode | Reason |
|---|---|---|
| Code formatting (Prettier, Black) | Async | Cosmetic change, no feedback needed |
| Linting with auto-fix (eslint —fix) | Async | Non-critical improvements |
| Type checking (tsc, mypy) | Sync | Errors must block for iteration |
| Security validation | Sync | Must block dangerous operations |
| Logging/metrics | Async | Pure side-effect, no feedback |
| Notifications (Slack, email) | Async | User alerts, non-blocking |
| Test execution | Sync | Results influence next action |
| Git context injection | Sync | Enriches prompt before processing |
Performance Impact
Section titled “Performance Impact”Example session (10 file edits):
- Sync hooks:
auto-format.sh(500ms) × 10 = 5s blocked - Async hooks:
auto-format.shruns in background = 0s blocked - Gain: ~5-10s per typical development session
Limitations of Async Hooks
Section titled “Limitations of Async Hooks”⚠️ Async hooks cannot:
- Block Claude on errors (exit code 2 ignored)
- Provide real-time feedback via stdout or
systemMessage - Guarantee execution order with other hooks
- Return
additionalContextthat Claude can use
Use async only when the hook’s completion is truly independent of Claude’s workflow.
When Async Was Introduced
Section titled “When Async Was Introduced”- v2.1.0: Initial async hook support (configuration via
async: true) - v2.1.23: Fixed bug where async hooks weren’t properly cancelled when headless streaming sessions ended
Shell Scripts vs AI Agents: When to Use What
Section titled “Shell Scripts vs AI Agents: When to Use What”Not everything needs AI. Choose the right tool:
| Task Type | Best Tool | Why | Example |
|---|---|---|---|
| Deterministic | Bash script | Fast, predictable, no tokens | Create branch, fetch PR comments |
| Pattern-based | Bash + regex | Reliable for known patterns | Check for secrets, validate format |
| Interpretation needed | AI Agent | Judgment required | Code review, architecture decisions |
| Context-dependent | AI Agent | Needs understanding | ”Does this match requirements?” |
Rule of thumb: If you can write a regex or a simple conditional for it, use a bash script. If it requires “understanding” or “judgment”, use an agent.
Example — PR workflow:
# Deterministic (bash): create branch, push, open PRgit checkout -b feature/xyzgit push -u origin feature/xyzgh pr create --title "..." --body "..."
# Interpretation (agent): review code quality# → Use code-review subagentWhy this matters: Bash scripts are instant, free (no tokens), and 100% predictable. Reserve AI for tasks that genuinely need intelligence.
Inspired by Nick Tune’s Coding Agent Development Workflows
7.2 Creating Hooks
Section titled “7.2 Creating Hooks”Hook Registration (settings.json)
Section titled “Hook Registration (settings.json)”{ "hooks": { "PreToolUse": [ { "matcher": "Bash|Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/security-check.sh", "timeout": 5000 } ] } ] }}Configuration Fields
Section titled “Configuration Fields”| Field | Description |
|---|---|
matcher | Regex pattern filtering when hooks fire (tool name, session start reason, etc.) |
type | Hook type: "command", "http", "prompt", or "agent" |
command | Shell command to run (for command type) |
prompt | Prompt text for LLM evaluation (for prompt/agent types). Use $ARGUMENTS as placeholder for hook input JSON |
timeout | Max execution time in seconds (default: 600s command, 30s prompt, 60s agent) |
model | Model to use for evaluation (for prompt/agent types). Defaults to a fast model |
async | If true, runs in background without blocking (for command type only) |
statusMessage | Custom spinner message displayed while hook runs |
once | If true, runs only once per session then is removed (skills only) |
Hook types:
command: Runs a shell command. Receives JSON on stdin, returns JSON on stdout. Most common type.http(v2.1.63+): POSTs JSON to a URL and reads JSON response. Useful for CI/CD webhooks and stateless backend integrations without shell dependencies. Configure withurland optionalallowedEnvVarsfor header interpolation.prompt: Sends prompt + hook input to a Claude model (Haiku by default) for single-turn evaluation. Returns{ok: true/false, reason: "..."}. Configure model viamodelfield.agent: Spawns a subagent with tool access (Read, Grep, Glob, etc.) for multi-turn verification. Returns same{ok: true/false}format. Up to 50 tool-use turns.
HTTP hook example (v2.1.63+):
{ "hooks": { "PostToolUse": [ { "matcher": "Write|Edit", "hooks": [ { "type": "http", "url": "https://ci.example.com/webhook/claude-hook", "allowedEnvVars": ["CI_TOKEN"] } ] } ] }}HTTP hooks receive the same JSON payload as command hooks and must return valid JSON. The allowedEnvVars field lists environment variables that can be referenced in headers (e.g., for Bearer token authentication).
Hook Input (stdin JSON)
Section titled “Hook Input (stdin JSON)”Hooks receive JSON on stdin with common fields (all events) plus event-specific fields:
{ "session_id": "abc123", "transcript_path": "/home/user/.claude/projects/.../transcript.jsonl", "cwd": "/project", "permission_mode": "default", "hook_event_name": "PreToolUse", "tool_name": "Bash", "tool_input": { "command": "git status" }}Common fields (all events):
session_id,transcript_path,cwd,permission_mode,hook_event_name. Event-specific fields (liketool_nameandtool_inputfor PreToolUse) are added on top.
Hook Output
Section titled “Hook Output”Hooks communicate results through exit codes and optional JSON on stdout. Choose one approach per hook: either exit codes alone, or exit 0 with JSON for structured control (Claude Code only processes JSON on exit 0).
Universal JSON fields (all events):
| Field | Default | Description |
|---|---|---|
continue | true | If false, Claude stops processing entirely |
stopReason | none | Message shown to user when continue is false |
suppressOutput | false | If true, hides stdout from verbose mode |
systemMessage | none | Warning message shown to user |
Event-specific decision control varies by event type:
- PreToolUse: Uses
hookSpecificOutputwithpermissionDecision(allow/deny/ask),permissionDecisionReason,updatedInput,additionalContext - PostToolUse, Stop, SubagentStop, UserPromptSubmit, ConfigChange: Uses top-level
decision: "block"withreason - TeammateIdle, TaskCompleted: Exit code 2 only (no JSON decision control)
- PermissionRequest: Uses
hookSpecificOutputwithdecision.behavior(allow/deny)
PreToolUse blocking example (preferred over exit code 2):
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "permissionDecision": "deny", "permissionDecisionReason": "Destructive command blocked by hook" }}PreToolUse context injection:
{ "hookSpecificOutput": { "hookEventName": "PreToolUse", "additionalContext": "Current git branch: feature/auth. 3 uncommitted files." }}Exit Codes
Section titled “Exit Codes”| Code | Meaning | Result |
|---|---|---|
0 | Success | Allow operation, parse stdout for JSON output |
2 | Blocking error | Prevent operation (for blocking events), stderr fed to Claude |
| Other | Non-blocking error | Stderr shown in verbose mode (Ctrl+O), execution continues |
7.3 Hook Templates
Section titled “7.3 Hook Templates”Template 1: PreToolUse (Security Blocker)
Section titled “Template 1: PreToolUse (Security Blocker)”#!/bin/bash# Blocks dangerous commands
INPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# List of dangerous patternsDANGEROUS_PATTERNS=( "rm -rf /" "rm -rf ~" "rm -rf *" "sudo rm" "git push --force origin main" "git push -f origin main" "npm publish" "> /dev/sda")
# Check if command matches any dangerous patternfor pattern in "${DANGEROUS_PATTERNS[@]}"; do if [[ "$COMMAND" == *"$pattern"* ]]; then echo "BLOCKED: Dangerous command detected: $pattern" >&2 exit 2 fidone
exit 0Template 2: PostToolUse (Auto-Formatter)
Section titled “Template 2: PostToolUse (Auto-Formatter)”#!/bin/bash# Auto-formats code after edits
INPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Only run for Edit/Write operationsif [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then exit 0fi
# Get the file pathFILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
# Skip if no file pathif [[ -z "$FILE_PATH" ]]; then exit 0fi
# Run Prettier on supported filesif [[ "$FILE_PATH" =~ \.(ts|tsx|js|jsx|json|md|css|scss)$ ]]; then npx prettier --write "$FILE_PATH" 2>/dev/nullfi
exit 0Template 3: UserPromptSubmit (Context Enricher)
Section titled “Template 3: UserPromptSubmit (Context Enricher)”#!/bin/bash# Adds git context to every prompt
# Get git informationBRANCH=$(git branch --show-current 2>/dev/null || echo "not a git repo")LAST_COMMIT=$(git log -1 --format='%h %s' 2>/dev/null || echo "no commits")STAGED=$(git diff --cached --stat 2>/dev/null | tail -1 || echo "none")UNSTAGED=$(git diff --stat 2>/dev/null | tail -1 || echo "none")
# Output JSON with contextcat << EOF{ "hookSpecificOutput": { "additionalContext": "[Git] Branch: $BRANCH | Last: $LAST_COMMIT | Staged: $STAGED | Unstaged: $UNSTAGED" }}EOF
exit 0Template 4: Notification (Sound Alerts)
Section titled “Template 4: Notification (Sound Alerts)”#!/bin/bash# Plays sounds on notifications (macOS)
INPUT=$(cat)TITLE=$(echo "$INPUT" | jq -r '.title // ""')MESSAGE=$(echo "$INPUT" | jq -r '.message // ""')TYPE=$(echo "$INPUT" | jq -r '.notification_type // ""')
# Determine sound based on contentif [[ "$TITLE" == *"error"* ]] || [[ "$MESSAGE" == *"failed"* ]]; then SOUND="/System/Library/Sounds/Basso.aiff"elif [[ "$TITLE" == *"complete"* ]] || [[ "$MESSAGE" == *"success"* ]]; then SOUND="/System/Library/Sounds/Hero.aiff"else SOUND="/System/Library/Sounds/Pop.aiff"fi
# Play sound (macOS)afplay "$SOUND" 2>/dev/null &
exit 0Windows Hook Templates
Section titled “Windows Hook Templates”Windows users can create hooks using PowerShell (.ps1) or batch files (.cmd).
Note: Windows hooks should use the full PowerShell invocation with
-ExecutionPolicy Bypassto avoid execution policy restrictions.
Template W1: PreToolUse Security Check (PowerShell)
Section titled “Template W1: PreToolUse Security Check (PowerShell)”Create .claude/hooks/security-check.ps1:
# Blocks dangerous commands
$inputJson = [Console]::In.ReadToEnd() | ConvertFrom-Json$command = $inputJson.tool_input.command
# List of dangerous patterns$dangerousPatterns = @( "rm -rf /", "rm -rf ~", "Remove-Item -Recurse -Force C:\", "git push --force origin main", "git push -f origin main", "npm publish")
foreach ($pattern in $dangerousPatterns) { if ($command -like "*$pattern*") { Write-Error "BLOCKED: Dangerous command detected: $pattern" exit 2 }}
exit 0Template W2: PostToolUse Auto-Formatter (PowerShell)
Section titled “Template W2: PostToolUse Auto-Formatter (PowerShell)”Create .claude/hooks/auto-format.ps1:
# Auto-formats code after edits
$inputJson = [Console]::In.ReadToEnd() | ConvertFrom-Json$toolName = $inputJson.tool_name
if ($toolName -ne "Edit" -and $toolName -ne "Write") { exit 0}
$filePath = $inputJson.tool_input.file_path
if (-not $filePath) { exit 0}
if ($filePath -match '\.(ts|tsx|js|jsx|json|md|css|scss)$') { npx prettier --write $filePath 2>$null}
exit 0Template W3: Context Enricher (Batch File)
Section titled “Template W3: Context Enricher (Batch File)”Create .claude/hooks/git-context.cmd:
@echo offsetlocal enabledelayedexpansion
for /f "tokens=*" %%i in ('git branch --show-current 2^>nul') do set BRANCH=%%iif "%BRANCH%"=="" set BRANCH=not a git repo
for /f "tokens=*" %%i in ('git log -1 --format^="%%h %%s" 2^>nul') do set LAST_COMMIT=%%iif "%LAST_COMMIT%"=="" set LAST_COMMIT=no commits
echo {"hookSpecificOutput":{"additionalContext":"[Git] Branch: %BRANCH% | Last: %LAST_COMMIT%"}}exit /b 0Template W4: Notification (Windows)
Section titled “Template W4: Notification (Windows)”Create .claude/hooks/notification.ps1:
# Shows Windows toast notifications and plays sounds
$inputJson = [Console]::In.ReadToEnd() | ConvertFrom-Json$title = $inputJson.title$message = $inputJson.message
# Determine sound based on contentif ($title -match "error" -or $message -match "failed") { [System.Media.SystemSounds]::Hand.Play()} elseif ($title -match "complete" -or $message -match "success") { [System.Media.SystemSounds]::Asterisk.Play()} else { [System.Media.SystemSounds]::Beep.Play()}
# Optional: Show Windows Toast Notification (requires BurntToast module)# Install-Module -Name BurntToast# New-BurntToastNotification -Text $title, $body
exit 0Windows settings.json for Hooks
Section titled “Windows settings.json for Hooks”{ "hooks": { "PreToolUse": [ { "matcher": "Bash|Edit|Write", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File .claude/hooks/security-check.ps1", "timeout": 5000 } ] } ], "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": "powershell -ExecutionPolicy Bypass -File .claude/hooks/auto-format.ps1", "timeout": 10000 } ] } ] }}7.4 Security Hooks
Section titled “7.4 Security Hooks”Security hooks are critical for protecting your system.
Advanced patterns: For comprehensive security including Unicode injection detection, MCP config integrity verification, and CVE-specific mitigations, see Security Hardening Guide.
Claude Code Security (research preview): Anthropic offers a dedicated codebase vulnerability scanner that traces data flows across files, challenges findings internally before surfacing them (adversarial validation), and generates patch suggestions. Separate from the Security Auditor Agent above — waitlist access only. See Security Hardening Guide → Claude Code as Security Scanner.
Validated at scale: In a March 2026 partnership with Mozilla, Claude Opus 4.6 scanned ~6,000 C++ files in Firefox’s JS engine in two weeks, surfacing 22 confirmed vulnerabilities (14 high severity) — roughly one fifth of all high-severity Firefox CVEs fixed in 2025. Demonstrates the model’s practical depth for production security work, well beyond surface-level linting.
Recommended Security Rules
Section titled “Recommended Security Rules”#!/bin/bashINPUT=$(cat)COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // ""')
# === CRITICAL BLOCKS (Exit 2) ===
# Filesystem destruction[[ "$COMMAND" =~ rm.*-rf.*[/~] ]] && { echo "BLOCKED: Recursive delete of root/home" >&2; exit 2; }
# Disk operations[[ "$COMMAND" =~ ">/dev/sd" ]] && { echo "BLOCKED: Direct disk write" >&2; exit 2; }[[ "$COMMAND" =~ "dd if=" ]] && { echo "BLOCKED: dd command" >&2; exit 2; }
# Git force operations on protected branches[[ "$COMMAND" =~ "git push".*"-f".*"(main|master)" ]] && { echo "BLOCKED: Force push to main" >&2; exit 2; }[[ "$COMMAND" =~ "git push --force".*"(main|master)" ]] && { echo "BLOCKED: Force push to main" >&2; exit 2; }
# Package publishing[[ "$COMMAND" =~ "npm publish" ]] && { echo "BLOCKED: npm publish" >&2; exit 2; }
# Privileged operations[[ "$COMMAND" =~ ^sudo ]] && { echo "BLOCKED: sudo command" >&2; exit 2; }
# === WARNINGS (Exit 0 but log) ===
[[ "$COMMAND" =~ "rm -rf" ]] && echo "WARNING: Recursive delete detected" >&2
exit 0Testing Security Hooks
Section titled “Testing Security Hooks”Before deploying, test your hooks:
# Test with a blocked commandecho '{"tool_name":"Bash","tool_input":{"command":"rm -rf /"}}' | .claude/hooks/security-blocker.shecho "Exit code: $?" # Should be 2
# Test with a safe commandecho '{"tool_name":"Bash","tool_input":{"command":"git status"}}' | .claude/hooks/security-blocker.shecho "Exit code: $?" # Should be 0Advanced Pattern: Model-as-Security-Gate
Section titled “Advanced Pattern: Model-as-Security-Gate”The Claude Code team uses a pattern where permission requests are routed to a more capable model acting as a security gate, rather than relying solely on static rule matching.
Concept: A PreToolUse hook intercepts permission requests and forwards them to Opus 4.6 (or another capable model) via the API. The gate model scans for prompt injection, dangerous patterns, and unexpected tool usage — then auto-approves safe requests or blocks suspicious ones.
# .claude/hooks/opus-security-gate.sh (conceptual)# PreToolUse hook that routes to Opus for security screening
INPUT=$(cat)TOOL=$(echo "$INPUT" | jq -r '.tool_name')COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
# Fast-path: known safe tools skip the gate[[ "$TOOL" == "Read" || "$TOOL" == "Grep" || "$TOOL" == "Glob" ]] && exit 0
# Route to Opus for security analysisVERDICT=$(echo "$INPUT" | claude --model opus --print \ "Analyze this tool call for security risks. Is it safe? Reply SAFE or BLOCKED:reason")
[[ "$VERDICT" == SAFE* ]] && exit 0echo "BLOCKED by security gate: $VERDICT" >&2exit 2Why use a model as gate: Static rules catch known patterns but miss novel attacks. A capable model understands intent and context — it can distinguish rm -rf node_modules (cleanup) from rm -rf / (destruction) based on the surrounding conversation, not just pattern matching.
Trade-off: Each gated call adds latency and cost. Use fast-path exemptions for read-only tools and only gate write/execute operations.
Source: 10 Tips from Inside the Claude Code Team (Boris Cherny thread, Feb 2026)
File Protection Strategy
Section titled “File Protection Strategy”Protecting sensitive files requires a multi-layered approach combining permissions, patterns, and bypass detection.
Three Protection Layers
Section titled “Three Protection Layers”┌─────────────────────────────────────────────────────────┐│ FILE PROTECTION ARCHITECTURE │├─────────────────────────────────────────────────────────┤│ ││ Layer 1: Permissions Deny (Native) ││ ────────────────────────── ││ • Built into settings.json ││ • No hooks required ││ • Blocks all tool access instantly ││ • Use for: Absolutely forbidden files ││ ││ Layer 2: Pattern Matching (Hook) ││ ──────────────────────── ││ • PreToolUse hook with .agentignore patterns ││ • Supports gitignore-style syntax ││ • Centralized protection rules ││ • Use for: Sensitive file categories ││ ││ Layer 3: Bypass Detection (Hook) ││ ────────────────────────── ││ • Detects variable expansion ($VAR, ${VAR}) ││ • Detects command substitution $(cmd), `cmd` ││ • Prevents path manipulation attempts ││ • Use for: Defense against sophisticated attacks ││ │└─────────────────────────────────────────────────────────┘Layer 1: permissions.deny
Section titled “Layer 1: permissions.deny”{ "permissions": { "deny": [ ".env", ".env.local", ".env.production", "**/*.key", "**/*.pem", "credentials.json", ".aws/credentials" ] }}Pros: Instant blocking, no hooks needed Cons: No custom logic, cannot log attempts
Layer 2: .agentignore Pattern File
Section titled “Layer 2: .agentignore Pattern File”Create .agentignore (or .aiignore) in your project root:
# Credentials.env**.key*.pem*.p12credentials.jsonsecrets.yaml
# Configconfig/secrets/.aws/credentials.ssh/id_*
# Build artifacts (if generated from secrets)dist/.envbuild/config/production.jsonUnified hook (See: examples/hooks/bash/file-guard.sh):
# Reads .agentignore and blocks matching files# Also detects bash bypass attemptsPros: Gitignore syntax familiar, centralized rules, version controlled Cons: Requires hook implementation
Layer 3: Bypass Detection
Section titled “Layer 3: Bypass Detection”Sophisticated attacks may try to bypass protection using variable expansion:
# Attack attemptsFILE="sensitive.key"cat $FILE # Variable expansion bypass
HOME_DIR=$HOMEcat $HOME_DIR/.env # Variable substitution bypass
cat $(echo ".env") # Command substitution bypassThe file-guard.sh hook detects these patterns:
# Detection logicdetect_bypass() { local file="$1"
# Variable expansion [[ "$file" =~ \$\{?[A-Za-z_][A-Za-z0-9_]*\}? ]] && return 0
# Command substitution [[ "$file" =~ \$\( || "$file" =~ \` ]] && return 0
return 1}Complete Protection Example
Section titled “Complete Protection Example”1. Configure settings.json:
{ "permissions": { "deny": [".env", "*.key", "*.pem"] }, "hooks": { "PreToolUse": [ { "matcher": "Read|Write|Edit", "hooks": [ { "type": "command", "command": ".claude/hooks/file-guard.sh", "timeout": 2000 } ] } ] }}2. Create .agentignore:
.env*config/secrets/**/*.key**/*.pemcredentials.json3. Copy hook template:
cp examples/hooks/bash/file-guard.sh .claude/hooks/chmod +x .claude/hooks/file-guard.shTesting Protection
Section titled “Testing Protection”# Test direct accessecho '{"tool_name":"Read","tool_input":{"file_path":".env"}}' | \ .claude/hooks/file-guard.sh# Should exit 1 and show "File access blocked"
# Test bypass attemptecho '{"tool_name":"Read","tool_input":{"file_path":"$HOME/.env"}}' | \ .claude/hooks/file-guard.sh# Should exit 1 and show "Variable expansion detected"Cross-reference: For full security hardening including CVE-specific mitigations and MCP config integrity, see Security Hardening Guide.
7.5 Hook Examples
Section titled “7.5 Hook Examples”Smart Hook Dispatching
Section titled “Smart Hook Dispatching”Instead of configuring dozens of individual hooks, use a single dispatcher that routes events intelligently based on file type, tool, and context.
The problem: As your hook collection grows, settings.json becomes unwieldy with repeated matchers and overlapping configurations.
The solution: One entry point that dispatches to specialized handlers.
#!/bin/bash# Single entry point for all PostToolUse hooks# Routes to specialized handlers based on file type and tool
INPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // .tool_input.command // ""')EVENT=$(echo "$INPUT" | jq -r '.hook_event_name // "unknown"')
HOOKS_DIR="$(dirname "$0")/handlers"
# Route by file extensioncase "$FILE_PATH" in *.ts|*.tsx) [[ -x "$HOOKS_DIR/typescript.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/typescript.sh" ;; *.py) [[ -x "$HOOKS_DIR/python.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/python.sh" ;; *.rs) [[ -x "$HOOKS_DIR/rust.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/rust.sh" ;; *.sql|*.prisma) [[ -x "$HOOKS_DIR/database.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/database.sh" ;;esac
# Route by tool (always runs, regardless of file type)case "$TOOL_NAME" in Bash) [[ -x "$HOOKS_DIR/security.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/security.sh" ;; Write) [[ -x "$HOOKS_DIR/new-file.sh" ]] && echo "$INPUT" | "$HOOKS_DIR/new-file.sh" ;;esac
exit 0Configuration (minimal settings.json):
{ "hooks": { "PostToolUse": [{ "matcher": "Edit|Write|Bash", "hooks": [{ "type": "command", "command": ".claude/hooks/dispatch.sh" }] }] }}Handler directory structure:
.claude/hooks/├── dispatch.sh # Single entry point└── handlers/ ├── typescript.sh # ESLint + tsc for .ts/.tsx ├── python.sh # Ruff + mypy for .py ├── rust.sh # cargo clippy for .rs ├── database.sh # Schema validation for .sql/.prisma ├── security.sh # Block dangerous bash commands └── new-file.sh # Check naming conventions on WriteBenefits over individual hooks:
- Single matcher in settings.json (instead of N matchers)
- Easy to extend: Drop a new handler in
handlers/, no config change needed - Language-aware: Different validation per file type
- Composable: File-type hooks and tool hooks both run when applicable
- Debuggable:
echo "$INPUT" | .claude/hooks/dispatch.shtests the full chain
Example 1: Activity Logger
Section titled “Example 1: Activity Logger”#!/bin/bash# Logs all tool usage to JSONL file
INPUT=$(cat)LOG_DIR="$HOME/.claude/logs"LOG_FILE="$LOG_DIR/activity-$(date +%Y-%m-%d).jsonl"
# Create log directorymkdir -p "$LOG_DIR"
# Clean up old logs (keep 7 days)find "$LOG_DIR" -name "activity-*.jsonl" -mtime +7 -delete
# Extract tool infoTOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")SESSION_ID=$(echo "$INPUT" | jq -r '.session_id')
# Create log entryLOG_ENTRY=$(jq -n \ --arg timestamp "$TIMESTAMP" \ --arg tool "$TOOL_NAME" \ --arg session "$SESSION_ID" \ '{timestamp: $timestamp, tool: $tool, session: $session}')
# Append to logecho "$LOG_ENTRY" >> "$LOG_FILE"
exit 0Example 2: Linting Gate
Section titled “Example 2: Linting Gate”#!/bin/bash# Runs linter after code changes
INPUT=$(cat)TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name')
# Only check after Edit/Writeif [[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]]; then exit 0fi
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // ""')
# Only lint TypeScript/JavaScriptif [[ ! "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]]; then exit 0fi
# Run ESLintLINT_OUTPUT=$(npx eslint "$FILE_PATH" 2>&1)LINT_EXIT=$?
if [[ $LINT_EXIT -ne 0 ]]; then cat << EOF{ "systemMessage": "Lint errors found in $FILE_PATH:\n$LINT_OUTPUT"}EOFfi
exit 0Validation Pipeline Pattern
Section titled “Validation Pipeline Pattern”Chain multiple validation hooks to catch issues immediately after code changes. This pattern ensures code quality without manual intervention.
The Pattern
Section titled “The Pattern”Edit/Write → TypeCheck → Lint → Tests → Notify Claude ↓ ↓ ↓ ↓ file.ts tsc check eslint jest file.test.tsBenefits:
- Catch errors immediately (before next Claude action)
- No need to manually run
npm run typecheck && npm run lint && npm test - Fast feedback loop → faster iteration
- Prevents cascading errors (Claude gets quality signal early)
Three-Stage Pipeline Configuration
Section titled “Three-Stage Pipeline Configuration”{ "hooks": { "PostToolUse": [ { "matcher": "Edit|Write", "hooks": [ { "type": "command", "command": ".claude/hooks/typecheck-on-save.sh", "timeout": 5000 }, { "type": "command", "command": ".claude/hooks/lint-gate.sh", "timeout": 5000 }, { "type": "command", "command": ".claude/hooks/test-on-change.sh", "timeout": 10000 } ] } ] }}Hook order matters: Run fast checks first (typecheck ~1s), then slower ones (tests ~3-5s).
Stage 1: Type Checking
Section titled “Stage 1: Type Checking”See: examples/hooks/bash/typecheck-on-save.sh
# Runs tsc on TypeScript files after edits# Only reports errors (not warnings)# Timeout: 5s (should be fast)What it catches:
- Type mismatches
- Missing imports
- Invalid property access
- Generic constraints violations
Stage 2: Linting
Section titled “Stage 2: Linting”Already documented in Example 2 above (lint-gate.sh).
What it catches:
- Code style violations
- Unused variables
- Missing semicolons
- Import order issues
Stage 3: Test Execution
Section titled “Stage 3: Test Execution”See: examples/hooks/bash/test-on-change.sh
# Detects associated test file and runs it# Supports: Jest (.test.ts), Pytest (_test.py), Go (_test.go)# Only runs if test file existsTest file detection logic:
| Source File | Test File Patterns |
|---|---|
auth.ts | auth.test.ts, __tests__/auth.test.ts |
utils.py | utils_test.py, test_utils.py |
main.go | main_test.go |
What it catches:
- Broken functionality
- Regression failures
- Edge case violations
- Integration issues
Smart Execution: Skip When Irrelevant
Section titled “Smart Execution: Skip When Irrelevant”All three hooks check conditions before running:
# Only run on Edit/Write[[ "$TOOL_NAME" != "Edit" && "$TOOL_NAME" != "Write" ]] && exit 0
# Only run on specific file types[[ ! "$FILE_PATH" =~ \.(ts|tsx|js|jsx)$ ]] && exit 0
# Only run if config exists[[ ! -f "tsconfig.json" ]] && exit 0This prevents wasted execution on README edits, config changes, or non-code files.
Performance Considerations
Section titled “Performance Considerations”| Project Size | Pipeline Time | Acceptable? |
|---|---|---|
| Small (<100 files) | ~1-2s per edit | ✅ Yes |
| Medium (100-1000 files) | ~2-5s per edit | ✅ Yes (with incremental) |
| Large (1000+ files) | ~5-10s per edit | ⚠️ Consider async or skip tests |
Optimization strategies:
- Use
async: truefor lint/format (cosmetic checks) - Keep typecheck sync (errors must block)
- Skip full test suite, run only changed file’s tests
- Use incremental compilation (
tsc --incremental)
Example Output (Error Case)
Section titled “Example Output (Error Case)”You: Fix the authentication logicClaude: [Edits auth.ts]
⚠ TypeScript errors in src/auth.ts:
src/auth.ts:45:12 - error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
45 userId: user.id.toString(), ~~~~~~~~~~~~~~~~~~~
⚠ Tests failed in src/__tests__/auth.test.ts:
FAIL src/__tests__/auth.test.ts ● Authentication › should validate user token Expected token to be valid
Fix implementation or update tests.Claude sees these messages immediately and can iterate without manual test runs.
Example 3: Session Summary Hook
Section titled “Example 3: Session Summary Hook”Event: Stop
Display comprehensive session statistics when Claude Code ends, inspired by Gemini CLI’s session summary feature.
The Problem
Section titled “The Problem”After a long Claude Code session, you might wonder:
- How much time did I spend?
- How many API requests did Claude make?
- Which tools did I use most?
- What did this session cost?
Without session tracking, this information is buried in JSONL files that are hard to parse manually.
The Solution
Section titled “The Solution”A Stop hook that automatically displays a formatted summary with:
- Session metadata (ID, auto-generated name, git branch)
- Duration breakdown (wall time vs active Claude time)
- Tool usage statistics with success/error counts
- Model usage per model (requests, input/output tokens, cache stats)
- Estimated cost (via ccusage or built-in pricing table)
Implementation
Section titled “Implementation”File: examples/hooks/bash/session-summary.sh
Requirements:
jq(required for JSON parsing)ccusage(optional, for accurate cost calculation via Claude Code Usage tool)- bash 3.2+ (macOS compatible)
Plugin Install (Recommended):
claude plugin marketplace add FlorianBruniaux/claude-code-pluginsclaude plugin install session-summary@florian-claude-toolsHooks are auto-wired for SessionStart (RTK baseline) and SessionEnd (summary display). No manual configuration needed.
Manual Configuration (alternative):
{ "hooks": { "SessionEnd": [{ "hooks": [{ "type": "command", "command": "~/.claude/hooks/session-summary.sh" }] }] }}Environment Variables:
| Variable | Default | Description |
|---|---|---|
NO_COLOR | - | Disable ANSI colors |
SESSION_SUMMARY_LOG | ~/.claude/logs | Override log directory |
SESSION_SUMMARY_SKIP | 0 | Set to 1 to disable summary |
Example Output
Section titled “Example Output”═══ Session Summary ═══════════════════ID: abc-123-def-456Name: Security hardening v3.26Branch: mainDuration: Wall 1h 34m | Active 14m 24s
Tool Calls: 47 (OK 45 / ERR 2) Read: 12 Bash: 10 Edit: 8 Write: 6 Grep: 5 Glob: 4 WebSearch: 2
Model Usage Reqs Input Outputclaude-sonnet-4-5 42 493.9K 2.5Kclaude-haiku-4-5 5 12.4K 46
Cache: 1.2M read / 45.3K createdEst. Cost: $0.74═══════════════════════════════════════Data Sources
Section titled “Data Sources”The hook extracts data from two locations:
1. Session JSONL file (~/.claude/projects/{encoded-path}/{session-id}.jsonl):
- API requests count
- Token usage per model
- Tool calls (extracted from assistant messages)
- Tool errors (from tool_result with is_error: true)
- Turn durations (system messages with subtype: turn_duration)
- Wall time (first to last timestamp)
2. Sessions index (~/.claude/projects/{encoded-path}/sessions-index.json):
- Session summary (auto-generated by Claude)
- Git branch
- Message count
Log File
Section titled “Log File”Session summaries are also logged to ~/.claude/logs/session-summaries.jsonl for historical analysis:
{ "timestamp": "2026-02-13T10:30:00Z", "session_id": "abc-123-def", "session_name": "Security hardening v3.26", "git_branch": "main", "project": "/path/to/project", "duration_wall_ms": 5640000, "duration_active_ms": 864000, "api_requests": 47, "tool_calls": {"Read": 12, "Bash": 10, "Edit": 8}, "tool_errors": 2, "models": { "claude-sonnet-4-5-20250929": { "requests": 42, "input": 493985, "output": 2505, "cache_read": 1200000, "cache_create": 45300 } }, "total_tokens": { "input": 506458, "output": 2551, "cache_read": 1200000, "cache_create": 45300 }, "cost_usd": 0.74}Performance
Section titled “Performance”- Execution time: <2s for sessions up to 100MB
- Memory: Streaming JSONL processing via
jq reduce inputs(memory-bounded) - Impact: Runs at session end (doesn’t block during work)
Cost Calculation
Section titled “Cost Calculation”Priority 1: ccusage tool (accurate, if available)
ccusage session --id <session-id> --json --offlineFallback: Built-in pricing table (as of 2026-02)
| Model | Input (per 1M tokens) | Output (per 1M tokens) |
|---|---|---|
| claude-opus-4-6 | $15.00 | $75.00 |
| claude-sonnet-4-5 | $3.00 | $15.00 |
| claude-haiku-4-5 | $0.80 | $4.00 |
Edge Cases Handled
Section titled “Edge Cases Handled”- Empty sessions (0 API requests): Displays minimal summary
- Missing JSONL file: Falls back to sessions-index.json
- ccusage unavailable: Uses pricing table fallback
- No turn_duration entries: Shows wall time only
- Very large sessions (500MB+): Streams with jq (memory safe)
Installation
Section titled “Installation”Plugin system (recommended):
claude plugin marketplace add FlorianBruniaux/claude-code-pluginsclaude plugin install session-summary@florian-claude-toolsManual (alternative):
# Copy hookcp examples/hooks/bash/session-summary.sh .claude/hooks/chmod +x .claude/hooks/session-summary.sh
# Add to settings.json (see Configuration above)
# Test itecho '{"session_id":"test","cwd":"'$(pwd)'"}' | .claude/hooks/session-summary.shComparison with Gemini CLI
Section titled “Comparison with Gemini CLI”| Feature | Gemini CLI | Claude Code (with this hook) |
|---|---|---|
| Session summary | ✅ Built-in | ✅ Via hook |
| Duration tracking | ✅ Wall + active | ✅ Wall + active |
| Tool calls breakdown | ✅ Yes | ✅ Yes (with success/error) |
| Model usage | ✅ Requests + tokens | ✅ Requests + tokens + cache |
| Cost estimation | ✅ Yes | ✅ ccusage or pricing table |
| Structured logging | ❌ No | ✅ JSONL for analysis |
8. MCP Servers
Section titled “8. MCP Servers”Quick jump: What is MCP · Available Servers · Configuration · Server Selection Guide · Plugin System · MCP Security
Reading time: 15 minutes Skill level: Week 2-3 Goal: Extend Claude Code with external tools