SLOPSHOPPER

agent-bridge

Talk to OpenAI Codex from Claude Code: live peer messaging over a local pipe, plus headless delegation (ask_codex).

newnetworktimer
★ 1v0.30.4MITupdated 2026-10-09rennerdo30/agent-bridge/plugins/claude
A shopper browsing a rack in a slop shop
README

<img src="assets/logo.svg" alt="" width="72" height="72">

agent-bridge

Let Claude Code, OpenAI Codex, opencode and Google Antigravity CLI work together.

agent-bridge connects the coding agents on your machine, and on PCs you pair, into one team. They message each other, hand each other work as subagents, and share history. You follow and steer everything from one local dashboard.

Documentation · Install · Update · Changelog

The agent-bridge dashboard: sessions grouped by project, a subagent tree and a live conversation

What it does

  • Messaging between agents. Sessions send each other questions, reviews and results. Messages to an offline or idle agent wait and wake it when possible.
  • Subagents across CLIs. Any agent can run another one headlessly (ask_codex, ask_claude, …) or start it in the background (spawn_*). You can talk to a running subagent, continue a finished one with its full context, cancel it, or hand all of them to another session. Edits can run in their own git worktree.
  • Projects with a main and a secondary. A Claude and a Codex session in the same folder share their project's jobs. The main session is the first contact. If it closes or runs out of usage, the secondary takes over and nothing is lost.
  • Questions to you. Agents ask you decisions in one place. You hear a chime in the dashboard, or get a Windows notification that opens the question. Your answer goes straight back to the agent that asked.
  • A dashboard for everything. Sessions, subagent trees, chats, progress with ETA, approvals, pinned decisions, full history search and paired PCs, at http://127.0.0.1:4777.
  • Paired PCs. Two PCs pair once over TLS. Their agents then message each other, transfer files and start jobs on the other PC.
  • Nothing gets lost. Every message, job, chat and decision is stored and searchable. History is archived, never deleted, and upgrades migrate your data with a backup first.

Install

Requires Node.js 22.16+. Install for every supported CLI it finds on your machine:

npx -y github:rennerdo30/agent-bridge install

It shows its plan per tool and asks first; add --yes to skip the questions, or name tools: install claude codex opencode antigravity. In Codex, open /hooks once and trust the agent-bridge hooks. Per-CLI details: Installation.

Update

npx -y github:rennerdo30/agent-bridge update
npx -y github:rennerdo30/agent-bridge status   # which sessions still run an older version

You can update while sessions run. New sessions start on the new version, and running ones switch at their next plugin reload. Mixed versions keep working together. See Updating.

First steps

Just ask your agent in plain language:

Ask Codex to review the diff in src/auth and tell me what it finds.

Start two Codex subagents in worktrees: one writes the tests, one fixes the bug.

Hand all your subagents to codex-myproject, I'm closing this session.

Open the agent-bridge dashboard.

All tools, parameters and behavior are in the MCP tools reference.

How it works

There is no daemon. Each agent session starts its own small agent-bridge server. The first one becomes the broker on a local pipe (a Windows named pipe or a Unix socket), and if it exits another takes over. Data lives in SQLite under ~/.agent-bridge. Subagents run in detached job runners, so they survive plugin reloads.

 Claude Code session                          Codex session
┌───────────────────────┐                    ┌───────────────────────┐
│ agent-bridge server   │◄── local pipe ────►│ agent-bridge server   │
│ hooks · channels      │                    │ hooks · app-server    │
└───────────────────────┘         │          └───────────────────────┘
          first server to start is the broker · messages in SQLite

⚠️ WHAT AGENT-BRIDGE MAY DELETE ON YOUR DISK

[!WARNING] Read this before you turn on any cleanup. By default agent-bridge removes nothing of yours.

  • Job worktrees only, and only ones agent-bridge created itself. When a subagent gets its own git worktree, agent-bridge writes an ownership record for it at creation time. A worktree without that record, or whose folder was replaced, is never touched, whatever its branch or folder name. Your own worktrees and checkouts are never removed.
  • Automatic removal when a job closes is OFF by default (jobCloseCleanup: false). If you turn it on, a finished job's worktree is removed only when all of these hold: no uncommitted or untracked files, no unknown ignored files, no links into other folders, and every commit has first been pushed and verified on your remote (as wip/<job>). If any check fails, the worktree is kept and the reason is recorded. Local branches are always kept.
  • agent-bridge cleanup is manual. Without --yes it only lists what it would remove. With --yes it removes only worktrees that are inside agent-bridge's own worktree folder, belong to a recorded agent-bridge job, are on an agent-bridge/* branch fully merged into its base, and have nothing uncommitted. Everything else is kept.
  • Your history and messages are never deleted. Old data is archived. agent-bridge storage finalize removes old backup copies only after a migration has been verified with counts and hashes, and only when you confirm with --yes.

If you are unsure, leave cleanup off. Nothing breaks; worktrees simply stay until you remove them.

Security

  • Only your own agent-bridge processes can connect: every connection presents a secret from ~/.agent-bridge/token.
  • The dashboard listens on 127.0.0.1 only and needs its access key.
  • Subagents are read-only by default. You raise access per job, and edits can be isolated in worktrees.
  • Messages from other agents are marked as such, and agents are told not to act destructively only because a peer asked.
  • Pairing uses TLS 1.3 with a one-time code; discovery never connects on its own.

Details: Security.

Development

npm install
npm run check   # typecheck, tests and build

npm run build bundles src/ into plugins/*/dist, which is committed so the plugins install straight from Git. The documentation site lives in docs/ (Astro Starlight).

License

MIT

Source 1 files
hooks/wake.ts 114 lines
1import type { Register } from 'claude-code'
2
3/**
4 * agent-bridge's mod (Claude Code 2.1.287+): wakes the idle session with a real turn when something it waits
5 * for arrives (a subagent's result, an approval question, an awaited reply).
6 *
7 * The MCP server publishes its wake endpoint per session in ~/.agent-bridge/sessions/<session id>.json. While
8 * the session is idle the mod long-polls it (`/wait?role=mod`) and submits what comes back as a prompt. It
9 * tells the server when turns start and end (`/mod?busy=`), so messages arriving mid-turn go out with the tool
10 * hooks instead. Once the server has seen the mod, the asyncRewake Stop hooks step aside; older Claude Code
11 * versions keep using them.
12 */
13const RETRY_MS = 5_000
14/** A wake-up that started no turn within this long is asked for again (the server hands out the same messages). */
15const RETAKE_MS = 25_000
16
17type Engine = Parameters<Parameters<Parameters<Register>[0]>[2]>[0]
18type Endpoint = { port: number; secret: string }
19
20const state = {
21  /** Bumped by every new poll and every turn start: an older poll loop stops. */
22  generation: 0,
23  /** Main-loop turns that started and have not completed. */
24  openTurns: new Set<string>(),
25  turnsStarted: 0,
26  /** Ids of the messages already submitted: never submitted twice (a server that missed the delivery). */
27  submitted: [] as string[],
28}
29
30const MAX_REMEMBERED = 500
31
32async function endpoint($: Engine): Promise<Endpoint | null> {
33  const home =
34    (await $.env.get('AGENT_BRIDGE_HOME')) || `${(await $.env.get('USERPROFILE')) || (await $.env.get('HOME')) || ''}/.agent-bridge`
35  const id = (await $.session.id()).replace(/[^\w-]/g, '_')
36  try {
37    const reg = JSON.parse(await $.fs.read(`${home}/sessions/${id}.json`)) as Endpoint
38    return typeof reg.port === 'number' && typeof reg.secret === 'string' ? reg : null
39  } catch {
40    return null // the server has not registered this session yet
41  }
42}
43
44async function call($: Engine, path: string, method = 'GET') {
45  const ep = await endpoint($)
46  if (!ep) return null
47  return $.http.fetch(`http://127.0.0.1:${ep.port}${path}`, { method, headers: { authorization: `Bearer ${ep.secret}` } })
48}
49
50async function poll($: Engine, mine: number): Promise<void> {
51  while (mine === state.generation) {
52    try {
53      const r = await call($, '/wait?role=mod')
54      if (mine !== state.generation) return
55      if (!r || !r.ok) {
56        await $.clock.sleep(RETRY_MS)
57        continue
58      }
59      const { text, superseded } = JSON.parse(r.text) as { text?: string; superseded?: boolean }
60      if (superseded) return
61      if (!text) continue
62      const ids = [...text.matchAll(/<agent-bridge-message id="([^"]+)"/g)].map((m) => m[1]!)
63      if (ids.length && ids.every((id) => state.submitted.includes(id))) {
64        // Shown before: tell that server they were delivered (a turn start confirms), instead of a second turn.
65        await call($, '/mod?busy=1', 'POST')
66        await call($, '/mod?busy=0', 'POST')
67        continue
68      }
69      state.submitted = [...state.submitted, ...ids].slice(-MAX_REMEMBERED)
70      const started = state.turnsStarted
71      await $.prompt.submit({ text })
72      // No turn came of it: ask again (the messages stay unread until a turn shows them).
73      $.clock.after(RETAKE_MS, () => {
74        if (state.turnsStarted !== started || mine !== state.generation) return
75        // They were never shown: submitting them again is right this time.
76        state.submitted = state.submitted.filter((id) => !ids.includes(id))
77        void poll($, ++state.generation)
78      })
79      return
80    } catch {
81      await $.clock.sleep(RETRY_MS).catch(() => undefined)
82    }
83  }
84}
85
86export const register: Register = on => {
87  on('session.start', async ($, e, next) => {
88    const result = await next(e)
89    void poll($, ++state.generation)
90    return result
91  })
92
93  on('turn.start', async ($, e, next) => {
94    state.turnsStarted++
95    state.openTurns.add(e.turnId)
96    state.generation++
97    void call($, '/mod?busy=1', 'POST').catch(() => undefined)
98    return next(e)
99  })
100
101  on('turn.complete', async ($, e, next) => {
102    const result = await next(e)
103    // A subagent's runs complete too; only the main loop's turns count.
104    if (!e.agentId) {
105      state.openTurns.delete(e.turnId)
106      if (state.openTurns.size === 0) {
107        await call($, '/mod?busy=0', 'POST').catch(() => undefined)
108        void poll($, ++state.generation)
109      }
110    }
111    return result
112  })
113}
114