Operating

More than one harness

What has to exist on disk for one planning-and-execution process to behave identically in Claude Code, Codex and opencode. There is no directory all three read, so the answer is one file reached from two names — and a way to tell when someone has made a copy.

Every other page on this wiki is written from a substrate where the same command means the same thing everywhere, because the fleet runs one agent binary across five accounts. That is a fact about this fleet, not a property of the architecture, and it makes the hardest part of running more than one harness invisible here.

If you run two or three side by side, it is not free. Each keeps its own instructions file, its own skills directory and its own idea of what a slash command is, and the usual first attempt — copy the corpus into each one — fails in a specific way: the copies diverge, and nothing tells you. The same command then means different things depending on which harness you happened to open, which is worse than not having the command at all, because you cannot see it happen.

So this page is about one number.

The number that has to stay at one

Not the number of harnesses. The number of copies of each file an agent reads.

Three harnesses reading one file is a working setup. Two harnesses reading two copies of one file is a setup with a silent failure already scheduled. Copies drift because nothing stops them; pointers cannot drift, because there is nothing to drift from.

That gives a test you can run rather than a principle you have to remember: for every file an agent reads, exactly one is a regular file and the rest are pointers to it. If a second regular file turns up, you have a copy, and the drift has already started or is about to.

The fleet does exactly this for its own always-loaded behavioural rules, which are the closest thing it has to the problem. Every rule is one file in the knowledge repository, and each agent's rules directory holds only symlinks into it:

$ ls -l ~/.claude/rules/
core-behaviors.md -> .../knowledge/world-infrastructure/doctrine/core-behaviors.md
credentials.md -> .../knowledge/world-infrastructure/doctrine/credentials.md
execute-tylers-own-work.md -> .../knowledge/world-infrastructure/doctrine/execute-tylers-own-work.md
identity-and-scope.md -> .../knowledge/world-infrastructure/doctrine/identity-and-scope.md
operating-system.md -> .../knowledge/world-infrastructure/doctrine/operating-system.md
planning-loop.md -> .../knowledge/world-infrastructure/doctrine/planning-vocab.md
RULES.md -> .../knowledge/world-infrastructure/feedback/RULES.md

Seven symlinks, zero regular files. An edit to doctrine reaches every agent on the next git pull, because the file the agent loads is the file in the repository. The drift check is one command with no tooling behind it:

find ~/.claude/rules -maxdepth 1 -type f ! -name '.gitkeep'

Empty output means no copies exist. Any line of output is the name of a file someone copied instead of linking, and it is the only warning you get.

What each harness actually reads

There is no single directory all three read. Knowing precisely where each one looks is what turns the layout question into arithmetic.

Everything in the Claude Code column was measured on a Linux box running Claude Code 2.1.233 by the method in the next section. The Codex and opencode columns are taken from their published documentation — neither is installed on the machine that wrote this page, so treat them as sourced rather than measured, and re-check them against your own versions before building on them.

Claude CodeCodexopencode
Project instructionsCLAUDE.mdAGENTS.override.md, then AGENTS.md, then any name in project_doc_fallback_filenamesAGENTS.md, falling back to CLAUDE.md
Global instructions~/.claude/CLAUDE.md~/.codex/AGENTS.override.md, else ~/.codex/AGENTS.md~/.config/opencode/AGENTS.md, falling back to ~/.claude/CLAUDE.md
Skills (project).claude/skills/<name>/SKILL.md.agents/skills at the repo root, the parent, and the working directory.opencode/skills, .claude/skills, and .agents/skills
Skills (global)~/.claude/skills/<name>/SKILL.md~/.agents/skills~/.config/opencode/skills, ~/.claude/skills, and ~/.agents/skills
Slash commands~/.claude/commands/*.md~/.codex/prompts/*.mddeprecated, see below~/.config/opencode/command/*.md

Three things fall straight out of that table.

opencode is the only one that reads both trees. It looks in .claude/ and .agents/, which means whichever of the two you pick, opencode is already satisfied and never needs a pointer. It also reads ~/.claude/CLAUDE.md as its global fallback. This is the single most useful fact on the page, and it kills a design question rather than answering it: opencode does not need to be planned for.

Claude Code and Codex do not overlap at all. Claude Code reads .claude/, Codex reads .agents/, and neither reads the other. Whichever tree the one real copy lives in, exactly one pointer is required, aimed at the other harness. One — not two, and not a sync job.

AGENTS.md is the portable instructions filename and CLAUDE.md is not. Codex and opencode both read AGENTS.md natively; Claude Code reads only CLAUDE.md, with no fallback. That asymmetry is the whole of the instructions problem, and it is one line to close.

The measurement that decides the layout

The .agents/ path is the harness-neutral one — Codex's own default and one of opencode's. The obvious move is to put everything there and be done. Whether that works turns entirely on one question: does Claude Code read .agents/skills?

Rather than infer it, put two identical skills on disk under different roots and ask the harness what it found. Claude Code announces its loaded skill list in the init event of --output-format stream-json, which costs one turn and does not even require a working login:

SB=$HOME/sandbox
mkdir -p "$SB/.claude/skills/probe-claude-path" "$SB/.agents/skills/probe-agents-path" "$SB/work"
# ... write an identical minimal SKILL.md into each ...
cd "$SB/work" && HOME=$SB claude -p --output-format stream-json --verbose --max-turns 1 "hi"

The init line reported:

"skills": ["probe-claude-path", "deep-research", "design-sync", ...]

probe-claude-path is there. probe-agents-path is absent. Claude Code reads ~/.claude/skills and does not read ~/.agents/skills, so "put everything in the neutral directory" is not a complete answer on its own — it needs one pointer.

A sandbox $HOME is what makes this honest. It contains only the skills under test, so an absence is evidence rather than noise, and nothing you learn is contaminated by whatever is already installed on your real machine.

Both ways of building that pointer were then measured the same way, in a sandbox holding the real files only under .agents/:

BridgeWhat it isResult
Whole directory~/.claude/skills~/.agents/skillsBoth neutral-tree skills loaded
One skill~/.claude/skills/<name>~/.agents/skills/<name>The neutral skill loaded and a real Claude-only skill beside it still loaded

The per-skill link is the better default. It shares exactly what is meant to be shared and leaves the harness's own directory a real directory, so anything genuinely specific to one harness can live beside the link without being swept into the shared corpus.

Symlinks are not always available. Directory symlinks on Windows want Developer Mode or an elevated prompt, and on a corporate-managed machine that is a policy decision rather than yours. For the instructions file there is a route that needs no filesystem support at all: Claude Code expands @path imports inside CLAUDE.md at session start, as if the text were inline.

So AGENTS.md holds the content, and CLAUDE.md is one line:

@AGENTS.md

Measured, with a canary string in AGENTS.md and a prompt that forbids reading files:

CLAUDE.md containsModel printed
@AGENTS.mdHARNESS-BRIDGE-9137
no import lineNONE

The negative control is the half that matters. Without it the first row only shows that a model can find a token in a small directory; with it, the import is demonstrably the mechanism carrying the text.

Codex has the mirror-image escape hatch if you would rather keep CLAUDE.md as the real file: project_doc_fallback_filenames in ~/.codex/config.toml adds filenames to its project-level search, so it can be told to treat CLAUDE.md as an instructions file.

project_doc_fallback_filenames = ["CLAUDE.md"]

Either direction is one line. Pick the one whose real file you would rather open.

Prefer a skill to a slash command

A slash command is the one artifact with three different homes and no overlap between them: ~/.claude/commands, ~/.codex/prompts, ~/.config/opencode/command. Three locations, three copies, and the drift Tyler describes — "every pair I checked had already diverged" — is exactly what three copies produce.

Skills do not have that shape. SKILL.md is one format all three read, and two of the three read at least one directory in common with another. Codex has already drawn the same conclusion about its own prompts directory, in its documentation, verbatim:

Custom prompts are deprecated. Use skills for reusable instructions that Codex can invoke explicitly or implicitly.

So if a process is going to exist in more than one harness, write it as a skill, not as a slash command. A skill is still invocable by name; it is simply the form that has one home instead of three.

The shape that survives

For a process that belongs to a repository — the usual case, and the one with the least to maintain, because it travels with the clone and there is nothing to install:

.agents/skills/plan-start/SKILL.md      the one real file
.claude/skills/plan-start   →  ../../.agents/skills/plan-start
AGENTS.md                                the one real instructions file
CLAUDE.md                                one line: @AGENTS.md

Two real files, two pointers, nothing scheduled and nothing to run. Codex and opencode read the real files directly; Claude Code arrives through the pointers. Committed to git, the whole arrangement is reviewable in a diff, and a copy reintroduced by hand shows up as a regular file where a symlink used to be.

For a process that belongs to you rather than to a repository, invert it and no symlink is needed at all: keep the real files in ~/.claude/, which Claude Code and opencode both read natively, and give Codex the one pointer it needs.

What does not port, and what to do about it

An honest list, because the parts that do not port are the parts that cost you later.

Hooks and plugins are per-harness, and there is no bridge. They are each harness's own extension mechanism with its own shape. Anything load-bearing that lives in a hook exists in one harness only. The fleet hits this in its own way: its doctrine ships to the agent boxes as a Claude Code plugin, and that plugin is exactly what would not survive contact with a second harness — which is why the rules on the worker boxes are symlinks into a git repository instead. A file in a repo ports; a plugin does not.

MCP is the one tool layer all three speak, but it is not free. The server is written once and each harness still needs its own registration pointing at it. That is one copy of a connection string per harness rather than one copy of your logic, which is the trade you want, but it is not zero.

A tracker you cannot reach is a substrate difference, not a configuration problem. The rule this fleet runs on — nothing the tracker owns is ever copied to disk, so scope and status have exactly one home and cannot drift — holds only where the tracker is reachable. On a network where it is blocked, no amount of config makes "read scope live from the ticket" true.

The honest shape there is not a hidden cache that looks live. It is the fleet's own rule about anything read from a distance, which ports cleanly to this case: a cached value must carry its own age and be able to say it is stale. A cached ticket with a fresh-looking timestamp defeats every check you might put on it and is strictly worse than an empty one, because it fails silently and confidently. If you must hold ticket state offline, hold it somewhere that prints when it was fetched, and treat a stale read as a stop rather than as data.

The one line to take away

Count the copies, not the harnesses. Three harnesses reading one file is fine. One file copied twice is a divergence with a start date you will not be able to find.

On this page