Self-validating Claude Code agents: wire PostToolUse lint hooks, Stop hooks, and read-only reviewer sub-agents into agent definitions so bad output never ships.
Stop configuring. Start building.
SaaS builder templates with AI orchestration.
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
Stop configuring. Start building.
SaaS builder templates with AI orchestration.
Stop configuring. Start building.
SaaS builder templates with AI orchestration.
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.
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'
---
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 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
Then 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
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.
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"])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.
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.