Configure event hooks to enforce policies, automate linting, and inject context—without touching the agent’s code.
Hooks are external scripts executed by the harness at specific points in the agent lifecycle. They receive event metadata via stdin and can influence the harness's behavior through the exit code and output.
The big advantage: you add behavior without modifying the prompt or agent. Policies live in versioned scripts in the repository and are automatically applied to the whole team.
Runs when a new session starts. Stdout is injected as the initial message in the context. Ideal for loading project state, the current branch, and pending tasks.
Runs before each tool. Exit 0 = allows it. Non-zero exit = blocks the tool. Stderr with an error message is returned to the agent.
Runs after each tool returns. Stdout is injected into the context. Ideal for automatic linting, auditing, and usage metrics.
Runs when the agent exits. Ideal for cleanup, completion notifications, session reports, and memory persistence.
Hooks are configured in the key hooks of the settings.json. Each entry defines the event, an optional tool matcher, and the shell command to run.
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-start.sh"
}
]
}
],
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/block-dangerous.sh"
}
]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/lint.sh"
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "bash .claude/hooks/session-end.sh"
}
]
}
]
}
}
Applies to all projects. Good for personal policies: blocking destructive commands, lint preferences.
Project-specific. Committed to the repository—the whole team automatically inherits the policies.
PostToolUse on Write/Edit runs eslint/prettier. The output is injected into the context — the agent sees the errors and fixes them automatically.
#!/bin/bash # .claude/hooks/lint.sh
FILE=$(cat /dev/stdin | jq -r '.tool_input.file_path // empty')
if [[ "$FILE" == *.ts || "$FILE" == *.tsx ]]; then
npx eslint --fix "$FILE" 2>&1
fi
PreToolUse on Bash inspects the command. If it detects prohibited patterns, it returns exit 1 with a message explaining the block.
#!/bin/bash # .claude/hooks/block-dangerous.sh
CMD=$(cat /dev/stdin | jq -r '.tool_input.command // empty')
DANGEROUS="rm -rf|git push --force|DROP TABLE|truncate"
if echo "$CMD" | grep -qE "$DANGEROUS"; then
echo "❌ Comando bloqueado por política de segurança" >&2
exit 1
fi
At the start of each session, it automatically injects the current state: branch, recent commits, open issues. The agent starts with context already in place.
#!/bin/bash # .claude/hooks/session-start.sh
echo "=== CONTEXTO DO PROJETO ==="
echo "Branch: $(git branch --show-current)"
echo "Último commit: $(git log -1 --oneline)"
echo "Arquivos modificados:"
git status --short | head -10
Fail-open is the default behavior: if a hook fails (timeout, script error, crash), the harness continues execution normally. The tool the hook should intercept isn't blocked.
This is intentional for enrichment hooks (lint, context)—you don't want a broken lint to interrupt all work. But for security hooks, fail-open is dangerous.
#!/bin/bash # Sempre: timeout explícito para não travar o agente # Leitura do stdin com timeout INPUT=$(timeout 2 cat /dev/stdin 2>/dev/null || echo '{}') # Parse seguro com fallback CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) # Se nada para verificar, sair com sucesso (fail-open explícito) [ -z "$CMD" ] && exit 0 # Verificação principal if echo "$CMD" | grep -qE "rm -rf /|DROP DATABASE"; then echo "Comando bloqueado: risco de perda de dados irreversível" >&2 exit 1 fi exit 0
| Event | Data on stdin | Output impact |
|---|---|---|
| SessionStart | Session metadata | Stdout injected as the initial message |
| PreToolUse | Tool name + tool_input (JSON) | Exit 0=allows, 1=blocks; stderr→agent |
| PostToolUse | tool_input + tool_response (JSON) | Stdout injected into the current context |
| Stop | Reason for termination | Side effects only (log, notification) |
The skill /update-config of Claude Code makes it easy to add hooks to settings.json using a natural language prompt: "add a lint hook for TypeScript".
4.3 — ⌨️ Slash commands and deterministic workflows