How to force red-green-refactor discipline on Claude Code with subagents and hooks
By VCA Newsroom
Test-driven development is a simple loop: write a failing test (red), write the minimum code to pass it (green), then clean up the design (refactor). Ask an AI coding agent to follow it and you hit a structural problem almost immediately — large language models default to implementation-first development. Prompt one to build a feature and it writes the happy path, then backfills tests around whatever it just wrote.
Forcing TDD inside a single conversation doesn't fix this. It creates a subtler failure instead: context pollution. The requirements and reasoning from the test-writing phase bleed into the implementation phase, so the model writes tests that fit the implementation it is already planning. The tests still pass. They just aren't testing anything the model didn't already intend to do, which defeats the point of writing them first.
The fix, worked through in detail in Alex Opalic's write-up of a Claude Code TDD skill, is to stop trying to make one agent behave and instead give each phase of the cycle its own isolated context.
Give each phase its own subagent
Each phase runs as a separate subagent that starts with only the context it needs and nothing else:
- The TDD skill (
.claude/skills/tdd-integration/skill.md) is the orchestrator. It manages the high-level workflow and defines explicit "do NOT proceed until…" gates between states. Phase emojis (🔴, 🟢, 🔵) make the active phase easy to track in terminal output. - The test writer (
.claude/agents/tdd-test-writer.md) owns the red phase. It's restricted to test-writing and test-running tools, focuses purely on behavioural requirements, and has no knowledge of how the feature will be implemented. - The implementer (
.claude/agents/tdd-implementer.md) owns the green phase. It sees the failing test and the codebase, and writes the minimum code that satisfies the assertions. - The refactorer (
.claude/agents/tdd-refactorer.md) owns the refactor phase. With passing tests as a safety net it evaluates the design and extracts reusable logic — and its decision framework lets it skip refactoring when none is warranted.
The isolation is the whole trick. The test writer can't design tests around an implementation it has never seen.
Make the skill actually fire
Defining the skill is the easy half. Claude Code is prone to skipping structured workflows in favour of just getting on with the code, and in practice the skill activates only about 20% of the time — which means four times out of five you get exactly the implementation-first behaviour you built the skill to prevent.
The workaround is a lifecycle hook. Opalic points to Scott Spence's research on skill activation, which tested 200+ prompts across different hook configurations and found that a "forced eval" approach lifted activation from roughly 20% to roughly 84%. The mechanism: register a hook on UserPromptSubmit that runs before every prompt reaches the model and forces Claude to explicitly evaluate whether the skill should trigger before it does anything else.
In .claude/settings.json:
{
"hooks": {
"UserPromptSubmit": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "npx tsx \"$CLAUDE_PROJECT_DIR/.claude/hooks/user-prompt-skill-eval.ts\"",
"timeout": 5
}
]
}
]
}
}
Note that hooks are configured as an array of matcher groups, each containing the commands to run — not a bare path string. The $CLAUDE_PROJECT_DIR variable keeps the path portable across machines.
What a full cycle looks like
Opalic's worked example is a frontend feature in a Vue app: a workout detail view, where clicking a past workout opens a breakdown of its exercises and sets.
🔴 Red
The skill triggers and delegates to tdd-test-writer. The agent uses a createTestApp() helper to initialise application state — Pinia, Vue Router, a jsdom environment — and writes an integration test asserting that clicking a workout card navigates to the detail page and displays the completed sets. It runs the test and it fails cleanly: the view component and route don't exist yet.
That clean failure matters. A test that fails for the wrong reason — a typo, a missing import — is a false red, and it will go green later for reasons that have nothing to do with your feature.
🟢 Green
Control passes to tdd-implementer, which gets the failing test and the codebase and nothing about the earlier discussion. It creates WorkoutDetailView.vue, adds a click handler to the parent workouts view, and registers the route. Tests re-run and pass.
🔵 Refactor
tdd-refactorer takes over with passing tests as a safety net. It extracts the data-fetching logic into a reusable composable (useWorkoutDetail), moves duration and date formatters into lib/formatters.ts, and adds keyboard navigation to the clickable cards. It re-runs the tests to confirm no regressions, then commits.
It isn't really about Vue
The example stack is Vue, but nothing in the architecture depends on it. React, Svelte, Angular, Python, Go, Rust — the red-green-refactor orchestration is identical, because the thing being fixed isn't a framework problem. It's a context problem.
The setup costs a couple of hours: three subagent definitions, one skill, one hook. What you get back is an agent that can't quietly skip the test — and on a codebase you intend to keep, that's the difference between tests that describe the behaviour you wanted and tests that describe the code you happened to get.
SOURCES
Auto-generated by Vibe Coding Academy on July 26, 2026, grounded in the real sources linked above. We review for accuracy, but please verify time-sensitive details against the primary sources.
Build Blueprint · Builder
Have an idea? Get the spec your AI agent can build from.
Describe any product and get a complete build blueprint — stack, data model, screens, APIs, and a ready-to-paste prompt for Claude Code or Cursor. Export to PDF.
Open the Blueprint