# Two Agents, Two Worktrees: Codex Writes the Tests Blind, Claude Code Makes Them Pass

> Source: <https://dev.to/gabrielanhaia/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them-pass-2k9m>
> Published: 2026-10-24 08:01:37+00:00

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 <tests branch>`, followed by the Step 3 prompt. Once it starts, press Shift+Tab in its terminal to switch to accept-edits mode, the same as in any terminal ([permission modes](https://code.claude.com/docs/en/permission-modes)). Claude Code stops to ask before running the git command. The attention inbox counts the agents blocked on you, and ⌘I jumps to the one that has waited longest. You answer in the agent's own terminal, and you see the exact command before it runs. Hermes never answers a permission prompt for you.

When the suite is green, open the Review Desk (⌘G) on Claude's task. It shows changes by turn or by file. The cherry-pick itself shows up in the first turn. Anything under `test/` in a later turn means Claude found a way around the rule, and you can leave a line comment that goes straight back to Claude. Run the Step 4 commands in that task's worktree folder too, with Codex's `hermes/` branch name in place of `tests/duration`.

*The Review Desk (⌘G) in By turn view: each turn with its files and line counts, the selected turn's diff, and a commit message drafted from the turns.*

Then Land. It commits, opens a pull request or merges locally in one step, into the branch the task started from, and it can be undone. Landing Claude's task brings the cherry-picked tests and the implementation into `main` together. Hermes is free, source-available under BSL 1.1, and runs on macOS, Windows and Linux. It isn't affiliated with Anthropic, OpenAI, Nous Research or any agent vendor.

Git keeps the two agents' contexts apart and keeps the tests from drifting. It can't make the tests good. If Codex misreads the spec, Claude Code will implement the misreading. That's why Step 2 ends with you reading the tests.

It also doesn't stop Claude from overfitting. A function that special-cases `"1h30m"` passes a test that only checks `"1h30m"`. More spec examples help, and so does reading `git diff --stat` for anything that looks like a lookup table. I don't have evidence that a split workflow ships fewer bugs. What it does give you is narrower: the tests you grade against were written before the code and stayed put while the code was written.

If you try it, I'd like to know what broke first: Codex's tests, Claude's patience with them, or the deny rule. Tell me in the comments.

A test suite the implementing agent can't edit is a guardrail, and guardrails are half of what *AI That Ships* is about. The book covers evals, guardrails, cost control and deploying on Node.js, in TypeScript, for LLM features you have to keep running after the demo. If the `git diff --exit-code` check above felt like the right instinct, the book applies that instinct to everything around the model.

It is book 5 of *AI in TypeScript*, a five-book series that runs from your first LLM call to agents in production. If you want to run this two-agent setup without juggling terminals, [download Hermes IDE](https://hermes-ide.com/download) or [star it on GitHub](https://github.com/hermes-hq/hermes-ide).
