Deep Dive into Claude Code Hooks: Stop Hooks and the Self‑Validation Loop
The article dissects Claude Code's Hooks system, detailing its 27 lifecycle events, four hook types, the special behavior of Stop Hooks with exit code 2, the self‑validation loop, practical patterns like the Ralph Loop, and the design trade‑offs and mitigation strategies.
01 Hook System Overview: 27 Lifecycle Events
Claude Code defines HOOK_EVENTS as a 27‑item string array in src/entrypoints/sdk/coreTypes.ts. The events are grouped by trigger frequency:
Per session : SessionStart, SessionEnd, Setup Per round : UserPromptSubmit, Stop, StopFailure, PreCompact, PostCompact Per tool call : PreToolUse, PostToolUse, PostToolUseFailure, PermissionRequest, PermissionDenied Sub‑agent lifecycle : SubagentStart, SubagentStop, TaskCreated, TaskCompleted, TeammateIdle Asynchronous/reactive : FileChanged, CwdChanged, ConfigChange, InstructionsLoaded, WorktreeCreate, WorktreeRemove, Notification, Elicitation, ElicitationResult Each hook can be one of four types, defining how it is executed and typical use cases: command: runs a shell script (bash/pwsh) – e.g., run scripts or call tools. http: sends an HTTP POST – e.g., call external services or webhooks. prompt: invokes an LLM sub‑model – e.g., let another model review output. agent: runs a sub‑agent – e.g., implement complex validation logic.
Combined, the 27 events and 4 types form a comprehensive AI‑behaviour programmable interface.
02 Stop Hook: Not Just "Run After Completion", It Validates Before Ending
The core of the self‑validation loop lives in handleStopHooks inside src/query.ts. Its return handling follows three paths:
// src/query.ts (excerpt, ~1267 lines)
const stopHookResult = yield* handleStopHooks(
messagesForQuery, assistantMessages,
systemPrompt, userContext, systemContext,
toolUseContext, querySource,
stopHookActive, // ← key field, prevents infinite loops
);
if (stopHookResult.preventContinuation) {
return { reason: 'stop_hook_prevented' } // Path 1: graceful termination
}
if (stopHookResult.blockingErrors.length > 0) {
// Path 2: inject Hook errors as user messages and continue
const next = {
messages: [
...messagesForQuery,
...assistantMessages,
...stopHookResult.blockingErrors, // ← errors become new input
],
stopHookActive: true, // ← marks that Stop Hook was active this round
transition: { reason: 'stop_hook_blocking' },
};
state = next;
continue; // state machine proceeds, Claude works again
}
// Path 3: no blocking errors, normal endThe key insight is that a Stop Hook returning exit 2 (a blocking error) does not stop Claude; instead the error is re‑injected as a new user message, allowing Claude to retry and self‑correct.
The stop_hook_active flag is passed through the Hook JSON input, giving the script control over whether to keep blocking.
03 Five Exit Codes and Their JSON Output Semantics
The exit‑code table defines behavior: 0: success, continue execution (validation passed). 1: non‑blocking error – recorded but execution continues (Hook itself failed). 2: blocking error , triggers the self‑validation loop (quality check failed). 130 (SIGINT): user interruption (Ctrl+C).
Other non‑zero values: non‑blocking errors shown to the user (script logic error).
Only exit 2 is treated as a blocking error; all other non‑zero codes are non‑blocking.
When a Hook outputs JSON directly (stdout) with exit 0, the JSON fields drive the next action, e.g.: continue: false → graceful termination ( preventContinuation). decision: "block" + reason → block the current tool call. hookSpecificOutput.permissionDecision: "deny" → fine‑grained permission control. hookSpecificOutput.updatedInput → inject modified parameters into the tool.
04 PreToolUse and PostToolUse: Intercept and Enrich
PreToolUseacts as the last gate of the permission system. A concrete example blocks deletion of .env files:
# .env protection Hook (JSON output)
{"decision":"block","reason":"禁止删除 .env 文件"}When the Hook returns JSON, it always exits with 0; the JSON content determines the behavior. PostToolUse can add contextual hints to tool results, reducing follow‑up questions. For a database query tool it may return:
{"additionalContext":"注意: user_id 是 UUID 格式,不是整数"}This tells Claude the expected format, preventing it from asking for clarification.
05 Self‑Validation Loop in Practice: Ralph Loop Pattern
By combining the above mechanisms, developers can implement the "Ralph Loop": after Claude finishes a task, a test suite runs; on failure the Hook returns exit 2, feeding the error back as new input, prompting Claude to auto‑repair, and the cycle repeats until all tests pass.
Configuration example (in .claude/settings.json):
{
"hooks": {
"Stop": [
{
"type": "command",
"command": "./verify.sh",
"timeout": 120
}
]
}
}Sample Bash script (simplified):
#!/bin/bash
INPUT=$(cat)
# Prevent infinite recursion
[ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ] && exit 0
cd "$CLAUDE_PROJECT_DIR"
TEST_OUTPUT=$(npm test 2>&1)
if [ $? -ne 0 ]; then
echo "测试套件失败,请修复以下错误:
$TEST_OUTPUT" >&2
exit 2 # trigger self‑validation loop
fi
echo "✅ 所有测试通过" >&2
exit 0The whole process runs without human intervention.
06 SessionEnd 1500 ms Hard Limit and Trust Mechanism
SessionEnd hooks have a default timeout of 1500 ms (≈400× shorter than tool hooks). To avoid timeout, developers can set "async": true or increase the timeout via the environment variable CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS.
// src/utils/hooks.ts
const SESSION_END_HOOK_TIMEOUT_MS_DEFAULT = 1500;
export function getSessionEndHookTimeoutMs() {
const raw = process.env.CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS;
const parsed = raw ? parseInt(raw, 10) : NaN;
return Number.isFinite(parsed) && parsed > 0 ? parsed : SESSION_END_HOOK_TIMEOUT_MS_DEFAULT;
}A trust check centralizes security: shouldSkipHookDueToTrust() skips all hooks until the user accepts a workspace‑trust dialog, preventing supply‑chain attacks from malicious hooks placed in .claude/settings.json.
// src/utils/hooks.ts (shouldSkipHookDueToTrust)
export function shouldSkipHookDueToTrust() {
if (!getIsNonInteractiveSession()) {
const hasTrust = checkHasTrustDialogAccepted();
return !hasTrust; // skip if trust not granted
}
return false; // SDK / non‑interactive mode assumes trust
}07 Critical Perspective: Boundaries of the Design
Design trade‑offs include:
Self‑validation loop risk : stop_hook_active only prevents two consecutive blocking rounds; a hook that errors every other round could cause a slow dead loop. Using an external counter is recommended.
Debug experience : non‑blocking errors are visible only with --debug and via the Transcript view (Ctrl+O), which many users overlook.
Parallel hook race conditions : hooks run with all(hookPromises) can write files concurrently; the SDK provides no built‑in locking.
SessionEnd timeout : the 1500 ms limit forces users to async‑ify long operations or adjust the environment variable.
08 Practical Recommendations (5 Reusable Patterns)
Quality gate : use a Stop Hook with exit 2 and eslint --max-warnings 0 to enforce code quality; on failure Claude auto‑repairs.
Parameter sanitization : PreToolUse can modify updatedInput to add LIMIT to SQL or convert relative paths to absolute.
Context enrichment : PostToolUse can append additionalContext (e.g., data type hints) to reduce follow‑up queries.
Audit logging : SessionEnd Hook exports session logs; use async execution to avoid the 1500 ms limit.
Protection net : PreToolUse blocks dangerous commands (e.g., deleting .env), offering deterministic safety over relying on Claude's judgment.
Conclusion
The Hooks system provides a powerful, programmable AI behavior layer. The Stop Hook’s exit 2 creates a self‑validation feedback loop, stop_hook_active gives users control over loop termination, and the 27 events plus 4 types cover virtually the entire AI lifecycle. SessionEnd’s strict timeout reflects a product trade‑off, while the centralized trust check mitigates supply‑chain risks. Understanding these mechanisms enables developers to build robust, automated AI workflows.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
James' Growth Diary
I am James, focusing on AI Agent learning and growth. I continuously update two series: “AI Agent Mastery Path,” which systematically outlines core theories and practices of agents, and “Claude Code Design Philosophy,” which deeply analyzes the design thinking behind top AI tools. Helping you build a solid foundation in the AI era.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
