@codependence/cli
v0.4.3
Published
Ask your other Claude Code sessions questions. Runs on the machine where your sessions live and answers on their behalf.
Readme
The machine bridge
The bridge is a daemon that runs on your laptop. It is the piece that makes a real Claude Code session answer questions from the outside.
What it actually does
YOUR SESSION (project A) CLOUD SERVICE PEER MACHINE (this bridge)
┌─────────────────────┐ ┌──────────────────┐ ┌────────────────────────────┐
│ Claude Code │ ask │ Worker + AgentDO │ poll │ bridge.mjs │
│ talking to /mcp │──────▶│ queue + D1 │◀──────│ long-polls for work │
│ │◀──────│ │──────▶│ │
│ gets a thread_id │ticket │ │message│ │ tmux send-keys │
└─────────────────────┘ └──────────────────┘ │ ▼ │
│ ▲ │ Claude Code in tmux │
│ check_answer │ reply │ (the live session, with │
└────────────────────────────┴─────────────────│ all of its context) │
└────────────────────────────┘Asking from a project
cd ~/the-project-that-asks
codependence connectMints a client token and registers the MCP server in that directory. The token is never displayed -- it goes straight from the service into your Claude Code config, which is the only handling of it that cannot end up in a terminal scrollback or a chat window.
Restart that session afterwards; MCP servers are read at startup. Then ask in
English: ask alice/billing why the webhook retries twice.
To see who you can ask:
codependence peersPublishing a peer
Stand in the repo you want answerable and run:
cd ~/src/billing
node ~/src/codependence.ai/bridge/bridge.mjs addIt takes the path, the name and the session from where you are standing, so
~/src/billing becomes <your-handle>/billing. Pass a name to override it:
add billing-api.
Then restart the bridge to connect it. To stop publishing one:
node bridge/bridge.mjs remove billingBoth edit bridge/config.json for you. Editing that file by hand still works and
does the same thing -- add exists so you do not have to know its shape.
Worth doing afterwards: give the peer some capability_tags in the config. They
are what an asking agent reads to choose who to ask, so they are written for the
agent doing the picking rather than for you.
Delivery modes
headless (default, recommended)
Shells out to claude -p --resume <session> --fork-session, with the question on
stdin. That single command does everything the tmux dance was working around:
--resumecarries the peer session's accumulated conversation -- the reasoning that never landed in a commit message, which is the entire reason to ask a peer instead of reading its repo yourself.--fork-sessionmeans answering does not touch the live session's history. Nobody's work gets interrupted.-pmeans the process exiting is the completion signal. No screen to scrape, no answer file to wait for. Its stdout is the answer.--allowedToolsrestricts the run to reading (Read,Grep,Globby default). The question is text from another machine and must not reach a tool that changes anything.- It works anywhere the
claudeCLI runs, which includes Windows.
Which session gets forked: session_id if you set one, otherwise the most
recently active session for that repo_path. If several sessions run in the same
repo, "most recent" may not be the one you want -- pin it. --doctor prints
which session each agent will use.
If there is no prior session at all, the run still happens: it just answers from CLAUDE.md, project memory and the repo, without conversation history.
tmux (legacy)
Types one line into a live Claude Code session telling it to read a question file, whose instructions tell it to write an answer file, which the bridge waits for.
This exists because you cannot tell from a TUI's rendered output when Claude has
finished thinking, so "detect when a TUI is done" had to become "wait for a file".
It works, and it is the only mode that reaches a session you are actively sitting
in -- but it needs tmux, it types into your live session, and that session runs
with whatever tool permissions you gave it. Prefer headless.
echo
Answers automatically with a canned reply. No Claude, no tmux. Use it first: it proves the whole loop, so a failure afterwards is never the plumbing's fault.
Safety: a question is text from another machine
Be clear-eyed about this. A question written by an agent elsewhere is read by a Claude Code session that has tools.
In headless mode the fork is restricted to read-only tools, which is a real
boundary rather than a polite request. In tmux mode it is injected into your
live session with whatever permissions that session already has, and the only
defense is the wrapper telling it to treat the text as data and not act on it --
a mitigation, not a guarantee.
Either way the peer can read its repo and put what it finds in the answer, so only put agents on the bus whose askers you trust. Today that means your own machines.
Install
npm install -g @codependence/cli
codependence loginNode 18 or newer. Node 22 has a WebSocket built in; on older versions the CLI uses a bundled fallback, so there is nothing to configure either way.
You also need the claude CLI signed in on the same machine -- that is what
actually answers questions, so the bridge is useless without it.
One thing that will catch you: if you use nvm and switch Node versions later,
global packages are kept per version, so codependence will be missing until you
install it again under the new one. That is nvm, not this.
Setup
1. Run the service
npm install
npx wrangler d1 migrations apply codependence --local
npm run dev2. Sign in and mint a bridge token
Tokens belong to you and are minted while signed in. Sign in at /auth/github,
choose a handle if you have not already, then:
curl -sS -X POST http://localhost:8787/v1/me/tokens \
-H 'Content-Type: application/json' -b cookies.txt \
-d '{"label":"my-laptop","kind":"bridge"}'It looks like cdp_<your-handle>_<random>. The handle is in the token because
tokens live in their owner's Durable Object, so verifying one is a parse rather
than a lookup in a global table -- and there is no cross-owner index to leak.
Shown once, stored only as a SHA-256 hash. One per machine, so a single laptop can be revoked without killing the rest.
3. Write the config
cp bridge/config.example.json bridge/config.jsonEdit it: paste the token, and list the agents this machine hosts. config.json
is gitignored.
4. Check everything before you trust it
node bridge/bridge.mjs --doctorIt verifies the service is reachable, the token is accepted, tmux is installed,
each tmux_target pane actually exists, and each repo_path is a real
directory. Fix whatever it flags.
5. Run it
codependence startThis holds the terminal, which is what you want the first time: you watch it register, connect and answer, and you learn what it does.
6. Keep it running
start dies with the shell that launched it. On a machine you reach over SSH
that means the peer goes offline the moment you disconnect.
codependence service installThat writes a user service — ~/.config/systemd/user/codependence.service
on Linux, ~/Library/LaunchAgents/ai.codependence.bridge.plist on macOS — and
starts it. It runs as you, not as root, because the bridge needs your home, your
claude credentials and your repos.
codependence service status
codependence service logs -f
codependence service uninstallTwo details that decide whether this actually survives:
- Lingering. systemd tears your user manager down when your last session
ends, so without
loginctl enable-linger <you>the service stops when you log out — exactly the problem it was installed to solve.service installenables it, falling back tosudo -nand telling you the command if both are refused. - PATH is baked in at install time. The bridge shells out to
claude, and a service inherits almost nothing. The unit pins the absolutenodethat ran the install and the PATH that could findclaudeat that moment. If you later change Node versions with nvm, re-runservice install.
Asking from a project
cd ~/the-project-that-asks
codependence connectMints a client token and registers the MCP server in that directory. The token is never displayed -- it goes straight from the service into your Claude Code config, which is the only handling of it that cannot end up in a terminal scrollback or a chat window.
Restart that session afterwards; MCP servers are read at startup. Then ask in
English: ask alice/billing why the webhook retries twice.
To see who you can ask:
codependence peersPublishing a peer
Stand in the repo you want answerable and run:
cd ~/src/billing
node ~/src/codependence.ai/bridge/bridge.mjs addIt takes the path, the name and the session from where you are standing, so
~/src/billing becomes <your-handle>/billing. Pass a name to override it:
add billing-api.
Then restart the bridge to connect it. To stop publishing one:
node bridge/bridge.mjs remove billingBoth edit bridge/config.json for you. Editing that file by hand still works and
does the same thing -- add exists so you do not have to know its shape.
Worth doing afterwards: give the peer some capability_tags in the config. They
are what an asking agent reads to choose who to ask, so they are written for the
agent doing the picking rather than for you.
Delivery modes
"delivery": "echo" answers automatically with a canned reply and needs no
tmux at all. Use it first. It proves the whole loop — ask, queue, poll, deliver,
reply, collect — so that when you switch to a real session you know any problem
is the session's, not the plumbing's.
"delivery": "tmux" hands the question to a live Claude Code session.
Find your pane address with:
tmux list-panes -a -F "#{session_name}:#{window_index}.#{pane_index}"Walking through a test
Test 1 — the plumbing, with no tmux
Config one agent with "delivery": "echo". Start the bridge, then ask it
something from another terminal:
CLIENT=$(curl -sS -X POST http://localhost:8787/v1/admin/tokens \
-H 'Content-Type: application/json' -H 'X-Admin-Secret: local-dev-admin-secret' \
-d '{"label":"asker","kind":"client"}' | jq -r .token)
curl -sS -X POST http://localhost:8787/v1/ask \
-H "Authorization: Bearer $CLIENT" -H 'Content-Type: application/json' -d '{
"to": "john/echo-peer",
"question": "Does the round trip work?",
"symptom": "I have never seen this system answer anything",
"what_i_tried": "Reading the code",
"relevant_code": "none -- this is a design question, not a bug",
"wait_ms": 15000
}' | jqYou should get "status": "answered" back, and see the bridge log the delivery.
If you do, every moving part except tmux is working.
Test 2 — a real Claude Code session
- Start a session in the repo you want to be the peer:
tmux new -s peer -c /path/to/that/repo claude - Get the pane address and put it in
tmux_target. - Set that agent to
"delivery": "tmux"and restart the bridge. - Ask it a question that genuinely requires reading that repo.
- Watch the peer pane: a line appears, the session reads the inbox file, works, and writes the outbox file.
- Your
askwill almost certainly return"status": "pending"with athread_id— that is the expected outcome, not a failure. Collect it:curl -sS "http://localhost:8787/v1/threads/$THREAD_ID" \ -H "Authorization: Bearer $CLIENT" | jq -r '.thread.answer'
Test 3 — from inside Claude Code, as an MCP server
This is the real thing: one session asking another with no curl involved.
claude mcp add --transport http codependence http://localhost:8787/mcp \
--header "Authorization: Bearer $CLIENT"Then in that session: "ask john/echo-peer whether the round trip works". It
will call list_agents, then ask, get a ticket, and — because the tool
description tells it to — carry on with other work and call check_answer
later rather than reporting a failure.
Things that will happen to you
Superseded. One bridge per agent. If you leave a second running in another
terminal, the older connection is sent a superseded message explaining why and
then closed, and that bridge exits with status 1 rather than lingering as a
process that keeps agents marked online while listening to nothing.
Restarting is instant: the new connection displaces the old one.
How work arrives. The bridge holds a WebSocket per agent and is pushed questions as they arrive. It does not poll. An earlier version long-polled over HTTP, which held each agent's Durable Object awake for the whole wait window and was billed by wall-clock time -- roughly 11,000 GB-s per agent per day against a 13,000 GB-s daily free tier, for agents doing nothing at all. A hibernated WebSocket costs nothing while idle.
GET /v1/agents/:id/peek still exists for a single non-blocking look at the
queue, but it will not wait, and asking it to will tell you why.
Which bridge is authoritative? Do not read logs across terminals. Ask:
curl -sS "http://localhost:8787/v1/admin/agents/john%2Fyour-agent/poller" \
-H "X-Admin-Secret: $ADMIN_SECRET" | jqIt tells you which machine holds the poll and since when, or says plainly that nobody is polling and questions are piling up.
A question that never gets answered. If the session is busy or the answer
file never appears, the bridge gives up after answer_timeout_ms (default 8
minutes) and marks the thread failed with a reason -- not answered. An
asking agent reading status answered should never find failure text there.
Keep answer_timeout_ms below the server's LEASE_MS (default 10 minutes).
A question is only ever handed to a bridge three times. A bridge that crashes on
the same question every time would otherwise requeue it forever, leaving the
asker polling a thread that can never resolve; after the third attempt it is
marked failed saying so.
Ctrl-c is clean. The bridge reports any question it was mid-way through as
failed before exiting, so the asking agent finds out immediately instead of
waiting out the ten minute lease.
A dead tmux pane. If the target pane does not exist when a question arrives, the bridge replies immediately saying so rather than swallowing the question.
