Skip to content
jsmanifest logojsmanifest

Claude Code Output Validation Hooks in 2026: Linting Agent-Written Code Before It Touches Your Repo

Claude Code Output Validation Hooks in 2026: Linting Agent-Written Code Before It Touches Your Repo

Disclosure: jsmanifest has a financial interest in NoCensor AI and may earn from signups through links in this article.

Deterministic validation hooks for AI-generated code. Stop type errors, security holes, and test failures before Claude commits them to your repository.

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.

%% alt: Claude Code workflow without validation showing compiler staying silent until CI fails
flowchart LR
  Agent("Claude writes code")
  Commit("Code reaches repo")
  CI("CI runs checks")
  Fail("Build fails")
  
  Agent --> Commit
  Commit --> CI
  CI --> Fail
  
  style Fail stroke:#ef4444,fill:#450a0a,color:#fca5a5
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.

%% alt: Claude Code workflow with validation hooks showing checks passing before code reaches the repository
flowchart LR
  Agent("Claude writes code")
  Hook("Validation hook runs")
  Pass("Checks pass")
  Commit("Code reaches repo")
  
  Agent --> Hook
  Hook --> Pass
  Pass --> Commit
  
  style Pass stroke:#34d399,fill:#0b3b2e,color:#d1fae5
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.
  • PostToolUse hooks 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 additionalContext injection 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.

%% alt: Hook execution flow showing PreToolUse blocking risky operations and PostToolUse validating results
flowchart TD
  ToolCall("Tool call requested")
  Pre("PreToolUse hook")
  PreCheck{"Risk assessment"}
  Block("Action blocked")
  Execute("Tool executes")
  Post("PostToolUse hook")
  PostCheck{"Output validation"}
  Fix("Claude fixes errors")
  Done("Action completes")
  
  ToolCall --> Pre
  Pre --> PreCheck
  PreCheck -->|Dangerous| Block
  PreCheck -->|Safe| Execute
  Execute --> Post
  Post --> PostCheck
  PostCheck -->|Invalid| Fix
  PostCheck -->|Valid| Done
  Fix --> Execute
  
  style Block stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
  style Fix stroke:#7c9cf0,fill:#142544,color:#eaf2ff
  style Done stroke:#34d399,fill:#0b3b2e,color:#d1fae5
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.

%% alt: PostToolUse feedback loop showing Claude editing, hook validating, and Claude fixing errors until validation passes
flowchart LR
  Edit("Claude edits file")
  Hook("PostToolUse hook runs")
  Lint("Linter checks output")
  Pass{"Errors found?"}
  Fix("Claude reads errors")
  Done("Turn continues")
  
  Edit --> Hook
  Hook --> Lint
  Lint --> Pass
  Pass -->|Yes| Fix
  Pass -->|No| Done
  Fix --> Edit
  
  style Lint stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
  style Fix stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
  style Done stroke:#34d399,fill:#0b3b2e,color:#d1fae5
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.

%% alt: Comparison of command, prompt, and agent hook types showing execution patterns and decision mechanisms
flowchart LR
  subgraph Command["Command hooks"]
    CmdInput("Hook input JSON")
    CmdScript("Shell script executes")
    CmdExit("Exit code decides")
    
    CmdInput --> CmdScript
    CmdScript --> CmdExit
  end
  
  subgraph Prompt["Prompt hooks"]
    PromptInput("Hook input JSON")
    PromptLLM("Single LLM call")
    PromptJSON("JSON response decides")
    
    PromptInput --> PromptLLM
    PromptLLM --> PromptJSON
  end
  
  subgraph Agent["Agent hooks"]
    AgentInput("Hook input JSON")
    AgentTools("Subagent uses tools")
    AgentDecision("Structured decision")
    
    AgentInput --> AgentTools
    AgentTools --> AgentDecision
  end
  
  style CmdExit stroke:#34d399,fill:#0b3b2e,color:#d1fae5
  style PromptJSON stroke:#7c9cf0,fill:#142544,color:#eaf2ff
  style AgentDecision stroke:#fbbf24,fill:#3a2f0b,color:#fef3c7
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 typeDecision mechanismFilesystem accessWhen to use
CommandExit code and stdoutDirect shell accessDeterministic checks with existing tools
PromptSingle LLM call returning JSONNone, evaluates input data onlyComplex logic that does not need file inspection
AgentSubagent with tools over 50 turnsRead, Grep, Glob toolsVerification 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.

%% alt: Multi-stage validation flow showing security check, linting, testing, and context injection
flowchart LR
  Start("Tool call begins")
  Security("Security pre-check")
  Execute("Tool executes")
  Lint("Linter validates")
  Test("Tests verify")
  Inject("Context injection")
  Done("Turn completes")
  
  Start --> Security
  Security --> Execute
  Execute --> Lint
  Lint --> Test
  Test --> Inject
  Inject --> Done
  
  style Security stroke:#c084fc,fill:#3b0764,color:#f3e8ff,stroke-width:4px
  style Lint stroke:#7c9cf0,fill:#142544,color:#eaf2ff
  style Test stroke:#7c9cf0,fill:#142544,color:#eaf2ff
  style Inject stroke:#7c9cf0,fill:#142544,color:#eaf2ff
  style Done stroke:#34d399,fill:#0b3b2e,color:#d1fae5
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.

Sources