Two Agents, Two Worktrees: Codex Writes the Tests Blind, Claude Code Makes Them Pass A developer demonstrated a two-agent TDD workflow that enforces test/implementation separation through git rather than prompts: Codex writes tests from a spec alone in its own worktree and branch, the tests are committed and cherry-picked into a second worktree where Claude Code implements against them under a deny rule on the test paths, and a single `git diff` verifies the tests were not altered. The setup uses Node's built-in test runner with native TypeScript type stripping and a `.claude/settings.json` deny list covering `/test/**`, `package.json` and `.claude/**`. Ask one agent for parseDuration and its tests in a single prompt, and you get a function and a test file that agree with each other. Whether either of them agrees with your spec is a different question, and the tests can't answer it. They were written by something that already knew what the code would do. People building TDD workflows for agents keep running into this. A Claude Code TDD write-up on alexop.dev https://alexop.dev/posts/custom-tdd-workflow-claude-code-vue/ describes how, in one context window, "the test writer's detailed analysis bleeds into the implementer's thinking," and splits the work into separate test-writer and implementer subagents. The tdd-context-split https://github.com/asmodehn/tdd-context-split skill, under active development in 2026, is built on "2 agents with minimal sharing": the test author and the implementer "do not share a context, only the spec." Anthropic's own Claude Code best practices https://code.claude.com/docs/en/best-practices put it in one line: "have one Claude write tests, then another write code to pass them." Those barriers live in prompts and subagent setups. A prompt asks the implementer not to touch the tests. This tutorial moves the wall into git. Codex https://learn.chatgpt.com/docs/developer-commands?surface=cli writes the tests from the spec alone, on its own branch in its own worktree, before any implementation exists. You commit them and cherry-pick that commit into a second worktree. Claude Code https://code.claude.com/docs/en/permissions works there under a deny rule on the test paths. Then a single git diff tells you whether the tests came back the way Codex left them. Start from a small TypeScript repo in ~/code/duration . Four files go on main before either agent starts. First the spec, which is all Codex gets: SPEC.md: parseDuration src/duration.ts exports parseDuration input: string : number . - Returns whole seconds. - Units are h, m and s. At least one part is required. - Parts appear in the order h, m, s, each at most once: "1h30m" - 5400, "45s" - 45, "2h" - 7200. - Leading and trailing whitespace is ignored: " 10m " - 600. - Anything else throws a RangeError: "", "90", "1x", "30m1h", "1h1h", "-5m". Then a stub, so the import resolves and every test starts red: // src/duration.ts export function parseDuration input: string : number { throw new Error not implemented: ${input} ; } The tests run on Node's built-in runner, so there is nothing to install. node:test https://nodejs.org/api/test.html has been stable since Node 20, and Node runs .ts files directly since type stripping became the default https://nodejs.org/api/typescript.html in Node 22.18.0 and 23.6.0. Type stripping only erases types, so imports need the .ts extension, and enums are out. { "name": "duration", "private": true, "type": "module", "scripts": { "test": "node --test \"test/ / .test.ts\"" } } The last file is the deny rule, in .claude/settings.json : { "permissions": { "deny": "Edit /test/ ", "Edit /package.json ", "Edit /.claude/ " } } According to the permissions docs https://code.claude.com/docs/en/permissions , Edit rules "apply to all built-in tools that edit files," and a leading / in project settings anchors the pattern at the session's primary working directory. In a worktree session, that is the worktree's own folder. The package.json line stops a quiet rewrite of the test script, and the .claude/ line keeps the rule file itself out of reach. "Deny rules block in every mode," says the permission modes page https://code.claude.com/docs/en/permission-modes , including bypassPermissions . Deny rules in project settings also take effect without the workspace trust step, because they only restrict. Commit all four files. The rule is there for this experiment. Take it out later if you want Claude writing tests in this repo again. Give each agent its own worktree, both cut from main : cd ~/code/duration git worktree add -b tests/duration ../duration-tests main git worktree add -b impl/duration ../duration-impl main cd ../duration-tests codex --sandbox workspace-write --ask-for-approval on-request Both flags are from the Codex CLI reference https://learn.chatgpt.com/docs/developer-commands?surface=cli . Then give Codex the task: Read SPEC.md. Write tests for parseDuration in test/duration.test.ts using node:test and node:assert/strict. Import from "../src/duration.ts". Every expected value must come from SPEC.md. Cover each example and each RangeError case. Do not create or change anything outside test/. Run npm test: it should fail, because src/duration.ts is a stub. Nothing to peek at exists yet. The only code in the repo is a stub that throws. The /tdd skill on AI Hero https://www.aihero.dev/skills-tdd has the rule worth holding Codex to: "Expected values have to come from somewhere else: a known-good literal, a worked example, the spec." The kind of test you want back looks like this: python import { test } from "node:test"; import assert from "node:assert/strict"; import { parseDuration } from "../src/duration.ts"; test "combines hours and minutes", = { assert.equal parseDuration "1h30m" , 5400 ; } ; test "rejects parts out of order", = { assert.throws = parseDuration "30m1h" , RangeError ; } ; This tutorial doesn't rely on a path rule for Codex. You check its work with git before anything is committed: git status --short npm test git add test git commit -m "Tests for parseDuration, from SPEC.md" git status --short should list paths under test/ and nothing else. npm test should fail. If a test passes against a stub that throws, it isn't testing anything, so send it back. Read the tests here, too. This is the one point in the run where a human checks that they match the spec. Move to the implementation worktree and bring the test commit in: cd ../duration-impl git cherry-pick tests/duration npm test claude --permission-mode acceptEdits git cherry-pick https://git-scm.com/docs/git-cherry-pick applies the change a commit introduces and records a new commit for it. It needs a clean working tree, which a fresh worktree has. npm test is red again, for the same reason. Now the task for Claude Code: npm test fails. Make it pass by changing src/ only. The tests in test/ come from SPEC.md and are fixed: you can't edit them. If a test looks wrong to you, stop and tell me which one and why. An agent that can't edit a test it disagrees with needs a way out other than working around it. The last line of the prompt is that way out. In acceptEdits mode, Claude Code edits files without asking and also auto-approves mkdir , touch , rm , rmdir , mv , cp and sed inside the working directory permission modes https://code.claude.com/docs/en/permission-modes . Other Bash commands still prompt, apart from a built-in read-only set. The deny rule sits above all of that. When the suite is green, compare the guarded paths with the commit Codex's tests came from: git diff --exit-code tests/duration -- \ test/ package.json .claude/ git status --short -- test/ git diff --stat tests/duration The first command compares your working tree with the tip of tests/duration on just those paths, and exits non-zero if anything differs. The second catches new, untracked files under test/ , which git diff doesn't show. The third shows what Claude changed overall, which should be src/ alone. You need this check because the deny rule has an edge, and the docs say exactly where it is. Read and Edit deny rules cover Claude's built-in file tools, file commands Claude Code recognizes in Bash "such as cat , head , tail , sed , and tee ," and redirect targets like file . They don't cover "arbitrary subprocesses that read or write files indirectly, like a Python or Node script that opens files itself." For a block at the OS level, the docs point to sandboxing https://code.claude.com/docs/en/sandboxing . The rule makes it hard for Claude to edit a test. The diff tells you whether it did anyway. If all three commands come back clean, commit and merge: git add src git commit -m "Implement parseDuration" cd ~/code/duration git merge impl/duration git worktree remove ../duration-tests git worktree remove ../duration-impl git branch -d impl/duration git branch -D tests/duration git branch -d refuses tests/duration , and it's right to. main holds a cherry-picked copy of the test commit, not the commit itself, so git can't see that branch as merged. Use -D once the diff check has passed. I build Hermes IDE https://hermes-ide.com , so weigh this section with that in mind. Everything above works in plain terminals. Hermes saves you the bookkeeping, and it doesn't replace the git diff check. Press ⌘N Ctrl+Shift+N on Windows and Linux , pick Codex in the agent picker, and paste the Step 2 prompt as the task. Hermes starts Codex in a real terminal on its own hermes/ branch in its own worktree, with the task as its first prompt. As in the screenshot below, the sidebar lists each session's branch, so you can read the tests branch name off it. Commit Codex's test files from the Review Desk, which handles stage, unstage and commit for each project. Codex and Claude Code side by side in Hermes 2.3.4, each in its own hermes/ worktree, with status, context use, cost and branch in the sidebar. Press ⌘N again and pick Claude Code. Make the first line of the task git cherry-pick