Claude Code Output Validation Hooks in 2026: Linting Agent-Written Code Before It Touches Your Repo
This article was written with the assistance of AI, under human supervision and review.
AI code agents produce output at a pace that makes manual review impractical. A Claude Code session can edit fifty files before the first test runs. The code lands in the repository clean or broken depending on whether validation happened before the commit. Most teams discover this after the damage shows up in CI.
The problem sits between the agent's tool execution and the repository state. Claude writes code that passes its internal checks but fails the toolchain's stricter rules. Type errors appear that TypeScript would have caught. Tests break that were passing ten minutes earlier. Security scanners flag patterns that a pre-commit hook would have blocked.
Claude Code workflow without validation showing compiler staying silent until CI fails
Validation hooks solve this by running checks at deterministic points in the agent's execution flow. A PreToolUse hook blocks a dangerous shell command before it runs. A PostToolUse hook lints every file edit and sends errors back to Claude for correction. A Stop hook verifies that tests pass before the turn ends.
Claude Code workflow with validation hooks showing checks passing before code reaches the repository
The validation happens in the same environment that runs the code. The agent sees the exact error message the compiler produces and fixes it in the next turn. The repository receives only code that already passed the checks.
Key Takeaways
- Validation hooks intercept AI tool calls at specific execution points (before, after, or on session events) and run deterministic checks that return pass/fail decisions.
- Exit code 2 blocks the action and returns stderr to Claude as feedback; exit 0 applies any JSON output fields but does not auto-approve.
-
PostToolUsehooks with formatters and linters are the most widely adopted pattern because they fix trivial errors automatically. - Agent-type hooks spawn subagents that can read files and run commands to verify conditions that require inspecting actual state rather than just evaluating input data.
- Production workflows layer multiple hooks (linting, security scanning, test execution) with
additionalContextinjection to enforce repository policy deterministically.
PreToolUse vs PostToolUse: Choosing the Right Hook Point
Claude Code fires hook events at different stages of execution. The choice between PreToolUse and PostToolUse determines whether validation prevents an action or corrects it afterward.
A PreToolUse hook runs before the tool executes. The hook receives the tool name and input as JSON. Exiting with code 2 blocks the tool call and sends stderr back to Claude as the reason. This works for preventing destructive operations that cannot be undone.
Hook execution flow showing PreToolUse blocking risky operations and PostToolUse validating results
A PostToolUse hook runs after the tool finishes. The hook sees the tool output and can verify that the operation produced valid results. Exiting with code 2 here does not undo the tool call. Instead it signals that the output failed validation and Claude should try again.
The PreToolUse pattern prevents errors. The PostToolUse pattern detects and corrects them. Most production setups use both. Security checks run pre-execution to block dangerous patterns. Linters and formatters run post-execution to clean up style issues.
A Stop hook fires when Claude tries to end its turn. Exiting with code 2 sends Claude back to work with stderr as the reason it cannot finish yet. This enforces that work meets completion criteria before the turn ends.
The timing decision depends on what can be fixed automatically. Formatters and linters produce errors that Claude can resolve by re-editing the file. Those belong in PostToolUse hooks. Destructive operations like dropping database tables cannot be fixed after execution. Those require PreToolUse blocks.
Building a Pre-Commit Linting Hook That Blocks Bad Edits
The most effective validation hook runs TypeScript's compiler in --noEmit mode after every file edit. The hook exits with code 2 if type errors exist, which blocks the turn from ending and sends the full compiler output back to Claude.
Developers place this hook at .claude/settings.json in the repository root so it applies to every team member who works in the project, as documented at https://code.claude.com/docs/en/hooks. Workspace trust controls when the hook starts running on each machine.
{ "hooks": { "Stop": [ { "type": "command", "command": "pnpm tsc --noEmit 1>&2 || exit 2" } ] } } The 1>&2 redirection sends TypeScript's output to stderr because Claude Code reads stderr as the block reason when a hook exits with code 2. Without the redirect, the compiler errors go to stdout and Claude never sees them.
The hook runs every time Claude tries to stop. If the compiler finds errors, the exit code 2 prevents the turn from ending. Claude receives the type errors and continues working until tsc passes.
Claude Code validation hooks running TypeScript compiler checks
This pattern extends to any tool that reports errors on stderr and uses exit codes. ESLint with --max-warnings 0 blocks on linting violations. Prettier with --check blocks when formatting is inconsistent.
{ "hooks": { "Stop": [ { "type": "command", "command": "pnpm tsc --noEmit 1>&2 || exit 2" }, { "type": "command", "command": "pnpm eslint . --max-warnings 0 1>&2 || exit 2" } ] } } Hooks in the same event array run in parallel. Both the typecheck and the linter execute at the same time. If either exits with code 2, the turn stays blocked until both pass.
The default block cap is eight consecutive stops with exit code 2 and no tool calls between them. After eight blocks, Claude Code overrides the next block and ends the turn anyway. Teams that need a higher cap set the CLAUDE_CODE_STOP_HOOK_BLOCK_CAP environment variable, documented at https://code.claude.com/docs/en/env-vars.
PostToolUse Feedback Loops: Teaching Claude to Fix Its Own Mistakes
PostToolUse hooks run after a tool finishes and receive the tool's output as part of the hook input. Exit code 2 in a PostToolUse hook does not block the original tool call because the tool already executed. Instead it signals that validation failed and Claude should continue the turn to address the errors.
PostToolUse feedback loop showing Claude editing, hook validating, and Claude fixing errors until validation passes
The hook script receives JSON on stdin that includes the tool name, input, and output. A PostToolUse hook for file edits parses this input to extract the edited file path and runs a linter against it.
#!/usr/bin/env node const input = JSON.parse(require('fs').readFileSync(0, 'utf-8')); if (input.tool_name === 'Write') { const filePath = input.tool_input.path; const { execSync } = require('child_process'); try { execSync(`eslint ${filePath} --max-warnings 0`, { stdio: 'inherit', encoding: 'utf-8' }); } catch (error) { process.stderr.write(`Linting failed for ${filePath}\n`); process.exit(2); } } process.exit(0); The script checks whether the tool that just ran was Write. If so, it lints the file. A linting failure causes the script to exit with code 2 and write the error to stderr. Claude sees this feedback and edits the file again to fix the violations.
The feedback loop continues until the linter passes. Each iteration gives Claude the exact error message and line number. The agent applies the fix and the hook validates again. The turn ends only when no errors remain.
This pattern handles any validation that can be expressed as a deterministic check. Formatters, type checkers, and test runners all fit. The hook runs the check, and Claude iterates until it passes. The pattern works better than prompting Claude to "make sure to lint" because the hook enforces the requirement regardless of how Claude interprets the instruction.
Teams extend this approach to security scanning by running tools like semgrep or gitleaks in PostToolUse hooks. A finding triggers exit code 2 with the scanner's output on stderr. Claude sees which pattern triggered the alert and rewrites the code to avoid it.
Security Scanners and Test Runners as Validation Hooks
Security scanning belongs in PreToolUse hooks for shell commands and PostToolUse hooks for file edits. A PreToolUse hook on the Bash tool receives the command as part of the hook input and can block execution if the command matches a dangerous pattern.
{ "hooks": { "PreToolUse": [ { "type": "command", "command": "node .claude/hooks/security-check.js" } ] } } The security check script parses stdin to extract the command and matches it against a deny list.
#!/usr/bin/env node const input = JSON.parse(require('fs').readFileSync(0, 'utf-8')); if (input.tool_name === 'Bash') { const command = input.tool_input.command; const dangerous = [ /rm\s+-rf\s+\//, /drop\s+table/i, /chmod\s+777/, /curl.*\|\s*bash/ ]; for (const pattern of dangerous) { if (pattern.test(command)) { process.stderr.write( `Blocked dangerous command: ${command}\n` ); process.exit(2); } } } process.exit(0); The hook exits with code 2 if the command matches any pattern. Claude receives the stderr message and knows the command was blocked. The agent adjusts its approach without executing the dangerous operation.
Test runners fit better in Stop hooks than in PostToolUse hooks because a single edit can break tests in unrelated files. Running tests after every file write creates unnecessary execution overhead. Running them once when Claude declares the work complete gives a single validation point.
{ "hooks": { "Stop": [ { "type": "command", "command": "pnpm test --run 1>&2 || exit 2", "timeout": 60 } ] } } The timeout field gives the test suite 60 seconds to complete. If tests fail, the stderr output returns to Claude with the failure details. The agent reads which tests broke and continues the turn to fix them.
Security scanning and test execution as validation hooks
Some teams run focused test suites in PostToolUse hooks when an edit touches specific directories. A hook script checks the edited file path and runs only the related test file if one exists.
#!/usr/bin/env node const input = JSON.parse(require('fs').readFileSync(0, 'utf-8')); if (input.tool_name === 'Write') { const filePath = input.tool_input.path; const testPath = filePath.replace(/\.ts$/, '.test.ts'); const fs = require('fs'); if (fs.existsSync(testPath)) { const { execSync } = require('child_process'); try { execSync(`pnpm test ${testPath} --run`, { stdio: 'inherit' }); } catch (error) { process.stderr.write(`Tests failed for ${testPath}\n`); process.exit(2); } } } process.exit(0); This approach gives fast feedback on test failures without running the entire suite after every edit. The full suite still runs in the Stop hook to catch cross-file regressions.
Command vs Prompt vs Agent Hook Types: When to Use Each
Claude Code supports three primary hook types that differ in how they make validation decisions. Command hooks run shell scripts, prompt hooks send a question to a Claude model for single-turn evaluation, and agent hooks spawn subagents that can use tools to investigate before deciding.
Comparison of command, prompt, and agent hook types showing execution patterns and decision mechanisms
Command hooks (type: "command") execute a shell command or script that receives the hook event data on stdin and communicates results through exit codes and stdout. The script has full access to the repository and can run any validation tool. This type handles most validation needs.
Prompt hooks (type: "prompt") send the hook input to a Claude model along with a prompt that asks for a decision. The model returns a JSON object with ok and reason fields. Prompt hooks work when the validation logic is too complex for a static script but does not require inspecting files. The official documentation at https://code.claude.com/docs/en/hooks shows examples of prompt hooks that evaluate whether a commit message follows conventional commit format.
Agent hooks (type: "agent") spawn a subagent that can use tools like Read, Grep, and Glob to verify conditions before returning a decision. The agent receives a prompt and the hook input, investigates using up to 50 tool turns, and returns a structured ok/reason response. This type fits verification that requires inspecting actual file contents or running commands to determine state.
| Hook type | Decision mechanism | Filesystem access | When to use |
|---|---|---|---|
| Command | Exit code and stdout | Direct shell access | Deterministic checks with existing tools |
| Prompt | Single LLM call returning JSON | None, evaluates input data only | Complex logic that does not need file inspection |
| Agent | Subagent with tools over 50 turns | Read, Grep, Glob tools | Verification requiring actual codebase state |
Command hooks cost nothing beyond the compute time to run the validation tool. Prompt hooks consume tokens on every invocation. Agent hooks can consume significant tokens if they perform extensive file reads or searches. The claude-code-cost-control-production post covers budgeting strategies for hook token usage.
Most teams start with command hooks for linting, typechecking, and security scanning. Prompt hooks enter the workflow when validation requires understanding intent rather than matching patterns. Agent hooks appear in workflows that need to verify cross-file consistency or check that tests exist for new features.
The official documentation at https://code.claude.com/docs/en/hooks describes agent hooks as experimental with behavior and configuration subject to change. Production workflows that require stability prefer command hooks with explicit tool invocations over agent hooks that may behave differently across Claude Code versions.
Production Validation Patterns: Multi-Stage Hooks and additionalContext
Production repositories layer multiple validation hooks that run at different execution points and inject context that guides Claude toward correct patterns. The layering creates defense in depth where each hook enforces a different aspect of repository policy.
Multi-stage validation flow showing security check, linting, testing, and context injection
A typical production configuration includes PreToolUse hooks that block dangerous operations, PostToolUse hooks that validate file edits, Stop hooks that enforce completion criteria, and SubagentStart hooks that inject security guidelines into background tasks.
The additionalContext field in hook output adds text to Claude's context. A SubagentStart hook can inject repository-specific rules that apply only when a subagent runs, as documented at https://code.claude.com/docs/en/hooks.
{ "hooks": { "SubagentStart": [ { "type": "command", "command": "node .claude/hooks/inject-security-context.js" } ] } } The injection script reads security guidelines from a file and returns them in the hookSpecificOutput field.
#!/usr/bin/env node const fs = require('fs'); const guidelines = fs.readFileSync( '.claude/security-guidelines.md', 'utf-8' ); const output = { hookSpecificOutput: { hookEventName: 'SubagentStart', additionalContext: guidelines } }; process.stdout.write(JSON.stringify(output)); process.exit(0); The subagent receives these guidelines in its initial context before its first prompt. This pattern keeps security rules in a single source file that applies automatically to all subagent work.
Hook precedence determines which settings file wins when the same hook appears in multiple locations. Managed settings override project settings, which override user settings. The claude-code-structured-output-zod post covers settings precedence in detail.
Teams that enforce validation hooks organization-wide place them in managed settings files that individual repositories cannot override. A managed Stop hook that requires passing tests applies to every project regardless of local configuration.
Production teams set onFailure: "block" on critical hooks to ensure that hook failures prevent the action even when the hook exits with a non-standard error code. This field appears in the hook handler object alongside type and command.
{ "hooks": { "PreToolUse": [ { "type": "command", "command": "node .claude/hooks/security-check.js", "onFailure": "block" } ] } } With onFailure set to "block", a hook that crashes or times out blocks the tool call instead of allowing it through. This setting matters for security hooks where a failure to validate should err on the side of blocking rather than permitting.
The multi-stage pattern combines fast feedback from PostToolUse hooks with comprehensive validation from Stop hooks. Linters and formatters run immediately after edits so Claude sees errors while the file is still in working memory. The full test suite runs once at the end to catch cross-file issues.
Frequently Asked Questions
Can validation hooks slow down Claude Code sessions significantly?
Hooks add latency proportional to the validation tool's runtime. A TypeScript typecheck that takes five seconds blocks the turn for five seconds. Fast linters like ESLint add under one second. Hooks run in parallel when multiple handlers exist for the same event, so a typecheck and a linter running together take the time of the slower tool, not the sum. The timeout field limits how long Claude Code waits before considering the hook failed.
What happens when a hook exits with code 2 but prints valid JSON to stdout?
Claude Code reads the JSON fields even when the hook blocks with exit code 2. Decision fields like permissionDecision: "block" apply, but the block from the exit code stands regardless. The JSON can add a structured reason or inject context, but it cannot override the block. The official documentation at https://code.claude.com/docs/en/hooks includes a table showing which fields apply on each exit code.
Do hooks configured in a repository apply to every developer who clones it?
Hooks in .claude/settings.json apply to everyone who works in the project after they accept the workspace trust dialog for that folder. Deny rules apply immediately. Allow rules and hooks wait for trust acceptance. Each developer's local machine respects the CLAUDE_CODE_STOP_HOOK_BLOCK_CAP setting, so teams that need higher caps document the required environment variable in the repository README.
Can agent-type hooks access environment variables and credentials?
Agent hooks spawn subagents that run in the same environment as the main session. Environment variables available to Claude Code are visible to the subagent. Credentials stored in environment variables or accessible via the system's credential store work in agent hooks the same way they work in command hooks. The subagent's tool set determines what it can access, and agent definitions can restrict tools to Read, Grep, and Glob to prevent modification.
How do hooks interact with Claude's prompt cache to avoid redundant context loading?
SubagentStart hooks that inject additionalContext place that content in the subagent's context before its first prompt. Claude Code reuses this context across requests via prompt caching as documented at https://code.claude.com/docs/en/prompt-caching. When the subagent's context undergoes auto-compaction and discards the injected copy, the next SubagentStart hook run injects it again, maintaining the guideline presence without breaking the cache.
Making Validation Hooks Repository Policy
Validation hooks enforce what prompts and documentation only suggest. A CLAUDE.md file that says "always run tests" depends on Claude interpreting and following that instruction. A Stop hook that exits with code 2 when tests fail makes the requirement deterministic.
The shift from guidance to policy changes how teams use AI coding tools in production. Repositories with comprehensive hook coverage allow less experienced developers to work with Claude because the hooks prevent the common mistakes that would otherwise require senior review. The claude-code-ci-agentic-review-test-auto-fix post shows how validation hooks integrate with CI to create a complete quality gate.
Hooks belong in the repository's .claude/settings.json file where they version-control alongside the code. Teams that adopt hooks as policy commit them to the main branch and document them in the project README. Developers who join the project inherit the validation rules when they accept workspace trust for the repository.
Organizations that enforce hooks across all repositories place them in managed settings. The hooks run whether or not individual projects configure them locally. This approach makes security scanning and compliance checks organization-wide policy rather than per-project configuration.
Validation hooks turn AI code generation from a supervised activity into a governed process. The agent operates within deterministic boundaries that hold regardless of how it reasons about the task.










