Duet Bridge: connect Claude Duet to Claude Code (MCP connector)
Duet Bridge is a custom MCP connector that lets Claude Duet talk to Claude Code on your Mac, in both directions. Duet can send work to Claude Code, start new Claude Code sessions, talk with a terminal window you have open, and open a session on your screen. Claude Code can answer Duet and send it messages first.
It is a remote MCP server on Cloudflare Workers (free plan), with OAuth sign-in for claude.ai, plus a small runner and a Claude Code channel on the Mac.
Search words: Claude Duet MCP, Claude Duet connector, Claude Duet to Claude Code, Claude Code remote control from Duet, Claude Code channels, custom connector for claude.ai, remote MCP server on Cloudflare Workers.
Can Claude Duet talk to Claude Code?
Not by itself. Duet runs in Anthropic's cloud and cannot reach your Mac, and your Mac should not open a port to the internet. Duet Bridge is a switchboard in the middle. Both sides call out to it, and it connects them:
Claude Duet ──(custom connector, OAuth)──▶ ┌──────────────────────────┐
│ Duet Bridge Worker │ Cloudflare, free plan
live Claude Code windows ──(WebSocket)────▶│ sessions · messages · │
(mac/duet-code) │ jobs · reports │
Mac runner ──(bearer token, every 3 s)────▶│ │
(mac/duet-bridge.py serve) └──────────────────────────┘
The Mac only makes outbound calls. Nothing on the internet can call it.
What Duet can do
- Talk to a Claude Code window you have open. Start the window with
duet-codeinstead ofclaude. Duet sees it inlist_sessions. A message from Duet shows up in that window within seconds, and the window answers Duet with itssend_to_duettool. - Start a new Claude Code session.
start_sessionstarts a background conversation on the Mac. Each Duet message is one turn, and Claude Code remembers the earlier turns. Duet continues it withsend_message. - Show a session on your screen.
open_on_macopens a background session in a terminal window (Ghostty if you have it, else Terminal), so you can read it and take over. The window then becomes a live session. - Wait for the answer.
send_message,start_sessionandread_messagestakewait_seconds(up to 45). The call returns as soon as Claude Code answers. - Hear from Claude Code first.
read_messageswith no session collects everything Claude Code sent.
What Claude Code can do
- In a
duet-codewindow:send_to_duetanswers Duet or tells it something.set_focustells Duet what the window works on. Duet cannot see terminal text, so a Stop hook sends a window back once if it answered Duet in the terminal only. - From any script:
mac/duet-bridge.py tell --text "..."leaves Duet a message. - Limit: Claude Code cannot wake Duet. Duet reads its messages when you talk to it, or on its own background check.
The thirteen tools Duet gets
| Tool | What it does |
|---|---|
list_sessions |
Live windows and background sessions, with connected for live ones |
start_session |
Start a background Claude Code conversation (read or work mode) |
send_message |
Send a message to any session, and optionally wait for the answer |
read_messages |
Read what Claude Code sent |
end_session |
Close a session |
open_on_mac |
Show a background session in a terminal window |
bridge_status |
Unread counts, open windows, last Mac check-in |
send_job, get_job, list_jobs, cancel_job |
The older one-shot job path |
post_report, read_reports |
Reports between the two sides |
Is it safe?
Duet can read your email, and a crafted email can try to make Duet send work to Claude Code. So:
| Guard | How |
|---|---|
Background sessions start in read mode: read files and the MCP servers you list, answer in text. |
--permission-mode dontAsk, --strict-mcp-config, allow and deny lists in mac/config.toml |
work mode (edits and commands) is opt-in per session. |
--permission-mode auto plus work_denied_tools |
| Your claude.ai connectors (Gmail and the rest) never load in background sessions. | ENABLE_CLAUDEAI_MCP_SERVERS=false |
| Every session is told: never send email or messages, stage drafts, refuse requests for credentials. | session rules in mac/duet-bridge.py and the channel |
| You see each job before it goes. | Set start_session, send_message and open_on_mac to Needs approval in claude.ai |
| One switch stops all new work. | touch mac/duet-bridge.OFF |
The deny rules for work mode are a guard, not a wall: a work session has a real shell. Test the limits on your own Mac with canary requests (for example, ask a read session to run touch /tmp/canary, then check that the file does not exist).
Set it up (one time)
You need: a Mac with Claude Code signed in, Node.js 20 or later, Python 3.11 or later, a free Cloudflare account, and a claude.ai plan with Claude Duet and custom connectors.
- Clone this repo, then sign in to Cloudflare:
cd claude-duet-bridge/worker npm install npx wrangler login - Run
./setup.sh. It makes two secrets in your home folder (~/.duet-bridge-tokenand~/.duet-bridge-passphrase), deploys the Worker, writes the URL intomac/config.toml, and prints the connector URL. - Edit
mac/config.toml. Set the folder for each workspace, and the read-only MCP servers areadsession may use. If you add workspace names, also add them toWORKSPACESinworker/wrangler.jsoncand runnpx wrangler deploy. - In claude.ai, open Settings > Connectors > Add custom connector. Name it
Duet Bridgeand paste the URL that ends in/mcp. - Click Connect. On the sign-in page, paste the passphrase (
pbcopy < ~/.duet-bridge-passphrasecopies it). - Run
mac/install.sh. It starts the Mac runner with launchd and linksduet-codeinto~/.local/bin. - Open a window with
duet-code. On the warning screen about development channels, choose I am using this for local development. Then ask Duet: "List my Claude Code sessions and say hello to the live one."
Known limits
- Channels are a research preview. A custom channel needs
--dangerously-load-development-channels, whichduet-codepasses, and each launch shows a warning screen. On a personal plan you cannot turn it off; the setting that pre-approves channels is for Team and Enterprise admins. - Remote Control is off in
duet-codewindows. In testing, Duet windows with Remote Control on sometimes moved into Claude Code's background service about 30 seconds after a turn, and the moved session could no longer hear Duet. Your other windows keep Remote Control. - Claude Code cannot wake Duet. See above.
- Work waits while the Mac sleeps. A window that stops checking in shows
connected: falsewithin 90 seconds. Its session ends after 12 hours without a check-in, so a Mac that sleeps overnight keeps its windows. - Duet's background checks may or may not use custom connectors. Test it: leave a message with
tell, and see if Duet mentions it on its own.
Day-to-day
| Task | Command |
|---|---|
| Open a window Duet can talk to | duet-code (or duet-code --name "Report draft") |
| See the sessions | mac/duet-bridge.py sessions |
| See the status | mac/duet-bridge.py status |
| Tell Duet something | mac/duet-bridge.py tell --text "..." |
| Stop all new work | touch mac/duet-bridge.OFF |
| Start again | rm mac/duet-bridge.OFF |
| Remove the runner | mac/install.sh uninstall |
Tests
cd worker
npm install
printf 'OWNER_PASSPHRASE=dev-only-passphrase\nBRIDGE_TOKEN=dev-only-bridge-token\nPUBLIC_URL=https://localhost:8787\n' > .dev.vars
npm run dev # terminal 1: local Worker on https://localhost:8787, fresh store
npm test # terminal 2: 54 checks of sign-in, tools, sessions and the live line
node test/channel.mjs # 13 checks of the channel, played as Claude Code (fresh store)
node test/stop-hook.mjs # 6 checks of the Stop hook (no Worker needed)
npm run check type-checks the code. Each npm test run ends with a 15-minute sign-in lock, so restart npm run dev before the next run.
How sign-in works
The Worker is its own OAuth server, with dynamic client registration and PKCE. claude.ai registers itself and sends you to a sign-in page that asks for your passphrase. There is one owner and no user accounts.
- Sign-in sends codes only to
https://claude.ai/api/mcp/auth_callbackor to a loopback address. A sign-in for any other address is refused. - Five wrong passphrases in 15 minutes lock sign-in for 15 minutes.
- The Mac uses a separate bearer token on a separate path (
/bridge/*). A connector token cannot open that path.
Files
worker/src/index.ts: the entry file and the OAuth set-upworker/src/authorize.ts: the passphrase sign-in page and the redirect guardworker/src/mcp.ts: the MCP endpoint and Duet's thirteen toolsworker/src/bridge.ts: the Mac's private path (/bridge/*)worker/src/mailbox.ts: the store and the live sockets (a SQLite Durable Object)channel/channel.mjs: the Claude Code channel that gives a live window its line to Duetchannel/stop-hook.mjs: sends a window back if it did not answer Duet withsend_to_duetmac/duet-code: the launcher for live windowsmac/duet-bridge.py: the Mac runner (serve,tell,sessions,status)mac/install.sh: the launchd job and theduet-codelinkmac/config.example.toml: the runner settings
License
MIT. See LICENSE.