Skip to main content

Meta-harness that runs Claude Code and Codex CLI with live trace sync.

Project description

tandem

A meta-harness that runs Claude Code and OpenAI Codex CLI as one paired session. Work in either agent and switch between them at any time without losing context: while one harness is active, the other is a shadow whose native session file is kept continuously up to date by translating the active harness's transcript entries into the shadow's own format. Switching is instant — the shadow was always resume-ready via that tool's native --resume/resume mechanism.

$ tandem                  # fresh paired claude+codex session; enters claude
$ tandem --active codex   # fresh session with codex active instead
tandem (claude)> switch   # after exiting claude: continue instantly in codex
tandem (codex)> exit
to continue this session: tandem resume a1b2c3d4e5f6
$ tandem resume           # continue the most recent session here
$ tandem resume <id>      # continue a specific session (id from the exit hint,
                          #   run from that session's own directory)
$ tandem run --on codex "second opinion: why is this test flaky?"
$ tandem status           # pairing, roles, sync position
$ tandem switch           # one-shot flip; does not enter a harness
$ tandem doctor [--live]  # verify both sessions are resumable
$ tandem sync             # manual catch-up translation (local file I/O only)
$ tandem sync-mcp         # opt-in: copy MCP server configs between the tools

Install

uv tool install tandem-cli   # or: pip install tandem-cli

Requires Python 3.11+ and the claude and codex CLIs on your PATH.

How it works

  • Per-command single-model invariant. Only one model is ever invoked per user command — the active harness's (or, for run --on, the target's). The shadow side is pure local file I/O: tandem tails the active transcript, translates each entry, and appends to the shadow's session file. The shadow's model is never called to "catch up".
  • A persistent prompt, not the OS shell. Leaving the harness lands you at tandem (claude)> instead of back in your shell. There, switch flips roles and drops you straight into the other tool (no re-launch needed), Enter or resume re-enters the current one, and status/sync/doctor/run --on …/sync-mcp run the same implementations as their one-shot forms, always against this session. exit (or Ctrl-D) returns to your shell and prints to continue this session: tandem resume <id> — pass that id to tandem resume <id> from the session's own directory. Every one-shot command also works from the OS shell, targeting the directory's most recently used session; one-shot tandem switch only flips, it does not enter a harness.
  • PTY passthrough. tandem launches the real CLI on a pty (raw mode, SIGWINCH resize forwarding, signals through the line discipline). Tandem never scrapes terminal output; the transcript files are the source of truth. Turn-complete hooks (claude --settings Stop hook, codex -c notify=[...]) are wired per-invocation as wake-up signals, with fs-watching (watchdog, polling fallback) as the data path and fallback. If the user's codex config already sets notify, tandem leaves it alone.
  • Append-only sync. Each new transcript entry is translated as it lands (no bulk re-export at switch time). Appends are whole-line + fsync, and a write-ahead intent in the sync cursor makes translation exactly-once across crashes. On restart, sync resumes from the last confirmed entry.
  • Tool calls are translated natively. The harnesses have different tool vocabularies (Read/Edit/Bash vs exec_command/apply_patch), so each completed call+result pair is re-expressed in the shadow's own vocabulary — Bashexec_command, Edit/Writeapply_patch, TodoWriteupdate_plan — and lands as a real tool-call record, so shadow history reads as the shadow's own work. Anything that would not map truthfully (and every unrecognized tool) passes through verbatim with its name and arguments intact. A call whose result never arrived is closed with a (tool result not recorded) placeholder when the source is handed off, since both replay APIs reject a dangling call.
  • Attribution. Every synced text message is tagged [via claude-code] / [via codex] (tandem's own notes use [tandem]), so interleaved histories stay legible to both the user and the models. Tool activity is untagged — it is mirrored as native records, not prose.
  • Error localization. An entry that fails translation becomes a placeholder in the shadow — [tandem: turn N could not be translated from <tool> — <reason>; raw entry quarantined at ~/.tandem/quarantine/...] — at most one per turn, and sync continues. The shadow is never corrupted or truncated.
  • Memory files. A fresh tandem launch and every switch sync CLAUDE.md ↔ AGENTS.md: shared content lives in a <!-- tandem:shared:begin/end --> block (newer file wins), tool-specific text outside the block is preserved, and a file without markers is read from but never rewritten. Git state is never touched.

Compatibility

Session formats are internal to the CLIs and drift between releases. Tandem pins what it was built against (observed formats documented in docs/formats.md):

CLI tested accepted range
Claude Code 2.1.220 ≥ 2.0, < 3
Codex CLI 0.145.0 ≥ 0.140, < 0.150

Outside the range tandem warns and asks you to run tandem doctor (--live performs a real one-word resume per harness — two small model calls). Format knowledge is isolated per tool in src/tandem/harness/claude_code.py and src/tandem/harness/codex.py.

Where things live

  • ~/.tandem/state.db — SQLite: session pairing + per-source sync cursors (override the directory with TANDEM_HOME)
  • ~/.tandem/quarantine/<session>/ — raw entries that failed translation
  • ~/.claude/projects/<munged-cwd>/<session-id>.jsonl — claude transcript
  • ~/.codex/sessions/YYYY/MM/DD/rollout-<ts>-<session-id>.jsonl — codex rollout (CLAUDE_CONFIG_DIR / CODEX_HOME honored)

Claude session ids are minted by tandem (claude --session-id); codex mints its own on first run and tandem captures it from the new rollout file.

Swapping in a different converter

The sync engine talks to a small adapter interface (tandem.converter.TraceConverter):

class TraceConverter(Protocol):
    def translate_entry(entry, direction, ctx) -> list[TargetEntry] | TranslationError

ReferenceConverter implements it via a normalized event model (tandem/events.py) derived from the observed formats. Pass your own converter to SyncEngine(store, session, source, converter=...).

Install & development

Requires Python 3.11+ and both CLIs on PATH. No network calls of tandem's own; all model calls happen inside the wrapped CLIs under your existing auth.

pipx install .        # or: uv tool install .
uv sync && uv run pytest   # development

Dependencies (deliberately small): click (CLI), pydantic v2 (event schema), watchdog (transcript tailing), pexpect/ptyprocess (PTY passthrough); state is stdlib sqlite3.

v1 non-goals

One active harness at a time (no concurrent dual-active). No other agents yet (the adapter interface is generic). No cloud sync or telemetry. Compaction state is not translated — on switch, the newly active tool's own compaction handles an over-long context.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

tandem_cli-0.1.0.tar.gz (146.1 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

tandem_cli-0.1.0-py3-none-any.whl (55.2 kB view details)

Uploaded Python 3

File details

Details for the file tandem_cli-0.1.0.tar.gz.

File metadata

  • Download URL: tandem_cli-0.1.0.tar.gz
  • Upload date:
  • Size: 146.1 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tandem_cli-0.1.0.tar.gz
Algorithm Hash digest
SHA256 a032405c29d5e41be4d220c70d9c3f1d762b0e161b881a0e9f364c94500aaf57
MD5 1e27d1553a61d3eec4f8a157ea32ea27
BLAKE2b-256 729e03781b0dc2261bd83de51e4401ebcc1628c7bfaae4195e43c31843469361

See more details on using hashes here.

File details

Details for the file tandem_cli-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: tandem_cli-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 55.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.14 {"installer":{"name":"uv","version":"0.11.14","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for tandem_cli-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 cf1a229e7afd2d8968d3697025adc5228a29309944b1bf2022fb18a4f98246ad
MD5 0527d75a316ed795953d33d2b27e7538
BLAKE2b-256 07859146e2bc3e750d5ee2a8904f80e274736970f10bfd0f1eb01bc5da87a307

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page