Claude Code and Codex each hand a task to DeepSeek Harness, and its subagents build this rainy-night street in 1 h 41 min (40 s, sound on):
https://github.com/user-attachments/assets/72e2fdff-275e-48bc-8e5a-39ffb025a0a8
See it run
One Codex chat starts a task, queues a follow-up, steers the running turn, and waits for all three: Codex and Claude take turns in the same Harness session, and every message is labelled with who sent it:
Quick start
Requires DeepSeek Harness ≥ 0.2.1-rc.2 (signed in) and Claude Code or Codex. Nothing else: no Node.js, no terminal. The steps are the same on macOS and Windows.
1. Add the plugin in Harness: Plugins (插件) → Add plugin (添加插件) → type dsh-baton-host → Install (安装) → Enable now (立即启用).
Switched on, the plugin registers dsh-baton for Claude Code and Codex, for every folder, and Harness says so.
2. Codex only, once: Codex Settings → Hooks → Trust dsh-baton: waiting for the Harness run. Codex is then woken when a run ends instead of checking every 270 s. You trust it once: updates and Harness restarts do not ask again. Skip it and Codex waits with dsh_wait as before; the first hand-over reminds you.
3. Check: in a new Claude Code or Codex conversation, ask it to call dsh_list_workspaces. It lists your Harness workspaces.
That is all. If Harness says it could not register, run npx -y dsh-baton-mcp setup in a terminal (this needs Node.js ≥ 22.15); it does the same registration. Updating, removing and Windows details are further down.
How it works
How each client waits (nothing to configure):
- Claude Code waits for a run in the background (the
wait_commandthatdsh_sendreturns) and is woken once, when the run ends. - Codex with the dsh-baton Stop hook trusted (from 0.6.1): after
dsh_sendit ends its turn; the hook waits for the run outside the model and, when the run ends or asks something, continues the same conversation with the result. One model request per run instead of one every 270 s. - Codex without the hook waits inside
dsh_wait, up to 270 s per call, just under its 300 s tool timeout. An unfinished wait returns only the state, so Codex calls again without summarising. Codex gets nowait_command: its terminal cannot wake it, so it would poll. - Codex asks you to approve MCP tool calls: approve the
dsh-batontools. For unattendedcodex execruns, add--approve-for-me.
Pick a model (from 0.6.1): just say which one, for example "hand it to DeepSeek, V4 Pro". dsh_send passes it as model (and reasoning_effort), matched against the models Harness lists, so a model Harness adds later works as soon as it shows up. It applies to that session only; Harness's default model stays as it was. dsh_list_models shows what is available.
How a hand-over is checked (from 0.6.0)
You don't have to take DeepSeek's word for it. Before the run, dsh-baton notes what is in the folder. When the run ends it shows exactly which files were created, changed or deleted, and runs your project's own tests itself (npm test, or whatever the client passes as checks). A second, read-only DeepSeek agent then goes through the request point by point and ends with a table: requirement, met or not, and where (file:line). Claude or GPT reads just that, sends anything missing back in one message, and opens code only where a row says "not met". Unless cwd names another folder, the new Harness session starts in the folder you're already working in.
Token usage
In short: hand over the big, long jobs. A feature that spans many files, a large refactor, a whole small app: that is where it saves the most. Claude Code's own bill drops by 65–81%, and Codex gets 2 to 5 times as many tasks out of a ChatGPT Plus 5-hour window (5 times on a medium task with GPT-6 Astra).
The honest part: for small tasks, Codex on GPT-6.1 Sol is cheap enough that doing it alone costs less in total than handing it over, and at medium size it comes out about even. The hand-over pays off when you want Claude or GPT to plan and review, a cheaper model to do the typing, and you'd rather not keep hitting the 5-hour limit.
Who this is for: most of us pay for Claude or ChatGPT with a plan (Plus, Pro, Max 5x…), while DeepSeek is pay-as-you-go from a balance you top up, so every hand-over is money spent outside your plan. If your plan's limits already cover how you work, you don't need this: keep it simple and keep the money. If you're like me and keep running long, messy jobs with round after round of changes, and keep hitting the limit, a little DeepSeek spend gets you a lot more out of the plan you already pay for. Cost per task at list price, DeepSeek V4.1 Flash 1017 at its peak price:
- Two tasks, asked the way a user would. The small one takes Claude Code or Codex about 7–8 minutes on its own; the medium one is a small app with a server and a page. Long jobs beyond that were not measured. The requests are in Chinese, linked as written:
- Small: a command-line task manager in Node, no dependencies, split into modules, with tests. Request · acceptance check
- Medium: a task manager web app, Node server plus page, REST API, stats, tests and a README. Request · acceptance check
- The only difference in the hand-over runs is one line added at the end: 交给 DeepSeek Harness 去做。 ("Hand it to DeepSeek Harness.")
- Same model and effort in both arms, three runs per cell (Astra: one run each, two for the small hand-over), and every run judged by the same acceptance check, which the model never sees. All 12 hand-over runs passed it the first time.
- Prices are list prices. DeepSeek is counted at its peak price, the most conservative case; the real DeepSeek bill came to about 58% of that.
- Every run, the raw data and the scripts: run log and full report.
Tools
CLI
dsh-baton workspaces
dsh-baton new-workspace ~/code/new-app --title "New App"
dsh-baton sessions --cwd ~/code/new-app
dsh-baton send --cwd ~/code/new-app "Scaffold the project"
dsh-baton send --session session-xxxx "Next: add tests"
dsh-baton send --session session-xxxx --mode steer "Interjection: don't change the API"
dsh-baton wait hrun-xxxx # blocks, prints the final reply; exit 0 on success
dsh-baton status hrun-xxxx
dsh-baton activity session-xxxx # running turn, sub-agents, background jobs
dsh-baton cancel --session session-xxxx
dsh-baton rename session-xxxx "New title"
dsh-baton rename-workspace ~/code/new-app "New name"
Configuration
When the plugin is unreachable, the MCP server falls back to Harness's dsh headless command. It can only continue sessions it created itself (headless does not load agent presets such as standard), and new sessions do not appear in the desktop sidebar. By default patches/account.patch.yml reuses the desktop app's DeepSeek account sign-in, so no API key is needed.
Install details
What the plugin does when switched on. It copies the MCP server to ~/.dsh-baton/app, a folder that stays put, and writes the MCP entry into ~/.claude.json and ~/.codex/config.toml directly, keeping a *.bak-dsh-baton backup of each. It starts the server with your own Node.js ≥ 22.15 when it finds one on PATH; without one, with Harness's own runtime (the Harness app in Node mode, ELECTRON_RUN_AS_NODE=1), so nothing else has to be installed. Either way it works with only the desktop apps installed, when claude / codex are not on your PATH. It runs again on each Harness start: an entry that is already right, or a working one that starts another copy of dsh-baton, is left alone. In Harness, the source switches to the npm registry ("Official npm registry", 「npm 官方源」) by itself once you type the package name, and no restart is needed. To turn the registration off, set DSH_BATON_AUTO_REGISTER=0 for Harness.
The Codex Stop hook. The plugin adds one [[hooks.Stop]] block, marked # dsh-baton, to ~/.codex/config.toml; Codex shows it as dsh-baton: waiting for the Harness run and runs it only once you trust it (dsh-baton never trusts it for you). The block only names a small script, ~/.dsh-baton/bin/codex-stop-hook (codex-stop-hook.cmd on Windows), and everything that can move, Node.js and the server, lives in that script. So the block itself never changes: updates, same-version refreshes and Harness restarts leave it as it is, and your trust stays. If the block is ever rewritten, dsh-baton drops the old trust record, and Codex asks you to trust it again. Windows uses a .cmd because Codex runs hooks through PowerShell -Command, which cannot start a quoted program path. Not added when the server runs on Harness's own runtime. dsh-baton setup --codex-hook adds it by hand, setup --remove takes it out, DSH_BATON_CODEX_HOOK=0 for Harness stops the plugin adding it.
Updates need no restart. When a newer version lands in ~/.dsh-baton/app (a plugin update, setup, or a copy by hand), or the same version with changed files, conversations already open in Claude Code and Codex use it from their next dsh_* call: the server checks the folder before each call, starts the new version, and says once in that call's result that it was updated. A dsh_wait that is already running finishes on the old version first. If the new version cannot start, the old one keeps working and the result says why. A tool added in the new version shows up in Claude Code at once (the server tells the client its tool list changed); Codex keeps the tool list a conversation started with, so it does not show a new tool until the next conversation; from 0.5.9 on it can still use one at once through dsh_call, which the update note points to, and the tools it already has run the new code right away. This works from the version after 0.5.8 on; a server started from 0.5.8 or earlier needs one last restart (or /mcp reconnect) to pick it up.
npx -y dsh-baton-mcp setup does the same registration from a terminal: for a plugin that could not register, or when you come from an MCP directory. With the plugin already registered it changes nothing, then points you to the plugin if it is missing.
Updating. Harness has an update button only for its official plugins, so: Plugins → dsh-baton-host → Uninstall, then Add plugin → dsh-baton-host. For a while after a release, Harness installs the previous version instead (pnpm's minimumReleaseAge holds back versions published very recently, and the install dialog says so); to get the new one at once, type the version: [email protected]. A pre-release must always be added by its exact version, such as [email protected]: Harness resolves @next to a broken placeholder. Plain dsh-baton-host gets the latest release. Switched on again, the plugin brings the MCP server to the same version; new conversations use it. If the two are ever out of step, the first tool call that notices it says so, naming the older side and the newer side's exact version to install.
Removing. Harness → Plugins → dsh-baton-host → Uninstall, then npx -y dsh-baton-mcp setup --remove. The plugin cannot tell an uninstall from Harness quitting, so it leaves the MCP entries to that command, which also deletes ~/.dsh-baton/app; run records stay in ~/.dsh-baton until you delete it.
Upgrading from dsh-bridge. In Harness, uninstall dsh-bridge-host, then add dsh-baton-host; switched on, it replaces the old dsh-bridge MCP entries. DSH_BRIDGE_* variables and an existing ~/.dsh-bridge folder keep working for now. Tools get a new prefix (mcp__dsh-baton__…), so update any permission allowlists. A plugin added before 0.4 by pasting ~/.dsh-baton/app/host still works but does not update with npm: uninstall it and add dsh-baton-host by name.
From source (to follow main or change the code): git clone https://github.com/JialaoLiu/dsh-baton.git, then in Harness add the plugin from the clone's host folder (Add plugin → paste the path). Switched on, it registers the clone itself, so changes run before they are published. Update with git pull. node dsh-baton/src/cli.mjs setup registers the clone from a terminal.
Windows. Same steps; WINDOWS.md covers what differs (a named pipe instead of a socket file) and fixes for a Plugins page that cannot install or uninstall.
The plugin and setup write the same entries these commands would (with the absolute path of node, since desktop apps start MCP servers without your shell's PATH):
claude mcp add dsh-baton -s user -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
codex mcp add dsh-baton -- "$(command -v node)" "$PWD/dsh-baton/src/server.mjs"
-s user matters: without it, Claude Code registers the server for the current project folder only.
Claude desktop app (optional): for the chat app's connectors (separate from Claude Code), add this to ~/Library/Application Support/Claude/claude_desktop_config.json and restart the app. It shows as a "Local dev" connector.
{ "mcpServers": { "dsh-baton": { "command": "/absolute/path/to/node", "args": ["/absolute/path/to/dsh-baton/src/server.mjs"] } } }
Security and limits
Any process running as your OS user can drive Harness through the socket, the same as running dsh directly. dsh-baton relies on Harness 0.2.1-series internal service APIs, so a major Harness update may need a follow-up release.
License
MIT