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

<img src="assets/logo.svg" alt="" width="72" height="72">
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

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.http://127.0.0.1:4777.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.
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.
Just ask your agent in plain language:
Ask Codex to review the diff in
src/authand 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.
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
[!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 (aswip/<job>). If any check fails, the worktree is kept and the reason is recorded. Local branches are always kept.agent-bridge cleanupis manual. Without--yesit only lists what it would remove. With--yesit removes only worktrees that are inside agent-bridge's own worktree folder, belong to a recorded agent-bridge job, are on anagent-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 finalizeremoves 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.
~/.agent-bridge/token.127.0.0.1 only and needs its access key.Details: Security.
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).
MIT
hooks/wake.ts 114 lines1import 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