tandem
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@tandemlaunch a Claude Code session and ask it to fix the failing tests"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
tandem
Each Codex session gets its own dedicated Claude Code session, running in the cmux terminal app. Codex launches that session and drives it directly through a small MCP server (the bridge) over the cmux socket.
Origin: extracted from sporedrive.
Architecture
Codex session --MCP stdio--> tandem (bridge/cmux_bridge) --cmux socket--> Claude Code in a cmux surfaceThe bridge wraps the cmux CLI through argument arrays only: no shell, no generic RPC, no pane deletion, no process cleanup.
Sessions are identified by UUID (workspace, surface, Claude session id, pid), never by title or focus.
Delivery is accepted only when the submitted text is found in Claude's own transcript (transcript-hash acceptance). Editor staging, a send "OK" or a hook event alone prove nothing. An unconfirmed submit is
uncertainand is reconciled under the samerequest_id, never resent blindly.skill/cmux-driver/is the Codex skill that holds the driver protocol;skill/cmux-driver/scripts/executor_session.pylaunches and verifies the dedicated Claude session. From any worktree, call it through the install symlink:python3 "${CODEX_HOME:-$HOME/.codex}/skills/cmux-driver/scripts/executor_session.py".
Related MCP server: Rail Connector MCP
Tools
Tool | Purpose |
| Diagnose cmux socket access; list Claude Code sessions visible in cmux |
| Bind a controller to one session (writer or monitor), take a lease, run the model gate |
| Screen state, context use and events after a sequence number |
| Deliver a message; keyed by |
| Wait for accepted / turn_complete / idle (default 25 s; loop on non-terminal results) |
| Compact the session with a checkpoint |
| Pause a binding (owner only, reason required) |
| Resume a paused binding (owner only) |
| Drop the leases; never closes the surface or process |
Identity and one writer
One Claude per Codex session, enforced: the Codex session's identity is its controller_id, stable for that Codex session. A new writer bind by the same controller_id supersedes its other live writer bindings (they are released, leases dropped; their ids come back as superseded_bindings). Two Codex sessions must never share a controller_id. A worktree has one writer at a time, enforced by leases; a writer bind by a different controller is refused with lease_conflict. Every binding tool (bridge_observe, bridge_submit, bridge_wait, bridge_compact, bridge_pause, bridge_resume, bridge_release) takes controller_id and refuses not_owner unless it matches the one the binding was bound with.
Choose controller_id once per Codex session: the Codex thread/session id if the environment exposes one, else a uuid generated once and kept in your notes. Reuse it on every bridge call. It must be a non-empty, non-whitespace string of at most 200 chars (bad_request otherwise). Worktree identity is the case-insensitive-canonical on-disk path (macOS F_GETPATH), so differently-cased spellings of one directory share one writer lease.
Retention of prompt text (PHI minimisation). While a request is in flight its record under requests/<binding_id>/<request_id>.json holds the delivered text (inline) and a 120-char first_line; a multi-line/long payload also lives in an 0444 file tasks/<binding_id>/<request_id>.txt. Task files are written only after every refusal gate passes (a refused submit writes nothing). Once a request is terminal (accepted, completed, uncertain_foreign) its record keeps only text_sha256/delivered_text_sha256 plus length/line counts (inline delivered_text and first_line are nulled; a task-file record keeps just the Task brief: <path> reference). bridge_release deletes tasks/<binding_id>/ unless a non-terminal request is still in flight on the binding. The in-memory CLI call log keeps only the last 500 calls and redacts send text as <redacted N chars>. Receipts under receipts/ never contain prompt text. Task files of a never-released binding remain until it is released.
Leases
Default TTL 1800 s (lease_ttl_s on bridge_bind, clamped 60..86400). No tool renews a lease. When it lapses, calls refuse with lease_expired; fix it by rebinding the SAME Claude session with the SAME controller_id (do not launch a new one). Distinguish lease_conflict (another controller holds the writer lease at bind time) and lease_lost (this binding was superseded or its lease no longer held).
Pause
bridge_pause(binding_id, controller_id, reason) marks the binding paused. While paused, bridge_submit and bridge_compact refuse with paused; bridge_observe and bridge_wait keep working and observe reports paused. Immediately before every Enter (submit, reconcile of staged text, compact) the bridge re-checks the writer lease and the pause flag under one state-lock hold. A pause landing after staging raises paused, marks the request uncertain, and leaves the payload staged with Enter NOT pressed. For submit: bridge_resume, then call bridge_submit again with the SAME request_id to reconcile. For compact: bridge_resume, clear the staged /compact from the prompt box, call bridge_compact with a NEW request_id (compact is not re-Entered). A rebind on the same surface carries the pause to the new binding, even after release or lease expiry. bridge_release does not clear a pause; it makes the binding unusable. bridge_resume clears it. Residual race: the check and the keystroke are not atomic, so a pause or release landing in that last gap is not caught.
Model gate
Implementation sessions must run on an explicitly chosen, verified model. Configure through the environment of the bridge (and of executor_session.py):
Variable | Meaning | Default |
| Comma-separated families ( |
|
|
| off |
A malformed value fails closed with model_policy_config. Other refusals: model_policy / planning_session (planning-only family), model_required (planning purpose without a model), model_unknown, model_mismatch, model_unverified, model_changed. Full list: skill/cmux-driver/references/bridge-mcp.md.
executor_session.py launch defaults to --focus false, but cmux creates an unfocused terminal with no view (its command would not start), so launch selects the new workspace briefly (~1s) to create the view, then restores your previous selection unless you already switched elsewhere. The receipt records view_realized and focus_restored; if no view appears, launch exits 4 with error_code: no_terminal_view and types nothing. The bridge refuses a viewless surface with no_terminal_view (bind/submit) or reports it per observe; it is not a transport fault and never triggers a resend.
Prompt suggestions. After each turn Claude Code renders a predicted next prompt as dim ghost text in the input editor; plain-text read-screen cannot tell it from typed text, so the screen would classify as staged and submit would refuse busy. executor_session.py launch therefore passes --env CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false to new-workspace (the receipt records prompt_suggestions: "disabled"; the command and argv, hence the pid identity checks, are unchanged). bridge_bind reads that one variable from the Claude process environment (nothing else is kept) and records prompt_suggestions: "disabled"|"enabled_or_unknown" on the binding. Binding a hand-started session requires suggestions off: start it with CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION=false claude ... or set promptSuggestionEnabled: false in settings (a settings-only opt-out is not visible to the bridge, so the binding stays enabled_or_unknown and the hint remains). When a binding is not disabled and the editor shows staged with no request of ours in flight, observe and the submit busy refusal carry an instruction naming this. The bridge never clears the editor.
Other environment variables: CMUX_BRIDGE_STATE_DIR, CMUX_BRIDGE_SOCKET, CMUX_BRIDGE_CLI, CMUX_BRIDGE_PASSWORD_FILE, CMUX_BRIDGE_CLAUDE_PROJECTS.
Permissions: bypass by default
executor_session.py launch starts Claude with --dangerously-skip-permissions by default. The executor can run any command and edit any file the user can, without prompting. Launch only in worktrees you are willing to have modified, and pass --permission-mode <mode> to use a stricter posture instead.
State directory is sensitive
State lives in ~/.local/state/tandem (override CMUX_BRIDGE_STATE_DIR), mode 0700: leases, bindings, requests, receipts, locks, tasks, plus launch receipts. Request records contain the full text sent to Claude. Treat it like a transcript; do not commit or share it.
Install
Prerequisites: macOS, cmux, the claude and codex CLIs, uv, Python 3.11+.
./install.shlinksskill/cmux-driverinto${CODEX_HOME:-~/.codex}/skills/and prints thecodex mcp add tandem ...command. Run that command yourself../install.sh --uninstallremoves the symlink.Make sure the cmux socket is reachable (see docs/installation.md).
Restart Codex;
codex mcp listshould showtandem.
Details: docs/installation.md, docs/operations.md.
Testing
Offline simulation suite (no cmux needed):
uv run --python 3.13 --with pytest python -m pytest bridge/tests -q -p no:cacheproviderLive acceptance (
bridge/run_live.sh) drives a real cmux session. Operator-run only, never CI; it requires a clean worktree and a passing offline suite first. Its default interpreter isuv run --quiet --project "$REPO_ROOT" --extra test python; override withPY.
Optional AGENTS.md snippet for Codex
## Driving a Claude Code session
- To supervise or delegate to Claude, use the `cmux-driver` skill; Claude is the sole writer in that worktree.
- Open the executor with `python3 "${CODEX_HOME:-$HOME/.codex}/skills/cmux-driver/scripts/executor_session.py" launch --cwd <worktree>` and an explicit `--model`; reuse a session only after `verify` passes.
- Bind by exact UUIDs, with a stable `controller_id` for this Codex session (never shared with another session).
- Prove delivery by transcript acceptance; keep an `uncertain` request under the same `request_id` and reconcile, never resend.
- Never send competing tasks or compact during conflicting work.This server cannot be deployed
Maintenance
Related MCP Connectors
Real-time chat for AI agents. Claude Code, Cursor, Cline and Codex join channels over MCP.
A paid remote MCP for OpenAI Codex agent coordination MCP, built to return verdicts, receipts, usage
Hosted MCP memory and agent control plane for durable conversations, jobs, and operations.
Hosted MCP messaging across owners, tools, and machines, with readable transcripts.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceEnables MCP clients to spawn and control Codex CLI and Claude Code sessions on the host machine, with session management and filesystem access.4MIT
- AlicenseAqualityBmaintenanceLocal MCP bridge that lets Codex operate local Claude Code sessions, including listing, starting, resuming, forking, prompting, and stopping conversations via the Remote Control CLI.14MIT
- AlicenseNot gradedqualityBmaintenanceEnables a local MCP client such as Codex or another Claude Code session to join and message already-running, allowlisted Claude Code sessions on the same Mac, using a local stdio server and daemon with kernel-verified peer identity and an append-only event ledger.Apache 2.0
- AlicenseNot gradedqualityCmaintenanceEnables local discovery, reading, and messaging between running Codex and Claude Code sessions through MCP.117 npmMIT