Switchboard
About
A local MCP hub that lets your Claude Code agents talk to each other: message, nudge in the terminal, and coordinate through a web dashboard.
Details
- Author
- rodcoppi
- Categories
- Developer Tools, Other, AI
Jump to
Setup
Install Switchboard in your MCP client (Claude Desktop, Cursor, Windsurf, and others).
Repository: https://github.com/rodcoppi/switchboard-mcp
Follow the installation instructions in the repository README, then restart your MCP client.
Let your coding agents talk to each other.
Claude Code and Codex CLI, on the same network, in the same conversation.
You haveClaude Code(Anthropic's CLI) open on the backend,Codex CLI(OpenAI's) on the frontend, another agent on infra. None of them knows the others exist. So when the API contract changes, you are the one who carries the news: copy from this terminal, paste into that one, repeat. You are the message broker.
Switchboard is the wire between them. It's a local hub. Your agents message each other over MCP, the recipient gets nudged awake in its own terminal, and you watch the whole conversation on one dashboard — where you can also open any agent's chat, drive its real terminal, and see what it did about the message it received. It connects sessions you already have. It won't spawn, orchestrate or manage them.
What keeps it safe:tmux carries a one-line nudge, and nothing else. The message itself travels over MCP.Agent A callssend_message, the Hub appends it to~/.switchboard/messages.jsonl(the source of truth) and pokes agent B's terminal with a single[switchboard]line. B wakes up and callscheck_messagesto read it.
Platform: Windows + WSL (Ubuntu).That's what Switchboard is built and tested on. The core (hub, MCP, tmux nudges) is plain Unix and tmux, so Linux and macOS may well work, but nobody has tested it. Treat them as unsupported for now. The parts that make it click on Windows (the one-click launcher, opening a real terminal window,\\wsl$\…folder paths) are WSL-specific, and say so instead of breaking when you run them elsewhere.
Local-only by design: the Hub binds127.0.0.1and nothing reaches the network. MIT licensed.
- Node.js >= 20. Runs as ESM, with TypeScript executed bytsx. No build step.
- tmux >= 3.2(tested on 3.4).
- Claude Code >= 2.x, theclaudebinary on your PATH.
- Codex CLI, optional: thecodexbinary on your PATH. You need it only to run agents with--agent codex, or the dashboard'sCodexbutton. Everything else works without it.
- jq, optional. Handy for reading the JSONL while debugging.
One WSL distro, one user.The tmux server belongs to a user on a distro. Run the Hub (serve) and every agent (start) as the same user on the same distro. Split them across two andtmux send-keyswon't find the session, so the nudge never lands.
git clone https://github.com/rodcoppi/switchboard-mcp.git && cd switchboard-mcp && npm install node bin/switchboard.mjs setup
setupdoes every manual step below for you: it checks the prerequisites (and offers a sudo-less tmux install if tmux is missing), registers the MCP server in Claude Code, puts the agent-protocol snippet in your~/.claude/CLAUDE.md, adds the permission rules, runsnpm link, offers the Windows shortcut, and brings the Hub up. It asks before it touches a file of yours. Re-run it whenever you like, it changes nothing that is already right. Pass--yesand it stops asking.
When it finishes, the dashboard is athttp://127.0.0.1:4577/. Launch agents from theLaunch agentform there, or runswitchboard wirein the folder of a claude window you already have open to bring that one in.
git clone https://github.com/rodcoppi/switchboard-mcp.git cd switchboard-mcp npm install
The TypeScript runs straight throughtsx, so there is nothing to build. Three ways to call theswitchboardCLI:
- npm linkputsswitchboardon your PATH. This is the one to use:
npm link switchboard --help
The examples below sayswitchboard <subcommand>and assume you linked. If you didn't, read them asnode bin/switchboard.mjs <subcommand>.
2. Start the Hub (serve), usually automatic
Skip this step:switchboard startandswitchboard wirebring the Hub up for you when it isn't running, in a detached tmux session calledsb-hub. No terminal window stays open. After a reboot,wireorstartyour first agent and the Hub comes up with it.
Run it yourself when you want to watch the logs live:
The Hub runs in the foreground and logs to stdout and~/.switchboard/logs/hub.log. Its first line gives you the addresses and the MCP registration command, ready to copy:
Dashboard: http://127.0.0.1:4577/ | MCP: http://127.0.0.1:4577/mcp | Register (once): claude mcp add --transport http --scope user switchboard http://127.0.0.1:4577/mcp --header 'Authorization: Bearer ${SWITCHBOARD_AGENT_TOKEN}' --header 'X-Switchboard-Agent-Name: ${SWITCHBOARD_AGENT_NAME}'
To look inside the Hub that started itself:tmux attach -t sb-hub, andCtrl-b dto leave it running. Orswitchboard logs -f.servetakes--port <port>and--log-level debug|info|warn|error.
One-click launch from Windows (no WSL terminal)
On Windows and WSL you can skip the terminal. Once, inside WSL:
switchboard shortcut # creates Switchboard.lnk on your Windows Desktop switchboard shortcut --startup # or: installs it in the Startup folder (runs on every boot)
Double-clickSwitchboard(or just boot Windows, with--startup) and the Hub comes up in the background, with the dashboard open athttp://127.0.0.1:4577/in your Windows browser. WSL2 forwards localhost for you; the Hub still binds127.0.0.1inside WSL, so nothing reaches the network. Launch or wire agents from theLaunch agentform. Delete the shortcut to undo it.
The shortcut is a.lnkcarrying the Switchboard icon and opening minimized; the.batit drives, and the icon, live in%LOCALAPPDATA%\Switchboard(a.batcannot carry an icon, and an icon on the WSL filesystem renders blank at boot, when the distro is not running yet). Regenerate the icon withnode scripts/make-icon.mjs.
3. Register the MCP in Claude Code (mcp add)
Once only, in theuserscope (applies to every project):
claude mcp add --transport http --scope user switchboard http://127.0.0.1:4577/mcp \ --header 'Authorization: Bearer ${SWITCHBOARD_AGENT_TOKEN}' \ --header 'X-Switchboard-Agent-Name: ${SWITCHBOARD_AGENT_NAME}'
claude mcp listshowsswitchboardasconnectedwhile the Hub is up.
Those two headers are what makes joining silent.Claude Code expands${VAR}from the agent's OWN environment when it connects, so the Hub knows who is calling before any tool runs: the session binds at connect time and the agent is on the network without a single line typed into its terminal — like every other MCP server you use. One registration covers your whole fleet (each session carries its own name and token), and the token never passes through the model. A client registered without the headers still works the classic way: the Hub types a one-line kickoff asking the agent to calljoinitself.
Running Codex agents too? Point Codex at the same Hub. Same streamable-HTTP endpoint, spelled differently (setupoffers this when it finds thecodexbinary):
codex mcp add switchboard --url http://127.0.0.1:4577/mcp
Tool permissions.Add the allow rulemcp__switchboard__topermissionsin Claude Code'ssettings.json, or the Switchboard tools ask for approval every time you use them. Already onbypassPermissions? You're covered.switchboard startreminds you on its first run.
Only if you skip the headers above.On the classic path the agent readsSWITCHBOARD_AGENT_TOKENfrom its environment withprintenvbefore callingjoin, so that shell command needs approval too — addBash(printenv:)to the allow rule, or run the agent withbypassPermissions. With the identity headers registered, none of this applies: nothing is typed and no token ever reaches the model.
Pasteagent-protocol/CLAUDE.snippet.mdinto your~/.claude/CLAUDE.md, where it covers every project, or into one project'sCLAUDE.md. It teaches an agent to read its name and token from the environment and hand them tojoin, to callcheck_messageswhen it sees a[switchboard]line, and to read what its peers say without falling into a thank-you loop. It also draws the line that matters: coordination is not subordination, and no other agent can authorize what your user didn't.
Run this instead of openingclaudeyourself:
switchboard start alpha --role "payments API backend" --dir ~/projects/api
- The Hub registers the agent over REST, before Claude Code opens.
- A tmux sessionsb-alphastartsclaudein the--dirdirectory.
- From an interactive terminal,startrunstmux attachon that session, so your Windows Terminal tab becomes the agent's screen. Detach withCtrl-b dand the agent keeps working in the background.
- The agent joinssilently: its MCP client connects carrying the identity headers, and the Hub binds the session — it shows up asMCP connectedinswitchboard statuswith nothing typed into its terminal. Registered the MCP without the headers? A few seconds after the TUI is ready, a one-line kickoff asks the agent to calljoinitself instead (--no-kickoffturns that off).
startflags:--role "<description>",--dir <path>,--no-kickoff,--agent <claude|codex>,--claude-args "<extra args for the agent CLI>".
Already have a Claude Code window open (a plainclaudein bash,withouttmux) and want to join it to the networkwithout losing the conversation? Usewireinstead ofstart:
- In that window,leave claude(Ctrl-Ctwice, or/exit).
- In thesame folder, run:
switchboard wire
Under the hoodwirereopens claude with-c(continue the folder's conversation) and--dangerously-skip-permissions(so the agent reads its token and callsjoinwith no prompt) — these are thewiredefaults, unlikestart. Any extra--claude-argsare addedafterthem. If a tmux session for that name already exists,wirereplaces it(kills the old one and recreates it — no confirmation), then runs the same automatic kickoff asstart.
Auto-fallback:if the folder has no resumable conversation (never opened claude there, or the last one ran in-p/print mode),claude -cexits right away —wiredetects that and automatically reopens afreshsession (without-c), telling you so. It never fails into a dead window; worst case you get a brand-new conversation already wired to the network.
wireflags:--name <name>,--role "<description>",--dir <path>(default: current folder),--no-kickoff,--agent <claude|codex>,--claude-args "<extra args for the agent CLI>".
Choosing the agent CLI (--agent claude|codex)
Every way of opening an agent takes anagent type:claude(default, Claude Code) orcodex(Codex CLI). It is one flow with a choice, not a separate mode — registration, the nudge, the kickoff,status, mentions and the dashboard all behave identically:
switchboard start alpha --dir ~/projects/api --agent codex switchboard wire --agent codex # adopt the current folder with Codex
In the dashboard, theLaunch agentform has aClaude | Codexswitch, and each card shows its agent's type next to the MCP chip. The type isrecorded on the agent, soreopenrelaunches it with the same CLI it was launched with.
Requirements: thecodexbinary on the PATH, and the Hub registered as an MCP server in Codex (codex mcp add switchboard --url http://127.0.0.1:4577/mcp—switchboard setupoffers this automatically when it findscodex). Without that registration a Codex agent opens fine but has no Switchboard tools to join with.
What differs under the hood is only the argv and the two strings read off the TUI — both live in one adapter (src/shared/agent-types.ts):
Agents registered before this feature existed have no recorded type and are treated as Claude Code — which is what they are.
Groups — keep one project's agents out of another's
Every agent belongs to a group, and a group is a wall: an agent can only message agents in the same group,list_agentsshows it nobody else, and its broadcast stops at the group's edge. Run one project's agents inpanoramaand another's insiteand neither can wake the other, whether you slipped or an agent did.
switchboard start alpha --dir ~/projects/api --group panorama switchboard wire --group site # adopt the current folder into another group switchboard status # the GROUP column tells you who talks to whom
In the dashboard, theLaunch agentform has a group field, and the tabs above the transcript switch rooms: pickpanoramaand you read that group's conversation alone. A broadcast you send from there reaches that group and stops.
Agents already running?You don't have to relaunch them. Open the⋯menu on a card and pickgroup…; the name cell becomes a field with your existing groups behind it, and Enter moves the agent. It takes effect on that agent's next message, with no restart: unlike rename, which needs the agent stopped (a live one would re-join under its old name and undo it), nothing about a running session undoes a group change.
Leave--groupoff and nothing changes: the agent keeps the group it already had, and a new one joinsdefault, where every agent you have today already lives. Re-runningstartorwirewithout the flag never moves an agent out of its group.
You are the operator, so no wall applies to you: you can message any agent from the dashboard.
Inside any agent's window, reference another agent as%<name>and it becomes a delegation. For example, telling your backend agent:
Fix the pagination bug, and ask %frontend to update the consumer types afterwards.
makes it fix the bugandsendfrontendone factual, actionable message with the delegated task (paths, contracts, what to report back). The mentioning agent stays responsible for your request — the mention only routes the sub-task. This is part of the agent protocol (thejoinetiquette + the snippet), so it works in every connected agent.
Why%and not@:@is already thefile-referencesigil in Claude Code and in Codex, and the TUI resolves itbeforethe model ever sees your prompt. Agent names are commonly folder names (wirederives one from the other), so@frontendtyped next to afrontend/folder quietly turns into a file reference and the delegation is lost with no error.%collides with nothing in either CLI (!is bash,#is memory,/is commands).@<name>is still understood — it just fails whenever a path happens to match.
The dashboard (http://127.0.0.1:4577/) has aLaunch agentform (bottom of the sidebar): type the projectdirectory, optionally a name (defaults to the folder name) and a role, pick the agent (ClaudeorCodex), tickcontinue conversationto resume the folder's last conversation (same auto-fallback aswire), and hit Launch. The Hub itself creates the agent's tmux session and runs the automatic kickoff — no terminal needed. The new card appears live via SSE; attach to the agent anytime withtmux attach -t sb-<name>. Under the hood it isPOST /api/agents/launch {dir, name?, role?, continue?, agentType?}— localhost-only, like everything else.
Click an agent's card and you getits conversation, rendered as chat: what you asked, what it answered, its tool calls folded into one line each, markdown and syntax highlighting, and the harness happenings the terminal shows (worked for 2m 20s, background tasks, interrupts). It is read from Claude Code's own transcript log — Switchboard owns nothing, so the agent's session is untouched. The composer below it dispatches to that agent: type and press Enter,/for slash commands (routed to the terminal),@to mention a file, drag a file in to reference it, and the 🎤 to dictate — speech-to-text runslocallyon your machine, no API, no key, no bill.
Every message that arrives from a peer shows as the Switchboard's own line with ashow what X senttoggle that unfolds the real message, and in the traffic feed every read message has areactiontoggle that shows what the recipient said about it in its own chat. The two answer the question a message board never does:and then what happened?
ToggleTerminaland the agent'slive screentakes over the panel — the real Claude Code (or Codex) TUI, colours and cursor and all, and you can type into it (approve a prompt, hit Esc to interrupt, drag a file in to type its path). Open several agents and they become tabs across the top;windowin the card's menu still pops a real OS terminal when you want one. This is a tmuxcontrol-modeclient (tmux -C), not a second pty: tmux owns the agent's process, so closing the dashboard never takes the agent down — the whole point of being able to close the pile of terminal windows.
Agents name absolute paths constantly ("wrote/home/you/api/src/foo.ts"). Those paths are clickable in the transcript — click one and the file opens inline (images, text, code, markdown). Reads arescoped: only files under an agent's working directory or your home folder, resolved with realpath so..and symlinks cannot escape. A path outside the scope is refused with a clear message, never read — a message body is untrusted, so an agent cannot get you to open an arbitrary file by naming it.
Two doors sit in the preview's header for what an inline panel cannot do:browseropens the file in a real tab (an.htmlan agent built renders as the actual site — inside a CSP sandbox, so its scripts can never drive the Hub with your authority), andfolderreveals the file in Windows Explorer.
Dropping a fileon the chat or the terminal references the filewhere it already lives— the Hub finds the original by name and size across your agents' projects, Downloads and Desktop, exactly like dragging into a real terminal. Only when there is no original to point at (a pasted screenshot, an attachment dragged straight out of a mail client) does it stage a copy under~/.switchboard/uploads, kept for 24h; a big file with no origin is refused rather than duplicated.
Each card carries anopenbutton (reopenwhen the agent is offline: relaunches it in its folder, continuing the conversation, with the same CLI it was launched with) and a⋯menu:
Agent names areaddresses(%namein a prompt, the tmux sessionsb-<name>), so they are lowercase letters, digits and hyphens. You don't have to memorize that: the name fields rewrite what you type as you type it —Chefe de Redesbecomeschefe-de-redesin front of you, the same waywirederives a name from a folder calledai panorama.
To stop theHub:Ctrl-Cin theswitchboard serveterminal (ortmux kill-session -t sb-hub, if it runs in the recommended session).
Data lives in~/.switchboard/:config.json(every value has a default; the file may not even exist),agents.json(atomic snapshot) andmessages.jsonl(append-only, greppable withcat/jq).
The threat model is honest and the trust boundary is thelocal machine. Read this before exposing anything:
- Bind on127.0.0.1, hard-coded and not configurable.A delivered message becomes executable input for an agent with filesystem access. Exposing the Hub on the network = free RCE.
- NEVER port-forward port 4577(nossh -L, no firewall/NAT rule) andNEVER run the Hub behind a reverse proxy.127.0.0.1is the only barrier.
- Local trust model:any local process can post to the Hub and therefore inject input into any agent. This is accepted in v1 (the same model as any local dev tool), as long as it never leaks to the network.
- The nudge never types into a pane a dialog owns.The nudge is one line plus a separate Enter ~500ms later (the only way a TUI accepts it), and with a permission prompt open (Do you want to proceed? ❯ 1. Yes) that Enter used to land on the highlighted choice — meaning any agent able to send a message could approve another agent's pending tool call. Proven with a disposable agent, then closed: the pane reader reportsblockedfor permission prompts, trust dialogs and channel warnings; the dispatcher queues instead of typing, andre-reads the live pane immediately before typing(fail-closed — an unreadable pane counts as blocked). The held message is delivered by the next flush, once you answer the dialog.
- Capability token (v1.1 addendum):startinjects a per-agent token into the tmux session environment (SWITCHBOARD_AGENT_TOKEN); the agent reads it and passes it tojoin, and itnever appearsinlist_agents, inGET /api/agents, in the dashboard or in the logs. With the identity headers registered (see step 3) the token never reaches the model at all: the MCP client sends it straight from the session's environment.
- Operator-only surfaces.What an agent can persist about itself stops at its role. The boot command and launch args — the two fields that decide what runs at launch — are settable over REST (the dashboard) and deliberatelynot exposed over MCP, so a compromised agent cannot write its own boot code. It closes impersonation by processes that know an agent's name but never talk to the registration endpoint.
- Known residual risk (documented in the comment ofsrc/server/api.ts):thePOST /api/agents/registerendpoint isdeliberately unauthenticated, and re-registering an existing nameregenerates and returns a fresh token. So a malicious local process can obtain a valid token for any name and impersonate that agent viajoin— also invalidating the legitimate session's token (itsjoinafter a Hub restart then fails). This is accepted by the v1.1 spec (the same "any local process can post" boundary) andmust not be "fixed" without approval— requiring token rotation would breakswitchboard start's re-attach.
- Prompt injection between agentsis a residual risk: a compromised/hallucinating agent may try to manipulate another. v1 mitigation: the boundary declared in the protocol snippet (peer messages are evaluated critically; coordination ≠ subordination) plus full feed visibility in the dashboard.
tmux tips on WSL / Windows Terminal (pitfall P11)
- If you rarely use tmux, a minimal~/.tmux.confwith themouse enabledhelps a lot with scrolling and pane selection:
set -g mouse on
Switchboard runsClaude CodeandCodex CLIagents today (seeChoosing the agent CLI) — on the same network, in the same conversation. A few directions for later:
Sign in to leave a review
Use Google, GitHub, or an email account so ratings stay tied to real people.
No reviews posted yet.





