convoy
OfficialAllows Convoy to onboard Hermes CLI sessions as neurons, manage resume tokens, and send headless synapses to Hermes for tasks on a shared thread.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@convoybring up an isolated Windows Terminal window with split panes for threads T0, T1, and T2"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Convoy
Convoy is shared project memory for Grok Bot: attach one MCP endpoint, route work to your existing CLIs, and keep every neuron grounded in one durable thread state.
Public MCP remains one root: https://convoy.bot/mcp. A named thread is a --root binding, not a second MCP URL.
Install
Python >= 3.11, standard library only (no runtime dependencies).
git clone https://github.com/Deploy-Forward/convoy.git
cd convoy
python -m pip install .
convoy --helpAlternative: pipx install . (not verified on Windows in this pass).
The convoy console script and python -m convoy both work only after
install. To run from a checkout without installing, put src on the path:
PYTHONPATH=src python test/run.py on bash, or
$env:PYTHONPATH='src'; python test/run.py in PowerShell.
Grok Marketplace plugin
The xAI-compatible plugin root is plugin/convoy: it ships
.grok-plugin/plugin.json, .mcp.json, and the convoy +
convoy-wizard skills. The official source of truth for Grok Marketplace
discovery is
xai-org/plugin-marketplace;
its third-party entry must pin a reviewed full commit SHA from this repository
and set path to plugin/convoy. After that catalog PR merges, Grok Build
install is /marketplace → convoy → i, the same path
exa-labs/exa-grok-plugin
documents. There is no OAuth step: .mcp.json points at
https://convoy.bot/mcp. The Agent Plugins/Cursor manifests remain
compatibility surfaces, not the xAI catalog. A Grok Bot Settings path is
unverified here.
OpenAI plugin
The OpenAI package lives at plugins/convoy, with the
required .codex-plugin/plugin.json, a remote HTTP .mcp.json, the canonical
Deploy Forward logo, and a bundled fail-closed Convoy skill. The repository's
local Codex marketplace is .agents/plugins/marketplace.json.
codex plugin marketplace add <checkout-root>
codex plugin add convoy@convoyThis package is intentionally separate from plugin/convoy: OpenAI, Cursor,
xAI, and Agent Plugins use different discovery manifests. Installing the
OpenAI plugin is the revocable permission grant for its declared MCP connection
and skill; endpoint write gates and exact action consent still apply. Public
directory submission remains a publisher step after Deploy Forward approves
privacy and terms URLs and, if used, OpenAI issues a real connector ID. The
release gates and exact submission sequence are tracked in
docs/openai-plugin-publication.md.
The canonical local, plugin, and future hosted sequence is documented in
docs/convoy-happy-path.md, including the exact
chair-addressed send path and the tenant-isolation requirement behind the
word "cloud."
Receiving messages needs a command that resolves
Neurons receive through a harness hook, and a hook runs in its own shell that inherits nothing from yours. Convoy therefore probes its own command line before writing any hook file, and refuses to write one that would not run. Check what it picked:
convoy --root <thread-root> skills --worktree <worktree>The card's hooks.resolved_via is one of console-script (the installed
convoy is on PATH — the best case), interpreter (this Python can
-m convoy), interpreter+src (no install: the command carries the
checkout's src with it), or kept-existing (a hook already there still
works and was left alone). If nothing resolves, the card carries an install
hint and no hook file is written; install the console script and re-run.
The same command is the repair when a hook goes stale: it prunes Convoy's own
dead entries and leaves your own hooks untouched.
Related MCP server: wmux
CLI reference
One line per verb; flags shown are the ones you will reach for (see
convoy <verb> --help for the full set). Every verb accepts a global
--root <thread-root> before the verb name.
Read (no writes to thread state):
threads [--prune]— every Convoy thread this machine knows.--prunedrops rows whose root is under the OS temp dir or is absent and reports every dropped row (never silent).panes— every body of every neuron on this thread, from the OS process table; never a token.whoami— which chair is this process? Walks process ancestry to the harness.graph [--neuron <chair>] [--html [--out <file>]]— read-only ontology of the thread.seats [--convoy-id <id>]— seat rows.feed --since <10m|2h|1d|45s|ISO>— events in a window; the card echoessince_iso.relaunch [--thread <name>] [--timeout <s>] [--dry-run]— after the panes died (shutdown): brings every chair up again fromseats.jsonlin its own worktree, queues each chair an inbox row saying when it left off (last_seen,unread, the exactfeed --since <ts>to run), stampskind=relaunch, and proves connected only from seated acks stamped after the relaunch. Dry shows the timeline and spawns nothing.rail [--since <window>]— the strip under the panes: feed events, seats connected | pending | stale (from the seated acks), usage remaining per harness (nullis unknown, never 0), last stamp, lead. Reads only the thread; from a chair's worktree it finds its thread through the machine index, so every neuron sees one rail.context [--instance-id <chair>]— pointer pack for a neuron.glance [--thread <name>] [--tray]— one-screen status.resume --neuron <chair>— dry: prints native argv + cwd, spawns nothing.choices— installed harnesses, known worktrees, chairs, terminal adapter; no resume tokens.probe --to <harness>,id,terminals.widget [--topmost/--no-topmost] [--refresh 3] [--service]— always-on-top tkinter strip: one dot per thread fromrecent(), expand chairs, click →focus; a stale chair shows anudgebutton (dry card first, keys typed only on confirm, then the feed is polled 60 s for the chair's own row — that row alone means delivered).pintoggles topmost;xhides to the tray wherepystray+PILimport, else minimizes.--servicestarts one detached strip per machine behind$CONVOY_HOME/widget.pid(already: truewhen the pid is alive and its image is our interpreter; a reused pid respawns).crew --launchandrelaunchstart it unless--no-widget. Stdlib only.focus --seat <chair>— ask the pane host to highlight that chair.{focused: false, reason}until a host adapter is evidenced (tmuxselect-pane -tis tested; Windows Terminalwt focus-paneis not evidenced on this machine).
Write (thread state):
init— create the thread layer at--root.bind --thread <name>— bind this root to a named thread.start [<repo>] [--to <harness> ...] [--thread <name>] [--cancel]— thin alias: git URL → clone once thenonboard --github yes; local path →onboard --github no; no repo → picker fromrecent()(title + root + last activity, never auto-picks newest); empty index → ask to start a new thread;--cancelleaves unbound. Already-live harness on the root (whoami/roster) →attach, never a duplicatebring_up.Outside-harness join: a session started in no worktree runs
whoami(chair null, an ask),threads,start(picker, never auto-picks), thenattach+join --to <harness>on the chosen root and receives queued sends without stealing a pane.docs/OUTSIDE_HARNESS_JOIN.md.onboard --to <harness> [--to ...] [--thread <name>] [--checkout-root <path|git-url>] [--github yes|no]— name installed harnesses and bind; a URL is cloned once under$CONVOY_HOME/checkouts/<owner>/<repo>(.convoy/andthread.mdgo into that clone's.git/info/exclude). Whoever launched first conducts: the first harness named on the first onboard becomeslead; a later onboard reports it and never steals it.seat --to <harness> --session-id <chair> [--worktree <path>] [--model M] [--resume <vendor-id>] [--title T] [--effort E]— register a seated neuron.join --to <harness> [--worktree <path>] [--title T] [--as <chair>] [--launch] [--consent <id>]— register one fresh chair.crew --seat <harness>[,model=M][,effort=E][,where=local|cloud][,title=T] [--seat ...] [--checkout <path>] [--launch]— N neurons at once: validates every seat first, mints one worktree per local seat, joins every chair with a boot prompt, and (with--launch) brings them up in ONE window. Launched is not connected: the card'sseatedsnapshot sayspending.await-seated --seat <chair> [--seat ...] [--timeout <s>]— observe the acks: per chairconnected(its ownseatedrow cites the minted token) |pending|stale, with the seconds waited.swap --seat <chair> --to <harness> --handoff <.convoy/handoff/<chair>-<ts>.md> --as <chair>— replace the occupant, keep the chair.seated --seat <chair> --token <token>— proof-of-life echo from the new occupant.lead --to <chair> --as <you>— pass lead to a chair.hook note "<text>" [--as-me] --to <chair>— leave a note for a chair (orgrok-bot).stamp "<summary>" [--agent A] [--model M] [--effort E] [--transcript <pointer>]— conductor stamp.send --to <chair|harness> "<body>" [--live] [--dry-run] [--instance-id <chair>]— synapse.--to <chair>(a session_id, e.g.codex-1-demo) queues into that chair's inbox in its own worktree (delivery: queued,delivered: false);--to <harness>with a chair already on that harness refuses (naming a vendor is not naming a neuron); default runner records a feed row (delivery: recorded);--liveruns a fresh headless vendor session (executed).inbox [--seat <chair>] [--drain | --hook-pretooluse]— list or drain the live-seat inbox. The hook command is alwaysconvoy inbox --hook-pretooluse(never a baked interpreter path).end [--summary <text>] [--push | --hook]— explicit task completion, or the Codex/Claude Stop heartbeat.--pushauthorizes exactly one plaingit pushand refuses dirty, detached, or no-upstream state;--hooknever pushes.install --to <harness> --opt-in [--live]— cataloged installer; dry-run by default.
Launch / panes:
choices— see above; run it first.launch --seat <chair> [--dry-run] [--consent <id>]— split one already-joined fresh chair into the active pane host.consent --grant <request-id>— grant a prior consent request after the user explicitly approves it.close --seat <chair> [--consent <id>]— request closure of one Convoy-managed pane.nudge --seat <chair> [--keys <exact>] [--target <tmux-pane>] [--dry-run] [--consent <id>]— wake an idle chair on this machine. Identifies the pane first (live body frompanesplus a unique WT title or tmux target). Live send needs a consent card that names that pane and the exact keys.delivery: nudged, neverdelivered. Refuses when the pane cannot be proven. Write-gated on MCP.bring-up/open [--thread <name>] [--dry-run]— bulk show of seated neurons in one new terminal window.hide/minimize/background [--dry-run]— bulk hide.resume --neuron <chair> --go— spawn once in the chair's worktree; refuses when a live body holds the chair.
MCP:
mcp [--root <thread-root>] [--host 127.0.0.1] [--port 8788]— serve the MCP endpoint for one root.
Run your own MCP
convoy mcp --root <thread-root> --port 8788Then attach http://127.0.0.1:8788/mcp in your MCP client. Write tools are
off by default on the RPC layer: set CONVOY_MCP_WRITE_TOOLS=1 on a
gated/loopback deploy to expose send, stamp, note, join, seat, launch,
crew, seated, consent, await_seated, focus, nudge, onboard, clone, mint,
repos, resume with go=true, and inbox with drain=true. An ungated
public tools/list hides the write tools rather than
listing and refusing them, so a listed verb is a promise. Reads (choices,
neurons, inbox pending, graph) stay public, and a public inbox
read never echoes the row token. repos wraps gh repo list on the MCP
process PATH (name, url, private, updated_at; gh absent is an install hint);
it lists the gh login on the MCP host, the conductor's account, which is why
it sits behind the gate rather than handing that inventory to strangers.
clone puts a URL under $CONVOY_HOME/checkouts/<owner>/<repo>; mint
derives one worktree per seat from that checkout as <checkout>-wt-<name>
on branch convoy/<name>, so nobody hand-makes worktrees for N neurons. crew
does the whole walk for N seats (validate, mint, join each with a boot prompt,
one window) and await_seated reads the acks back, so "they all connected" is
observed, never assumed. convoy preflight tells you which of the wizard's
verbs a live tools/list is missing and why.
The public https://convoy.bot/mcp is bound to one root; a different thread
means running your own server with your own --root.
Names you will see
Grok Bot — the xAI desktop conductor chat that attaches the MCP; not a neuron.
ola-brain — a private predecessor wrapper; refused by
install, not needed.Deploy-Forward/platform — a closed sibling repo; not needed to run this repo.
Aether — an internal demo host; not needed.
Terms
Grok Bot: the conductor in this chat; not a neuron and not a window.
neuron: one BYO harness session (
grok,claude,codex,cursor-agent,agy/antigravity,hermes, orpi) on a thread.synapse: a native Convoy
sendinto one neuron; one harness, one meter, compact card back.Convoy: source of truth (
feed, seats,convoy_id).thread: durable circuit keyed by
convoy_id.named thread: a
--rootbinding (not a second MCP URL).grok-bot-local vs grok-bot-cloud: neuron host (user machine vs cloud agent), not a second source of truth.
Product wording retires hop in favor of neuron/synapse/thread.
The problem
Single-harness chat is weak project memory: context windows bloat, meter state drifts, and another agent cannot safely rehydrate shared state without copy/paste loss.
Wrapper stacks (one vendor CLI inside another) add indirection and contention instead of shared truth.
The solution
Convoy keeps a slim pointer/stamp layer while Grok Bot remains conductor. Synapses run on native vendor CLIs, return compact cards, and keep session ownership separated.
Contract: feed + seats + convoy_id. Unknown values stay JSON null; no invented usage/session numbers.
Machine-readable contract: src/convoy/harness_effort.json (loaded by MCP-facing code).
Keyed effort language (locked)
Effort keys are harness-scoped and must not be merged. The locked key space
lives in CANON.md ("Effort keys are harness-scoped") and the
machine-readable source of truth is src/convoy/harness_effort.json — this
README deliberately does not restate the table, so there is exactly one place
for it to drift from the code: none.
Supported neurons (code-true contract)
grok, claude, codex, cursor-agent, and agy have a cataloged installer
(convoy install --to); hermes and pi are BYO-only (install refuses them)
and their direct-id resume is unverified.
Harness |
|
|
|
|
|
|
| Writes PATH ungate block; installs | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; installs neuron skills plus | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; installs neuron skills, | Native CLI on PATH. Named live seats queue; may |
|
|
| Writes PATH ungate block; installs | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; installs | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; installs | Native CLI on PATH. Named live seats queue ( |
|
|
| Writes PATH ungate block; installs | Native CLI on PATH. Named live seats queue ( |
Notes tied to code/tests:
seat.session_idandseat.resumeare distinct: session key vs vendor resume token.First-run seats can omit vendor resume; then no resume token is passed.
Live
sendis headless and never steals an active interactive neuron; refusal cards ask users tobring_up/ open a pane or write.convoy/handoff/<chair>-<ts>.md.context.packoverlays home-layerconvoy_id+thread_keyonto seat-worktree pointers when present.bring_up/openare the bulk show commands; targetedjoin --launchis the explicit one-chair exception described below.
Bring-up and pane invariants
Isolated WT only: one
wt --window newspawn, one tab, split panes joined with literal";"argv elements.Bulk bring-up never uses
-w 0or-w <thread>; targeted launch may use-w 0only after the user explicitly requests--launchfrom an active Windows Terminal session. Never--before harness exe; never per-seatCREATE_NEW_CONSOLE; never close on fail withWM_CLOSE.Two same-harness seats on different worktrees are two panes; duplicates collapse by worktree/resume/session key.
Ctrl+Shift+Wshould only drop one split pane at a time (or no-op when there is no split pane left).Codex TUI conflict: while a Codex pane is focused,
Shift+Up/Shift+Downcan change reasoning level and fight pane navigation. Do not use those shortcuts for pane selection in that focus state.
Targeted one-chair launch
Full contract and DoD: docs/targeted-launch.md.
convoy choices lists installed harnesses, known Git/registered worktrees,
existing chair identifiers, and the detected terminal adapter. It deliberately
omits every vendor resume token. A model or user can then invoke:
convoy join --to <harness> --worktree <path> --launchThis registers and launches exactly one fresh chair. Harness argv construction is independent of terminal placement, so all harnesses use the same terminal adapter contract. A persistent atomic launch claim refuses duplicate launchers; existing/resumable chairs are not eligible and a failed terminal spawn leaves the fresh chair pending for an explicit retry.
Supported active-pane adapters:
Host | Detection | Targeting |
Windows Terminal | Windows, |
|
tmux on macOS/Linux |
|
|
Other terminal hosts fail closed with a manual-pane instruction. Convoy never
injects keystrokes or guesses an iTerm, Terminal.app, WezTerm, kitty, or shell
API. Source installs currently require Python 3.11+; the project exposes a
cross-platform convoy console entry point, but a machine without Python still
needs a packaged executable/runtime before a skill can invoke it.
Creation and closure are separate capabilities. The Windows Terminal CLI can
create a split but does not expose its closePane action. A killed TUI may leave
an exited pane visible under graceful closeOnExit, so absent process IDs are
not pane-close proof. New targeted launches therefore use a Convoy lifecycle
host: after a separate, scoped close-chair consent it terminates only its owned
child tree and exits zero. Legacy panes still require Ctrl+D or the configured
closePane binding. First-run harness trust prompts are also user decisions;
Convoy returns an awaiting-user-consent card and never auto-accepts them.
How it works
Attach
https://convoy.bot/mcp.Run
onboardwith harnesses you already installed.Bind one thread at
--root; Convoy writes/reads one durableconvoy_id.Use
sendfor synapses. A send that names a live seat queues the body (delivery: queued,delivered: false); it does not type into the TUI and does not spawn a second--resume. Codex may usecodex queue. Drain withconvoy inbox --drainor the project hookconvoy inbox --hook-pretooluse(Grok PreToolUse, Claude PreToolUse + UserPromptSubmit). Hook files never bake an absolute interpreter path.Use
bring_up/openonly when you want visible interactive TUIs for seated neurons.
End-to-end example
# (after `python -m pip install .`, see Install)
# 1) Name installed harnesses and bind this root to one thread
convoy onboard --to grok --to claude --to codex --thread demo
# 2) Register seated neurons (session key + optional vendor resume token)
convoy seat --to grok --session-id seat-grok --worktree ../wt-grok --model gpt-5.6-sol --resume vendor-grok-uuid
convoy seat --to codex --session-id seat-codex --worktree ../wt-codex --resume vendor-codex-uuid
# 3) Dry-run bring-up shows native argv (Codex uses "resume" subcommand)
convoy bring-up --dry-run
# 4) Headless synapse (safe default)
convoy send --to claude "Summarize open payment retry bugs and propose a fix plan."
# 5) Optional live headless run in a fresh native session (no resume token)
convoy send --to codex --live "Draft unit tests for the retry planner."Development: PYTHONPATH=src python test/run.py (discovers test/demo/*_test.py).
License: MIT.
Cloudflare split hosting (static site + MCP proxy)
This repo includes a Cloudflare Worker config that serves the landing page/static files at the edge while preserving the existing Python MCP transport.
Config:
wrangler.jsoncWorker entry:
workers-site.mjsStatic assets directory:
src/convoy/siteProduction runbook:
docs/deploy-convoy-bot-mcp.md
Routing behavior:
/mcpand/mcp/*are proxied byte-for-byte toMCP_ORIGIN(the current Python MCP origin).all other paths are served from Worker static assets (
env.ASSETS.fetch(request)).
Production currently uses a Worker Route on convoy.bot/*,
MCP_ORIGIN=https://convoy.bot, and a proxied Cloudflare Tunnel whose ingress
is the separate Python process on 127.0.0.1:8788. A Worker deploy cannot
update that process. Follow the runbook to restart and prove the Python origin
first, then deploy the Worker only when its code/config/assets changed. Public
Gate 0 remains RED when the write gate is correctly closed; do not claim GREEN
from an updated build stamp or remembered tool count.
This server cannot be deployed
Maintenance
Related MCP Connectors
Eyes and hands on real Windows PCs — observe, click, type via Glasswarp API.
Develop, manage, and debug Railway projects, services, and deployments from within agents.
Securely control computers you explicitly pair through files, terminals, processes, screenshots, desktop UI/input, clipboard, browser automation, diagnostics, and document tools.
Operate Linux, macOS and Windows from your LLM. Every action runs through an auditable allowlist.
Related MCP Servers
- AlicenseAqualityDmaintenanceEnables management of visible, interactive terminal sessions across platforms (macOS, Windows, Linux, WSL). Supports creating, executing commands, capturing output, and managing multiple terminal windows simultaneously.51MIT
- AlicenseNot gradedqualityAmaintenanceA native Windows terminal multiplexer with MCP bridge for AI agents, enabling browser automation, multi-agent coordination, and terminal control.393MIT
- AlicenseBqualityCmaintenanceEnables stateful subprocess sessions (REPLs) and tmux-backed TUI program control for interactive tools like Python, r2, or shells.13AGPL 3.0
- FlicenseNot gradedqualityDmaintenanceEnables orchestration of multiple AI coding agents (Claude Code, Gemini CLI, etc.) in isolated Wezterm panes, allowing one agent to spawn, monitor, and communicate with others across projects.15 npm1-