Guides

Make your coding agent rerun the tests before it says done.

Quick answer

Add a Stop hook in .claude/settings.json that runs your test suite. If the tests fail, the script exits with code 2 and writes the failure to stderr. Claude Code then doesn’t end the turn: it hands Claude the stderr text as the reason to keep working. It’s a deterministic check, but not a complete one, because the agent can still edit the tests. Keep a trusted copy to compare against.

I checked everything below against Anthropic’s hooks reference and hooks guide on 14 October 2026. Hooks change between versions, so check them again if you’re reading this much later.

What does a Stop hook do?

Hooks are shell commands Claude Code runs at fixed points in a session. The docs’ lifecycle table lists Stop as firing “when Claude finishes responding.” A Stop hook can block that. Per the reference, exit code 2 on Stop “prevents Claude from stopping, continues the conversation”, and “Claude receives the stderr message as the explanation for why it should continue.”

That’s the whole trick. Instead of hoping the agent reran the suite after its last edit, you run it every time the agent tries to finish.

The Claude Code best-practices page lists this exact use: “a Stop hook runs your check as a script and blocks the turn from ending until it passes.”

How do I set it up?

Two files. First, register the hook in .claude/settings.json at the project root (or .claude/settings.local.json if you don’t want to commit it):

json
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/rerun-tests.sh",
            "timeout": 300
          }
        ]
      }
    ]
  }
}

There’s no matcher here on purpose. The guide’s matcher table lists Stop among the events with “no matcher support”, which “always fires on every occurrence.” timeout counts seconds, and the default for command hooks is 600. $CLAUDE_PROJECT_DIR is, in the docs’ words, “the project root where the session started.”

Then the script, at .claude/hooks/rerun-tests.sh:

bash
#!/bin/bash
# Stop hook: rerun the tests before Claude Code can end its turn.
INPUT=$(cat)   # the hook's JSON input arrives on stdin
cd "$CLAUDE_PROJECT_DIR" || exit 0

# Skip the run if nothing changed since the last green run.
GIT_DIR=$(git rev-parse --git-dir)
STATE=$( { git rev-parse HEAD; git diff HEAD; git ls-files --others --exclude-standard; } | shasum )
if [ "$STATE" = "$(cat "$GIT_DIR/last-green" 2>/dev/null)" ]; then
  exit 0
fi

# Replace with your real test command.
if OUT=$(npm test 2>&1); then
  echo "$STATE" > "$GIT_DIR/last-green"
  exit 0
fi

echo "The tests fail, so the work is not done. Fix the code, not the tests." >&2
echo "Last lines of the run:" >&2
echo "$OUT" | tail -n 40 >&2
exit 2

Make it executable, as the guide says hook scripts must be on macOS and Linux:

bash
chmod +x .claude/hooks/rerun-tests.sh

Run /hooks inside Claude Code to confirm it’s listed under Stop. The guide notes that the settings file watcher “normally picks up hook changes automatically” if you edit while a session is running.

Why does the script skip unchanged runs?

Because Stop fires more often than you’d think. The guide’s limitations section says Stop hooks “fire whenever Claude finishes responding, not only at task completion.” A clarifying question to you counts. Without the skip, you’d wait for the full suite every time the agent asks which file you meant.

The fingerprint is the current commit, the uncommitted diff, and the names of untracked files. If none of those moved since the last green run, there’s nothing new to test. It’s stored inside the git directory so it never shows up in a diff. It doesn’t hash the contents of untracked files, so a change only inside a new file the agent already created won’t trigger a rerun. Hash those files too if that matters to you.

Won’t this loop forever?

No, there’s a documented cap. The reference says Claude Code “applies an 8-consecutive-continuation cap: after stop hooks have continued the turn eight times in a row, Claude Code overrides the next block and ends the turn.” The count resets when Claude calls a tool. So an agent that keeps trying fixes keeps getting checked, and one that just keeps replying without doing anything gets let through after eight.

The docs also describe a stop_hook_active field in the input, which is true “when Claude Code is already continuing as a result of a stop hook”, and suggest exiting early when it’s set. I don’t, in this script. Exiting early would let the agent stop after one failed retry with the suite still red, which is the case the hook exists for. If your suite takes long, or failures often need you, use the documented pattern instead:

bash
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi

What can’t a Stop hook catch?

This is where it gets honest. A green suite at the end of the turn proves the suite is green. It doesn’t prove the suite still tests what it used to.

  • The agent can edit the tests. Loosen an assertion, change an expected value, add a skip. The hook goes green. This isn’t hypothetical: in ImpossibleBench (October 2025), Claude models mostly got through impossible tasks by modifying test cases.
  • The agent can edit the hook. .claude/settings.json and the script are files in the repo. The hooks guide itself notes that “Claude can also create or modify files by running shell commands”, so blocking the edit tools alone doesn’t close that door.
  • New code with no tests passes. A stub that nothing calls breaks nothing.
  • It knows nothing about the task. The hook can’t tell whether the change does what you asked.

How do I keep a trusted copy of the tests?

Compare the test files against a commit the agent didn’t write: the point where the branch left main. If they changed and you haven’t approved the change, make the agent say so to you before it can finish. Add this to the script after the skip check and before the test run:

bash
BASE=$(git merge-base HEAD origin/main)
CHANGED=$(git diff --name-only "$BASE" -- tests/)
if [ -n "$CHANGED" ] && [ ! -f "$GIT_DIR/tests-approved" ] \
   && [ "$STATE" != "$(cat "$GIT_DIR/tests-flagged" 2>/dev/null)" ]; then
  echo "$STATE" > "$GIT_DIR/tests-flagged"
  echo "Test files changed since the branch point:" >&2
  echo "$CHANGED" >&2
  echo "In your final message, list each one and why it changed." >&2
  echo "Tell the user the tests changed. Do not call the work done." >&2
  exit 2
fi

It blocks once for each new state of the working tree, so the agent has to put the test changes in front of you instead of folding them into “all tests pass”. When you’ve read them and agree, run touch "$(git rev-parse --git-dir)/tests-approved", and delete that file when the branch lands. Swap tests/ for your test paths.

ImpossibleBench found that read-only test access blocked test modification while keeping legitimate performance, though it didn’t stop other shortcuts like special-casing. This is a light version of that idea: the agent can still edit tests, it just can’t do it quietly.

For a harder line, keep the test suite in a second checkout the agent’s session can’t see, and run it from there against the agent’s branch. The same trust boundary applies: the copy is only trusted if the agent can’t reach it.

Where does a reviewer go further?

A hook answers one question deterministically: is the suite green right now. The questions it can’t answer need something that reads: did a test get weaker, is a function a stub, does the change match the ask. Claude Code’s docs offer an experimental agent hook type that runs a subagent to verify before stopping, and the best-practices page suggests a fresh-context reviewer so “the agent doing the work isn’t the one grading it.” I covered that pattern in should one coding agent review another’s work?, and the checks to do by hand in your coding agent said done, here’s how to check.

Where plottypus fits

Set up the hook either way. It’s cheap and it closes the most common gap. plottypus is the reviewer half: a second agent that follows the Claude Code session, checks every “done” against what actually ran (did the tests run after the last edit?), reads the test edits, and sends the agent back when it won’t hold, while it can still fix it. How we measure it, misses included, is on the evals page. If you want to try it, start a free trial.

Sources

Checked 14 October 2026.


plottypus is independent and not affiliated with any company or project named here. Claude Code is a trademark of Anthropic. Names belong to their owners. This is our research and opinion, dated above; details may have changed.