Test-driven development with Claude Code

Claude stops when work looks done, so give it a failing test to run. Our TDD prompt in three steps, and a Stop hook that runs tests when Claude finishes.

TEST FIRST beside a real screenshot of Shuriken in its Terminal look: monospace text and a › prompt, under "Test-driven development with Claude Code"
Contents

Test-driven development suits Claude Code because a failing test gives the agent a check it can run by itself. Write the test, watch it fail, then ask for the fix: the agent keeps going until npm test passes, not until the code looks done. "Test first" is one of our house rules for that reason.

Why tests first suits an agent#

Anthropic's best-practices page names the problem: "Claude stops when the work looks done. Without a check it can run, 'looks done' is the only signal available, and you become the verification loop" (Claude Code docs).

A failing test is a check it can run. It is also a contract. It says what "done" means, in code, before any code exists.

We like it for two more reasons. The test comes before the agent has an opinion, and a test written after the code tends to describe what the code does, bugs included. And a test outlives the chat: it stays in the repo through /clear, compaction and the next session, which a sentence in a prompt doesn't.

Our TDD prompt pattern for Claude Code#

Three steps, in one chat.

1. Ask for the test, and only the test.

text
Write a failing test in src/billing/invoice.test.ts: an invoice with a 100%
discount totals 0, never a negative number. Don't touch the implementation.
Run that test file and show me the failure.

2. Read the red. Does it fail because the behaviour is missing? Or because of a typo, a wrong import or a missing fixture? A test that fails for the wrong reason will pass for the wrong reason too. This is the one step you shouldn't hand over.

3. Ask for the fix.

text
Now make that test pass without changing the test. Then run the billing tests
and the type check, and paste the output.

Anthropic's docs suggest the same shape for bugs: "write a failing test that reproduces the issue, then fix it". "Without changing the test" matters, because the shortest path to green can be editing the test. And ask for the output, not a summary. The docs say it too: "Have Claude show evidence rather than asserting success."

One test at a time#

Our own testing guide, the one the agents in our repo read, says to work in vertical slices: one test, one implementation, repeat. It also says why: "Writing all tests first then all implementation produces bad tests — you end up testing imagined behavior instead of actual behavior."

So we never ask for twenty tests up front. One test, green, next test. Each new test reacts to what the last one taught you.

A Stop hook that runs your tests#

A prompt is advice. A hook is a rule. Claude Code runs a Stop hook when Claude finishes responding. If the hook exits with code 2, it "prevents Claude from stopping, continues the conversation", and Claude reads what the hook printed as the reason (hooks reference).

Two columns under the label: the Stop hook runs npm test. Tests fail: the hook prints the output and exits with code 2, and Claude keeps going and reads why. Tests pass: the hook exits 0 and Claude stops. Below: if Claude was already sent back once this turn, the hook lets it stop.
With the hook in place, a red test sends Claude back to work instead of letting it stop.

In .claude/settings.json:

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

And .claude/hooks/run-tests.sh (make it executable with chmod +x; it needs jq):

bash
#!/bin/bash
INPUT=$(cat)
# Already sent back once by this hook? Let Claude stop, so it can't loop.
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
  exit 0
fi
if ! OUTPUT=$(npm test 2>&1); then
  echo "Tests are failing. Fix the code, not the tests:" >&2
  echo "$OUTPUT" | tail -n 40 >&2
  exit 2
fi
exit 0

Before you rely on it, know three things.

  • It can't loop forever. stop_hook_active is true "when Claude Code is already continuing as a result of a stop hook". The check above gives Claude one extra try per turn. Without it, Claude Code overrides the hook after it blocks eight times in a row with no tool call in between (hooks guide).
  • Point it at fast tests. The hook runs every time Claude finishes responding, even after a one-line answer. A five-minute suite on every reply gets old quickly.
  • There's a model-based version too. Anthropic's hooks guide shows an agent hook that runs the suite through a subagent before Claude may stop. It is marked experimental, and each check is a model call against your plan. The shell script is plain and free.

What we learned the hard way#

We often have several agents in one repository at once. Early on, an agent that wanted to prove its test "fails first" would undo the fix, watch the test go red and put the fix back, and with other agents editing the same files, that undo could wipe someone else's edit.

The answer was the rule itself. Write the test before the fix, and the red comes for free. Nothing has to be undone to prove it.

A second rule came from the same place: run the test file you changed, not the whole suite. Our suites are heavy, and several agents running them at once can freeze a machine. Long runs also cost context, which is one of the reasons Claude Code uses so many tokens.

Where Shuriken fits#

Tests first is the habit. Shuriken gives it a clean place to run. Every chat except a project's Main chat works on its own branch in its own git worktree, so a red test in one of those chats never comes from another agent's half-finished edit (why every chat gets its own worktree). That's what makes running Claude Code agents in parallel safe enough to try.

Your repository on the left. The Main chat works in your own folder on your branch. Chats 1, 2 and 3 each get their own worktree folder on their own branch. Arrows from the three chats go through Finish, which runs your checks and then merges into your branch or opens a pull request. Your repoone history,many checkoutsMain chatyour folder · your branchChat 1worktree 1 · its own branchChat 2worktree 2 · its own branchChat 3worktree 3 · its own branchquick changes in placeFinishruns your checksMerge intoyour branchor a pullrequestEvery chat except Main works alone on its own branch. Finish brings the work back.
Every chat but Main gets its own worktree, so a red test belongs to that chat alone.

When a chat is done, Finish runs the checks you list in shuriken.json, such as your type check and tests, before it rebases and merges or opens a pull request with auto-merge. The details are in Finish: checks, rebase and merge in one click.

If you want to try that loop on your own repository, try Shuriken.

Sources

  1. Best practices for Claude Code (Anthropic) (accessed 22 October 2026)
  2. Hooks reference: Stop (Anthropic) (accessed 22 October 2026)
  3. Automate actions with hooks (Anthropic) (accessed 22 October 2026)

Frequently asked questions

Should Claude Code write the tests or should I?

Either works, as long as someone reads the failing test before any code is written. Ask Claude for the test only, check that it fails for the right reason, then ask for the fix in a separate step.

How do I stop Claude Code from changing the test to make it pass?

Say so in the prompt: make the test pass without changing the test. A Stop hook that runs the suite catches a fix that breaks other tests, but only you can tell whether a test was weakened, so look at the diff.

Can a Stop hook loop forever?

No. Claude Code ends the turn after a Stop hook blocks eight times in a row with no tool call in between. Checking the stop_hook_active field in your script stops it earlier, after one retry.

Should the Stop hook run the whole test suite?

Only if it is fast. The hook runs every time Claude finishes responding, so point it at your quick unit tests and leave the slow suites to CI or to the end of the task.

Free tools