{"slug": "two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them", "title": "Two Agents, Two Worktrees: Codex Writes the Tests Blind, Claude Code Makes Them Pass", "summary": "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/**`.", "body_md": "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.\n\nPeople 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.\"\n\nThose 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.\n\nStart 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:\n\n```\n# SPEC.md: parseDuration\n\n`src/duration.ts` exports\n`parseDuration(input: string): number`.\n\n- Returns whole seconds.\n- Units are h, m and s. At least one part is required.\n- Parts appear in the order h, m, s, each at most once:\n  \"1h30m\" -> 5400, \"45s\" -> 45, \"2h\" -> 7200.\n- Leading and trailing whitespace is ignored:\n  \" 10m \" -> 600.\n- Anything else throws a RangeError:\n  \"\", \"90\", \"1x\", \"30m1h\", \"1h1h\", \"-5m\".\n```\n\nThen a stub, so the import resolves and every test starts red:\n\n```\n// src/duration.ts\nexport function parseDuration(input: string): number {\n  throw new Error(`not implemented: ${input}`);\n}\n```\n\nThe 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.\n\n```\n{\n  \"name\": \"duration\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"test\": \"node --test \\\"test/**/*.test.ts\\\"\"\n  }\n}\n```\n\nThe last file is the deny rule, in `.claude/settings.json`:\n\n```\n{\n  \"permissions\": {\n    \"deny\": [\n      \"Edit(/test/**)\",\n      \"Edit(/package.json)\",\n      \"Edit(/.claude/**)\"\n    ]\n  }\n}\n```\n\nAccording 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.\n\nCommit all four files. The rule is there for this experiment. Take it out later if you want Claude writing tests in this repo again.\n\nGive each agent its own worktree, both cut from `main`:\n\n```\ncd ~/code/duration\ngit worktree add -b tests/duration ../duration-tests main\ngit worktree add -b impl/duration ../duration-impl main\ncd ../duration-tests\ncodex --sandbox workspace-write --ask-for-approval on-request\n```\n\nBoth flags are from the [Codex CLI reference](https://learn.chatgpt.com/docs/developer-commands?surface=cli). Then give Codex the task:\n\n```\nRead SPEC.md. Write tests for parseDuration in\ntest/duration.test.ts using node:test and\nnode:assert/strict. Import from \"../src/duration.ts\".\nEvery expected value must come from SPEC.md. Cover\neach example and each RangeError case. Do not create\nor change anything outside test/. Run npm test: it\nshould fail, because src/duration.ts is a stub.\n```\n\nNothing 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:\n\n``` python\nimport { test } from \"node:test\";\nimport assert from \"node:assert/strict\";\nimport { parseDuration } from \"../src/duration.ts\";\n\ntest(\"combines hours and minutes\", () => {\n  assert.equal(parseDuration(\"1h30m\"), 5400);\n});\n\ntest(\"rejects parts out of order\", () => {\n  assert.throws(() => parseDuration(\"30m1h\"), RangeError);\n});\n```\n\nThis tutorial doesn't rely on a path rule for Codex. You check its work with git before anything is committed:\n\n```\ngit status --short\nnpm test\ngit add test\ngit commit -m \"Tests for parseDuration, from SPEC.md\"\n```\n\n`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.\n\nMove to the implementation worktree and bring the test commit in:\n\n```\ncd ../duration-impl\ngit cherry-pick tests/duration\nnpm test\nclaude --permission-mode acceptEdits\n```\n\n[`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:\n\n```\nnpm test fails. Make it pass by changing src/ only.\nThe tests in test/ come from SPEC.md and are fixed:\nyou can't edit them. If a test looks wrong to you,\nstop and tell me which one and why.\n```\n\nAn 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.\n\nIn `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.\n\nWhen the suite is green, compare the guarded paths with the commit Codex's tests came from:\n\n```\ngit diff --exit-code tests/duration -- \\\n  test/ package.json .claude/\ngit status --short -- test/\ngit diff --stat tests/duration\n```\n\nThe 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.\n\nYou 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.\n\nIf all three commands come back clean, commit and merge:\n\n```\ngit add src\ngit commit -m \"Implement parseDuration\"\ncd ~/code/duration\ngit merge impl/duration\ngit worktree remove ../duration-tests\ngit worktree remove ../duration-impl\ngit branch -d impl/duration\ngit branch -D tests/duration\n```\n\n`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.\n\nI 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.\n\nPress ⌘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.\n\n*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.*\n\nPress ⌘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.\n\nWhen 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`.\n\n*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.*\n\nThen 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.\n\nGit 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.\n\nIt 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.\n\nIf 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.\n\nA 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.\n\nIt 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).", "url": "https://wpnews.pro/news/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them", "canonical_source": "https://dev.to/gabrielanhaia/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them-pass-2k9m", "published_at": "2026-10-24 08:01:37+00:00", "updated_at": "2026-10-24 08:12:56.637270+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Codex", "Claude Code", "Anthropic", "Node.js", "alexop.dev", "tdd-context-split"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them", "markdown": "https://wpnews.pro/news/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them.md", "text": "https://wpnews.pro/news/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them.txt", "jsonld": "https://wpnews.pro/news/two-agents-two-worktrees-codex-writes-the-tests-blind-claude-code-makes-them.jsonld"}}