An agent finishes a refactor, announces "All done — the tests pass," and hands back a diff that doesn't compile. Nobody ran the tests. The model predicted that a competent engineer, at that point in the conversation, would say the tests pass — so it said so. The gap between what the model claims and what is true is the entire problem with agentic coding, and no amount of upgrading to a larger model closes it on its own.
What closes it is the machinery around the model. After enough sessions where a coding agent confidently shipped something broken, I stopped trying to prompt my way to reliability and started building structure instead. Three primitives carry most of the load: hooks for deterministic guardrails, sub-agents for isolated context, and feedback loops that make the agent confront reality before it declares victory. They compose, and that's the point — each one earns the next.
Instructions are requests; hooks are guarantees
Everything you write in a CLAUDE.md, a system prompt, or a skill is a suggestion. The model usually follows it. "Usually" is fine for style preferences and catastrophic for anything load-bearing. If something must happen on every turn, it cannot live in prose the model is free to skip.
Hooks are how you make it deterministic. They're shell commands wired to specific points in the session lifecycle, and they run whether the model cooperates or not. The events I reach for constantly:
PostToolUse — fires after a tool succeeds. Run the formatter after every Edit, or a linter after every Write.
PreToolUse — fires before a tool runs and can block it. This is the security checkpoint: refuse a destructive shell command, deny a write to a protected path.
Stop — fires when the agent tries to end its turn.
Exit codes carry the meaning. Exit 0 is normal; exit 2 is "no." A PreToolUse hook that exits 2 blocks the tool call, and whatever it printed to stderr is handed back to the model as the reason. The Stop hook inverts that: exit 2 there refuses to let the agent stop, and stderr becomes the instruction for what to do next. That single mechanic turns "please run the tests before finishing" from a hope into a wall.
Wiring it up is a matcher plus a command in settings.json:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{ "type": "command", "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/format.sh" }
]
}
]
}
}
The formatter now runs on every edit, forever, with no dependence on the model remembering to. That reliability — a deterministic check firing unconditionally — is worth more than a probabilistic one that fires most of the time.
Sub-agents keep the main context clean
The second failure mode is quieter. A long session fills its context window with grep output, log tails, file dumps, and dead ends the agent will never reference again. Quality degrades as the window fills with noise, and the agent starts losing the thread of what it was actually doing.
A sub-agent is the fix. It runs a side task in its own context window and returns only a summary to the main conversation. The verbose exploration — the forty files it read to answer one question — stays isolated and never pollutes the parent. Sub-agents are Markdown files with YAML frontmatter, dropped in .claude/agents/ for a project or ~/.claude/agents/ for every project on your machine:
---
name: code-reviewer
description: Reviews a diff for correctness and style. Use after writing code.
tools: Read, Grep, Glob
model: sonnet
---
You are a review specialist. For each issue, explain the problem, show the
current code, and provide a corrected version. Do not modify files.
Three fields do real work here. tools restricts capability — a reviewer with only Read, Grep, and Glob physically cannot edit your code, so a constraint you'd otherwise state as a wish becomes structural. model routes cost — send search-heavy grunt work to a cheaper, faster model and keep the expensive one for judgment. And description is how delegation gets decided: the parent reads it to choose when to hand off, so a vague description means a sub-agent that never gets called. Beyond your own, there are built-ins — Explore for read-only codebase search, Plan for research before edits, general-purpose for multi-step work.
The loop is the actual engine
Strip agentic coding down and you get a cycle: observe, act, verify, iterate. Plain autocomplete stops at act — it emits code and moves on. What makes an agent an agent is the back half: it checks its own output against reality and keeps going until the check passes. Everything reliable about the workflow lives in that verify step.
Hooks and sub-agents exist to close that loop. Compose the two and you get something that self-corrects: the agent writes code, the Stop hook runs the build and the affected tests, and if they're red it exits 2 and feeds the failure back as the next instruction. The agent fixes, tries to stop again, gets checked again — and cannot escape until the suite is green. Then a review sub-agent reads the diff, because the author grading its own work is exactly how the "tests pass" lie survived in the first place.
A test that has never been seen to fail proves nothing. The same is true of an agent's own report that everything works — trust the loop that checked it, not the sentence that summarized it.
The caveat is cost. A verifier that runs after every turn multiplies your token spend, and a full re-review of untouched code is waste. Scope the loop: gate on the build and the tests that the change could plausibly break, not the entire suite and not files nobody edited. A tight loop that runs often beats a thorough one you disable because it's too slow.
Where to start
Don't build all of this at once. Start with one PostToolUse formatter and one Stop hook that runs your fast test suite and refuses to let the agent finish on red. That single loop kills the most common failure — the confident "tests pass" over code that doesn't — by the end of the afternoon you set it up. Add a review sub-agent once your diffs get large enough that reading them yourself is the bottleneck. The workflow scales because each piece removes a specific, observed failure, and you add the next one only when the last has paid for itself.
Sources: Claude Code — Hooks reference, Claude Code — Create custom subagents