Self-Validating Claude Code Agents
Wire PostToolUse hooks, Stop hooks, and read-only reviewer agents into agent definitions so bad output never reaches you.
Problem: An agent hands back work that looks fine at a glance. Then the linter screams, an export is missing, and a whole file was skipped. You only spot it during review, twenty minutes after the run finished.
Quick Win: Drop a PostToolUse hook straight into the agent definition. Every file this agent writes goes through the linter before you ever see it:
# .claude/agents/frontend-builder.md
---
name: frontend-builder
description: Build React components with automatic quality checks
model: sonnet
hooks:
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: 'npx eslint --fix "$CLAUDE_TOOL_INPUT_FILE_PATH" && npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH"'
---
You are a frontend builder agent. Create React components following
the project's established patterns. Every file you write is automatically
linted and formatted by your embedded hooks.Unlinted code is no longer an option for this agent. The check belongs to who it is, not something you bolt on afterwards.
There are three tiers to a self-validating agent. Each one catches a different class of bug, and they stack cleanly.
Micro is per tool call. A PostToolUse hook on the agent definition runs a linter, a formatter, or a type checker right after a file gets written. Anything broken gets caught seconds after it happens.
Macro is whole-job. When the agent tries to finish, a Stop hook asks the real questions: are the required files there, do exports exist, does the test suite go green. If any of it fails, the agent is not allowed to call the job done.
Team pulls in a second agent. A read-only reviewer opens the builder's work with a clean context window. It is the builder/reviewer pattern, one level higher up the stack.
PostToolUse: Micro Validation on Every Write
Hooks declared inside agent frontmatter only fire when that agent is the one running. Scope is automatic. ESLint belongs to the frontend builder, Ruff belongs to the Python builder, and they never collide.
Here is a Python agent wired to Black and mypy:
# .claude/agents/python-builder.md
---
name: python-builder
description: Build Python modules with automatic formatting and type checking
model: sonnet
hooks:
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: 'black "$CLAUDE_TOOL_INPUT_FILE_PATH" && mypy "$CLAUDE_TOOL_INPUT_FILE_PATH" --ignore-missing-imports'
---
You are a Python builder agent. Write clean, typed Python code.
Your hooks automatically format with Black and check types with mypy.The win is scope. These hooks belong to the agent. Nothing leaks into project-level settings. When the orchestrator spins this agent up through the Task tool, the validation goes along for the ride.
Micro catches syntax and formatting. It does not catch a file that never got written. For that, you need a Stop hook.
Stop Hooks: Macro Validation Before Completion
Stop hooks in agent frontmatter turn into SubagentStop events. This script confirms every required output file exists and has the content you asked for:
#!/bin/bash
# .claude/scripts/validate-output.sh
# Validates that agent output meets structural requirements
INPUT=$(cat)
STOP_HOOK_ACTIVE=$(echo "$INPUT" | jq -r '.stop_hook_active // false')
if [ "$STOP_HOOK_ACTIVE" = "true" ]; then
exit 0
fi
# Check that required files exist
REQUIRED_FILES=("src/components/index.ts" "src/components/Button.tsx")
MISSING=""
for file in "${REQUIRED_FILES[@]}"; do
if [ ! -f "$file" ]; then
MISSING="$MISSING $file"
fi
done
if [ -n "$MISSING" ]; then
echo "{\"decision\": \"block\", \"reason\": \"Missing required files:$MISSING\"}"
exit 0
fi
# Check that index.ts contains exports
if ! grep -q "export" src/components/index.ts; then
echo "{\"decision\": \"block\", \"reason\": \"index.ts has no exports. Add barrel exports for all components.\"}"
exit 0
fi
exit 0Then wire it into the agent itself:
---
name: component-builder
description: Build component libraries with output validation
hooks:
PostToolUse:
- matcher: "Write|Edit"
hooks:
- type: command
command: 'npx prettier --write "$CLAUDE_TOOL_INPUT_FILE_PATH"'
Stop:
- hooks:
- type: command
command: "bash .claude/scripts/validate-output.sh"
---Now both tiers are live. Each write gets formatted. The whole output gets graded before the agent is allowed to stop. If the Stop hook blocks, the agent keeps going until the checks finally pass.
Read-Only Validator Agents
The third tier is a reviewer that cannot touch files. disallowedTools enforces that at the tool layer:
# .claude/agents/output-validator.md
---
name: output-validator
description: Validate agent output without modifying files. Use after builder agents complete.
model: haiku
disallowedTools: Write, Edit, NotebookEdit
---
You are a read-only validator. Your job:
1. Read all files the builder created or modified
2. Verify exports, type safety, and error handling
3. Run the test suite with Bash
4. Report issues as a list. Do NOT fix anything.
If all checks pass, say "Validation passed" with a summary.
If issues exist, list each one with file path and line reference.Writing files is off the table for this one. Reading and reporting is all it can do. Pair it with a builder via task dependencies:
TaskCreate(subject="Build auth module", description="...")
TaskCreate(subject="Validate auth module", description="Run output-validator on src/auth/")
TaskUpdate(taskId="2", addBlockedBy=["1"])When to Use Each Tier
Micro only (PostToolUse) is the right fit for small, tight tasks whose quality bar starts and stops with linting. Overhead is low. Feedback is instant.
Micro plus macro (PostToolUse + Stop) suits agents that ship several files with a structural shape attached. The Stop hook catches what a linter never does: files that never got written, logic left half-done, a suite of red tests.
All three tiers belong on code paths you cannot afford to get wrong. The machine-driven checks do the grind. A separate reviewer gives you a second opinion the builder's own hooks could not.
Start the micro tier on the agent you use most. The moment an agent hands you half-finished work, bolt on a Stop hook. Bring in the reviewer once you care that the whole deliverable hangs together, not only that each file parses. Agent configs are fine to keep in your CLAUDE.md, or they can sit as their own files under .claude/agents/, whichever fits the project.
Beyond Single Agents
Think of self-checks and team checks as a stack, not alternatives. Around 90% of the quality bugs get handled by the embedded PostToolUse hooks plus the Stop script before the reviewer ever opens a file. What is left for the reviewer is integration and architecture, not the lint errors the hooks already swallowed.
Stop configuring. Start building.
Stop Hooks
Block Claude from ending a response while tests, builds, or lint checks still fail. Four patterns plus loop protection.
Claude Code Session Hooks
Four lifecycle hooks for Claude Code. Run init steps on demand, inject project context at every start, back up transcripts, and log cleanup on exit.