Back to posts
AINews

Your coding agent's only feedback loop is you catching mistakes — two hooks that close it

Claude Code cannot tell a passing build from a broken one unless something tells it. Two verified hook recipes — a PostToolUse check that feeds a failing type-check back after every edit, and a Stop hook that blocks the agent from ending a turn until tests pass — turn manual review into something the agent does to itself.

Every review pass you do by hand on an AI-written diff is a feedback signal the agent never got. You run tsc, see the error, paste it back, and the agent fixes it. That loop works, but you are the loop. The agent has no way to know it broke something until you tell it, so it keeps producing code that looks finished and is not.

Hooks close that gap without touching the model. They are plain shell commands Claude Code runs at fixed points in its own lifecycle, so a check that would otherwise depend on you remembering to run it becomes something that always happens.

Two points in the loop are worth wiring

After every edit. A PostToolUse hook with an Edit|Write matcher runs after Claude edits or writes a file — before the change ever reaches your review. Claude Code's own docs use this event for an auto-formatter: run Prettier on the file that was just touched, silently, every time. The same event can run a type-check or a linter instead, and the detail that makes it useful is PostToolUse's exit-code behavior: exit 2 shows your script's stderr to Claude as context, even though the tool already ran and cannot be undone. The agent sees the exact compiler error, in the same turn, without you copying anything.

.claude/settings.json:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/typecheck.sh",
            "timeout": 60
          }
        ]
      }
    ]
  }
}

.claude/hooks/typecheck.sh:

#!/bin/bash
input=$(cat)
file_path=$(jq -r '.tool_input.file_path' <<< "$input")

case "$file_path" in
  *.ts|*.tsx)
    if ! output=$(cd "$(dirname "$file_path")" && npx tsc --noEmit 2>&1); then
      echo "$output" >&2
      exit 2
    fi
    ;;
esac
exit 0

Make it executable (chmod +x .claude/hooks/typecheck.sh) and every TypeScript edit gets checked the moment it happens. Swap the tsc line for npx eslint "$file_path" or your test runner and the same shape covers linting or unit tests.

Before the agent decides it is done. PostToolUse catches mistakes per-file, but nothing stops Claude from touching five files, breaking the interaction between two of them, and declaring the task finished anyway. That is what the Stop event is for: it fires when Claude is about to end its turn, and an agent-based hook there can spawn a subagent that runs your test suite for real — not read a stale log, run the commands — before the turn is allowed to end.

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "agent",
            "prompt": "Verify that all unit tests pass. Run the test suite and check the results. $ARGUMENTS",
            "timeout": 120
          }
        ]
      }
    ]
  }
}

If the verifying subagent finds a failure, it returns "ok": false with a reason, and that reason goes back to Claude as its next instruction instead of the turn simply ending. You never see the failed state at all — the agent fixes it and only stops once the check passes. Agent-based hooks are marked experimental in Claude Code's docs (command hooks are the recommended default for anything production-critical), but for a local "did the suite pass" gate the tradeoff is worth it.

Why the two events, not one

PostToolUse is fast and cheap — it runs a fixed command against one file, right after the edit that touched it. It is good at the mechanical stuff: format, type-check, lint. It cannot see whether the whole change set still hangs together, because it only fires per tool call.

Stop is the opposite: slower (spawning a subagent, running a real suite), but it is the only point that sees the finished state of the turn. Wiring both means the cheap check catches syntax-level breakage the instant it happens, and the expensive check catches everything that only shows up once all the pieces are in place. Either alone leaves a gap — PostToolUse-only misses cross-file breakage, Stop-only means Claude can burn several edits down a broken path before the gate ever fires.

What this buys you

Once both hooks are in place, "review the diff" stops being how you catch mistakes and becomes how you catch decisions — architecture, naming, whether this was the right thing to build at all. The mechanical failures (bad syntax, a broken type, a red test) get caught and handed back to the agent before you are in the loop, which is the same shift a compiler gave programmers over manually re-reading code for typos: not fewer mistakes made, but a machine that catches the boring ones so a human catches the ones that matter.

Start with one hook, not both. A PostToolUse type-check on the languages you ship is a five-minute change with an immediate, visible effect: watch the next session where Claude edits a .ts file and breaks a type, and see it fix its own error before you ever open the file. Add the Stop-hook test gate once that is working and you trust the shape.

Full hook event list, exit-code table, and the full command/prompt/agent hook schema are in Claude Code's hooks reference and hooks guide — both worth reading end to end once this pattern earns its keep, since the same mechanism also covers blocking edits to protected files and enforcing project rules Claude cannot talk itself out of.

Get the next post when it ships

One email on Sunday with the new post and a short list of what shipped that week — new guides, tool updates, and a couple of links worth reading.