Set up
Runs on your computer. Pick your agent and operating system.
Before you start
- Node.js 20 or newer
{
"mcpServers": {
"ghostfleet": {
"command": "npx",
"args": [
"-y",
"ghostfleet-cli"
]
}
}
}same format Cursor, GitHub Copilot CLI, Junie, JetBrains AI Assistant, Augment (Auggie CLI), Factory Droid, Kiro, Devin CLI +12
Values in angle brackets are placeholders. Restart Claude Desktop after editing its config.Claude Desktop docs
README
synced Oct 21, 2026Run a fleet of Claude Code agents in parallel, from one terminal. Each agent gets its own git worktree — cut, branched, dependency-linked and booted in one keystroke — and you get one screen that shows what every one of them is doing.
A ghost fleet is a fleet of autonomous, unmanned vessels under one command — agents working with nobody in the seat, and one control plane steering them.
Why another one of these
Orchestrating agents is easy to demo and hard to trust. The parts that took real debugging — and that most wrappers get wrong:
| problem | what ghostfleet does |
|---|---|
| "is it working?" — the transcript's mtime says idle mid-generation, and busy when a background write lands | reads the live pane, the same signal you read |
| a worker needs you, you handle it, the card stays red forever | a need-you older than the session's own activity is treated as spent |
every project has a session called master, so their statuses collide | status is scoped by the fleet's socket, not by name |
| a dispatched prompt silently lands in the input box without submitting | dispatch waits for the paste, submits, then verifies a turn actually started |
| one account: 5 agents drain the budget 5× faster and all stall together | a non-Claude governor meters usage and parks workers at the ceiling, resuming on reset (it can't be the lead — the lead stalls too) |
| one laptop: 5 agents running tests can fill the process table and wedge the machine | the same governor watches the kernel's own memory-pressure level and the process table, and parks workers before the OOM killer does |
| you already run agents by hand in a dozen panes | fleet-adopt finds those conversations and rebuilds them as one fleet |
A worktree you can actually work in
git worktree add gives you a directory. An agent needs more than that before it can do
anything, and the gap is where the fiddly parts live — so fleet-spawn closes it:
| why it matters | |
|---|---|
node_modules symlinked from the main checkout | the worker can run lint, typecheck and tests on its first turn instead of waiting out an install |
| a dev-stack slot allocated | two workers don't both try to bind port 3000 and one of them silently lose |
| the task recorded in a manifest | fleet-worktrees shows what each tree is for, which is how a lead rebuilds its map after a restart instead of guessing |
| reuse before create | it refuses to cut a new tree while a free one is sitting there, and lists them — so the disk doesn't fill with abandoned checkouts |
| a session started in it, on the agent you picked | a worktree with nobody in it isn't a worker |
The branch is cut from your local ref, not the remote tip, so a worker never misses
work you committed but haven't pushed. And fleet-spawn refuses to run from inside a
worktree: a session already in one is a leaf, and spawning there would put a second tree
beside the one you're standing in rather than starting a worker.
Recycling an existing tree onto fresh work is one command — fleet-spawn <name> --reuse <worktree> --branch <new> --from <base> cleans it and checks out the new branch — which is
the usual case once a fleet has been running for a while. What gets recycled is the
folder, never the session: a worker that finished a task has that task all through its
context, so it is retired with fleet-stop --reclaim and the next task starts a new
conversation. fleet-send refuses to hand a finished, shipped worker a new brief, and says
to spawn instead.
Documentation
The README is the pitch, the install and the shape of the thing. Everything you look up
while using it lives in docs/, one file per question, because three reference sections
had grown to 78% of this page and a README is not where you go to check a keystroke.
| docs/SHORTCUTS.md | Every key, and what answers it. Start at §0 — the handful that covers almost everything — then the exhaustive sections: inside a session, the Projects screen, the grid, the stack, the fleet-* commands, and the behaviors that are not guessable from the UI |
| docs/ORCHESTRATION.md | A lead session driving workers. Dispatching briefs to siblings, watching them, unblocking them, reusing worktrees before making more, and metering cost with the governor |
| docs/OPERATIONS.md | The special cases. Adopting Claude sessions you started by hand, the phone client, notifications, work-vs-personal profiles, staying awake, where a worktree goes, the two ways to register a project, which fleet a worker lands on, and updating Claude Code under a live fleet |
| docs/mobile.md · web/README.md | The phone client — the design argument, and the client itself |
| docs/jarvis.md | Jarvis, one session above every fleet — experimental, off by default; fleet-experimental enable jarvis turns it on. ghostfleet jarvis, the digest it reads instead of polling, the confirm-list the tools enforce, conversation mode on the phone (transcribed on the Mac), and its fresh start once the conversation grows long |
| docs/stack-view.md | Why the stack is nested attaches and not join-pane, and what was measured to find out |
| docs/multi-agent-sessions.md | Running codex, opencode, agy and cursor beside claude — the measured capability matrix (hooks, MCP, skill, resume) and what picking a non-default agent costs |
| docs/attachments.md | Sending a photo from the phone: what was measured per agent, why the bytes are converted on the Mac rather than the phone, and the two recommendations the build overturned |
| docs/agent-council.md | Would a second agent checking the first one's work reduce iteration? Measured against 3,494 real turns and this repo's own history: misread intent is the smallest correction category, three quarters of the rest are found by a human looking at a screen, and a browser-based verifier would have passed the bug it was built for |
| docs/ROADMAP.md · docs/IDEAS.md | What is next, and what is only an idea |
| CHANGELOG.md | What changed between releases, and whether it is a reason to upgrade |
| CLAUDE.md | For working on ghostfleet: how to deploy a change, what the tests cover, and the failure modes that have bitten more than once |
Prerequisites
| requirement | why |
|---|---|
git | worktrees are the isolation model — a worker is a checkout on its own branch. fleet-spawn refuses to run without it |
claude (Claude Code) | what a session runs by default, and what the pane detectors are written against — missing? the installer offers Claude Code's native installer (into ~/.local/bin, no sudo) |
node (v18+ to run, v20.19+ to build from a clone) | the grid is a zero-npm-dependency Node TUI, and v18 runs it. The phone client is built from web/src with vite, which needs 20.19 — that applies only to a clone, because the published package ships web/ already built. Debian and Ubuntu package 18.x, so a clone there needs a newer node first; the installer checks the version up front and says which |
tmux | the hidden substrate that keeps sessions alive in the background — missing? the installer offers to install it for you (see below). With no terminal attached it has nobody to ask, so a piped or CI install prints the command instead — pass --yes there and it installs without prompting |
jq | the installer wires the hooks and MCP entries with it, and the status hook parses its payload with it. macOS 26 already ships it (/usr/bin/jq); anywhere it is missing the installer offers to install it |
| macOS, Linux, or Windows via WSL2 | sessions are tmux servers, and tmux is POSIX-only — see the native-Windows note below |
codex / opencode / agy / cursor-agent (optional) | alternative agents, chosen per worktree on the w form. Their pane signals are detected separately — see docs/multi-agent-sessions.md |
$EDITOR (optional, default nvim .) | what Ctrl-n's editor tab opens. Any editor works — override with CLAUDE_FLEET_EDITOR. With neither set and no Neovim new enough for LazyVim (0.11.2+), the installer offers the official Neovim release (under ~/.local) and, only where there is no ~/.config/nvim yet, the LazyVim starter |
tailscale (optional) | only for the phone client, and only off-LAN: it is how fleet-serve is reachable without exposing a port — see docs/mobile.md |
zellij (optional) | not required, but the included layout gives you one pane that frees Ctrl-s/arrows from its own bindings |
terminal-notifier + AeroSpace (optional, macOS) | for clickable notifications that jump straight to the fleet — see Notifications |
(Native Windows isn't supported and won't be: sessions are tmux servers, and tmux is POSIX-only. Under WSL2 it's just Linux and works the same — docs/windows.md is the whole path, from a bare Windows machine to an open fleet.)
Install
npx ghostfleet-cli # installs, no clone needed
ghostfleet demo # see it working, on three throwaway projects
On a fresh Linux box there is no npx to run that with. A stock Ubuntu 24.04 image
ships none of node, npm, git, tmux, jq or curl — measured, not assumed — so the line above
is command not found and nothing tells you which of the six is the one you need. Install
them first, and the rest of this page applies unchanged:
sudo apt-get update && sudo apt-get install -y nodejs npm git tmux jq curl
(That nodejs is 18.x, which runs the fleet perfectly well. It cannot build the phone
client, which matters only if you clone — see the node row above.)
ghostfleet demo is the fastest way to find out whether you want this. It creates three
scratch git repos under ~/gf-demo, registers them in a separate demo profile, and
opens the real control plane on them — the same screens the GIFs above were recorded
against. It is additive and it never repairs: anything already there is reused and said
so, and if it finds something it cannot safely reuse it stops and tells you what it found
rather than guessing. Your own profiles are untouched; rm -rf ~/gf-demo ~/.config/ghostfleet/projects.demo ~/.claude-demo removes every trace.
Every screen works before you log anything in — the Projects picker, a project's session grid, the new-worktree form, the stack. What needs a login is an agent taking a turn: Claude Code keeps credentials per config dir, so the demo profile has its own, and a session you open will sit at its login prompt until you do this once:
CLAUDE_CONFIG_DIR=~/.claude-demo claude # then /login
An empty pane before that is the login prompt, not a broken fleet.
Ready to point it at your own work? ghostfleet opens your projects — and with none
registered yet it walks you through picking a folder, naming it, and starting the first
session, rather than showing you an empty screen.
And put it on your phone:
fleet-phone # what is left to do, and the one command that does it
The same fleet as an installable app — every session as a chat, the real pane one tap
away, and a notification when a worker is blocked, so a permission prompt gets answered
from your pocket. fleet-phone reports which of the three steps you have done (configure,
enrol a passkey, run the daemon) and prints the next one; it never runs any of them for
you, because each opens a port, writes a config or spends a passkey. It reaches the phone
over Tailscale — fleet-serve refuses a wildcard, a LAN address or a public one before
the socket opens, because this endpoint runs commands. See docs/mobile.md
for the design and the threat model.
Optional, and local only: the Mac's voice. The phone can read replies aloud with
Kokoro running on your machine — better English,
real Spanish, chosen sentence by sentence — instead of the phone's own voice. It is ~350 MB, so
./install.sh asks and the default is No (--yes does not answer it). Add it any time:
fleet-jarvis voice --kokoro --install # verified download + a pinned Python 3.12 venv; re-run to repair
Without it nothing is missing: the phone reads with its own voice. fleet-phone and
fleet-jarvis status say whether it is installed, not installed or broken.
docs/mobile.md has the details.
Two things that cost people time, so they are in that command's output too: the passkey is enforced server-side, so until a phone is enrolled the app sits on its lock screen and the API answers 401 — and an installed iOS PWA resumed from the app switcher does not pick up a new client version. The shell is served cache-first, so reopening is a resume, not a navigation; swipe the app away and relaunch.
Prefer to clone the repo (e.g. to develop against it)?
git clone https://github.com/PabloG55/ghostfleet.git
cd ghostfleet
./install.sh # add --verbose to watch every step
Both run the same install.sh — npx ghostfleet-cli just fetches the package and runs it
for you, so nothing is left checked out afterwards. The command it installs is
ghostfleet, not ghostfleet-cli: the npm package name and the binary name are separate,
and only the package name had to change.
The package is ghostfleet-cli, and ghostfleet on npm is someone else's.
ghostfleet@0.0.2 was published by an unrelated project ten days after this one took the
name, pitched as "fleets of disposable AI agents in your own cloud" — close enough that
npx ghostfleet looks right and installs a stranger's package. Nothing here can be done
about that, so the suffix is load-bearing: type -cli.
Cloning is also what you want if you intend to edit ghostfleet itself: cf-sync syncs the
runtime from a real repo, and an npx cache is not one.
If you already have a clone, be careful running the npx installer over it. install.sh
records where to sync FROM in <runtime>/.source, and cf-sync with no argument reads it —
so an installer run from an npx cache used to repoint that at the cache, after which every
cf-sync in your clone copied the cache into the live runtime. Nothing errors and the sync
still prints synced runtime; your edits just quietly stop arriving. The installer now
keeps a recorded clone when it is running from a cache (it says so: pointer stays on your clone: …), so this only bites an older install. To fix one, do either:
cd /path/to/ghostfleet && ./install.sh # re-install from the clone — it re-records it
cf-sync /path/to/ghostfleet # or just repoint it once; later `cf-sync` remembers
Check it any time with cat ~/.local/libexec/ghostfleet/.source. An install run from a
clone always repoints — that includes a re-install from the same clone, a moved clone, and a
second clone — because the guard only fires when the copy being installed from could not serve
as a sync source at all.
Missing tmux? The installer detects it, works out the right package manager for your
OS (Homebrew, apt, dnf, yum, pacman, zypper, or apk), and asks before running anything
— it never installs (or sudos) without you confirming. No package manager recognized,
or you say no? It just tells you the command to run yourself.
Installing with no terminal (CI, a Dockerfile, curl | bash)? There is nobody to ask,
so the default is to install nothing and print the command — and tmux is not optional
here: a fleet session is a tmux server, so an install that skipped it leaves a grid
that cannot start anything. Consent up front instead:
npx ghostfleet-cli --yes # or: ./install.sh --yes, or CLAUDE_FLEET_YES=1
That installs the missing dependencies without prompting, using the same package manager
it would have offered — sudo included on Linux, which is why it is opt-in and never the
default. With --yes, an install that ends without tmux exits non-zero instead of
looking successful. Without it, nothing is installed unless you say yes at the terminal.
With npx, the flag goes after the package name. --yes (and -y) is npx's own
flag too, so npx --yes ghostfleet-cli is consumed by npm and this installer is invoked
with no arguments at all — it then refuses exactly as if you had never passed it
(measured on npm 11.18; it notices and says where the flag belongs, but the install still
skips tmux). In a Dockerfile or a CI config, the env var is the form nothing can
misparse:
ENV CLAUDE_FLEET_YES=1
RUN npx ghostfleet-cli
The installer stages the runtime — it copies bin/, hooks/, mcp/, skill/, and
layouts/ out of the repo into ~/.local/libexec/ghostfleet (override with CLAUDE_FLEET_HOME)
— then symlinks the commands (ghostfleet, claude-here, cf-sync, and the fleet-* helpers)
into ~/.local/bin pointing at the staged copy; wires the status + notification hooks into
every Claude config dir it finds (~/.claude, ~/.claude-*, backing each up), plus a PreToolUse
guard that stops Claude Code's built-in EnterWorktree from walking a fleet session off its own
checkout (it is appended to PreToolUse, so hooks you already have there survive); registers the
fleet MCP server into each config dir's .claude.json via claude mcp add -s user (Claude Code
reads MCP from .claude.json/.mcp.json, not settings.json) and once, globally, for
codex, opencode, agy and cursor-agent, which keep one config each and have no per-profile
equivalent (agy and cursor also get the event bridge and the skill, under ~/.gemini/config/ and
~/.cursor/; an edited ~/.cursor file keeps its original beside it as <file>.pre-ghostfleet);
installs the ghostfleet-orchestrate skill; and links the zellij layout. Re-run any time; it's idempotent.
macOS guards ~/Documents, ~/Desktop, and ~/Downloads with TCC. An app that hasn't been
granted Documents folder / Full Disk Access — notably ClaudeCode.app — gets
Operation not permitted when it tries to execute anything stored there. So if you cloned this
repo under ~/Documents, running the fleet CLI, the event hook, or the MCP server directly from
the repo breaks the instant such an app hosts your session (symlinks don't help — exec follows them
back into the protected folder). ~/.local is not TCC-protected, so the installer runs
everything from the staged copy there and the repo stays purely for development.
After you edit the repo, run cf-sync to push those edits into the live runtime (it copies the
runtime dirs from the recorded source repo into ~/.local/libexec/ghostfleet; the PATH symlinks,
hook, and MCP already point there, so no re-link is needed). The alternative — granting ClaudeCode.app
Full Disk Access — also works but can reset on app/OS updates; staging survives updates.
The screens
One terminal window (or one zellij pane) — ghostfleet is the whole control plane:
flowchart LR
Projects["Projects\npick a project · + add project"] -- "⏎ enter" --> Master["Master Claude\nthe lead — spawns &\ncoordinates workers"]
Master -- "Ctrl-s" --> Grid["The grid\napi · api-1 · api-2 …"]
` (backtick) always steps back exactly one level, from anywhere — session → grid → master
→ Projects — no matter which shortcut you took down.
- Projects — pick a project and
⏎drops you straight into its Master Claude. Each project has its own hidden tmux server (cf-<project>) holding its sessions. - Master Claude — the lead session that spawns worktrees and coordinates workers.
- The grid — a card per Claude session (status · branch · last message), plus a card for every worktree that has no live session yet. Every session keeps running in the background, so agents work in parallel while you jump between them.
- The stack (
tfrom the grid) — several of those sessions on screen at the same time, in split panes, including sessions from different projects.
zellij --layout fleet attach -c fleet # one zellij session runs everything
# or just run `ghostfleet` in any pane
What makes it different: it runs beside your setup instead of taking it over. Sessions live on per-project tmux servers ghostfleet manages for you, so you never lose a session by closing a window — and if you use zellij, it stays a single pane instead of commandeering your multiplexer (Claude Squad, ccmanager) or pushing you to a web dashboard (Omnara).
How it works
- One tmux server per project (
tmux -L cf-<project>) is the hidden substrate. It keeps each Claude session alive in the background and handles attach / detach / resize — the battle-tested part. You never interact with tmux directly. ghostfleetis a tiny loop: it runs the grid, and when you pick a card it hands off totmux attach. Detach and the loop redraws the grid. Node never owns PTYs.fleet-grid.mjsis a flicker-free Node TUI (zero npm deps). Each card joins three sources: the tmux session list, the per-session status file that the Claude hooks write to~/.claude/fleet/, and the last assistant line from the transcript in~/.claude/projects/. Worktrees with no live session are read straight fromgit worktree list. Card order is its own fourth source (<sock>.order, rewritten by⇧hjkl) — and the single one, sinceghostfleetcountsCtrl-f <p> <s>throughfleet-grid.mjs --orderrather than re-deriving it, andfleet-cyclereads the copy mirrored onto the tmux server as@cf_order.claude-hereis what each session runs, so sessions resume by checkout. If a conversation was registered as a Claude Code background agent (e.g. it was backgrounded, or created by a bg workflow), a plain--resumeis refused with "currently running as a background agent" — soclaude-heredetects that (viaclaude agents --json) and resumes with--fork-session, branching an interactive copy with full history. The fork becomes the newest conversation, so the next open resumes it cleanly.
Status per card: ● NEEDS YOU (permission/question) · ◆ working · ✓ ready · · idle ·
· FREE (worktree, no session).
Config
ghostfleet sets these per project; each spawned session inherits them (used by the grid, hooks,
and fleet-* tools):
| Env var | Meaning |
|---|---|
CLAUDE_FLEET_SCOPE | The project name (shown in the header; scopes checkout discovery). |
CLAUDE_FLEET_ROOT | The project's root folder (where its checkouts/worktrees live). |
CLAUDE_FLEET_SOCK | The project's tmux socket, cf-<project>. |
CLAUDE_FLEET_SOCK_FORCE | Pins fleet-spawn to that socket, beating $TMUX (which is inherited by anything a fleet session launches). Same as fleet-spawn -s. See Putting a worker on a specific fleet. |
CLAUDE_CONFIG_DIR | The account/config dir for the project's profile. |
CLAUDE_FLEET_DIR | Per-session status files ($CLAUDE_CONFIG_DIR/fleet). |
CLAUDE_FLEET_YOLO | 0 to require permission prompts in sessions (default: bypass). |
CLAUDE_FLEET_AWAKE | display to keep the screen on as well as the machine; off to inhibit nothing. Default on. See Staying awake. |
CLAUDE_FLEET_NOTIFY_LEAD | 1 to push worker done/need-you events to the lead instead of it polling. See docs/SHORTCUTS.md for the per-project/per-session settings-page toggle and the full precedence rules. |
ghostfleet <project> --plain prints a one-shot, non-interactive table for that project (scripts).
Updating
ghostfleet update # releases: follows the branch your clone is on
ghostfleet update --branch staging # follow development instead (--branch main to go back)
ghostfleet update --yes # ...and restart every session without asking
One command, because the code is not the only thing that is old. In order, it:
- brings the code up — in a clone, fetches and fast-forwards the tracked branch
(
mainfor releases,stagingfor development); from npm, runsnpx ghostfleet-cli@latest. It never merges and never stashes: uncommitted edits or local commits that origin lacks are yours, so it stops and says which it found. - runs the install, so new commands, hooks and the MCP registration land too.
- restarts the phone daemon — a launchd job whose label ends in
ghostfleet.fleet-serveis kickstarted. Run by hand instead? Restart it by hand. - offers
fleet-restart --all— every session's MCP server is the one it started with, for as long as it lives. This relaunches every session on the machine onto its own conversation, so it asks (or takes--yes). - tells you to relaunch the phone app — swipe it away and open it again. Reopening it from the app switcher is a resume, and a resume keeps the client it already had.
A clone from before the repository was recreated cannot be updated. Its history shares
no commit with origin's, so there is nothing to fast-forward from — ghostfleet update
says so and prints the git clone to run, and changes nothing. Keep the old clone until
anything only it holds has been copied out.
An npm install is told when a newer version is out: the Projects screen prints one line.
The check reads a cache (~/.config/ghostfleet/npm-latest, refreshed in the background at
most daily), so startup never waits on the network.
Uninstall
Claude — in each config dir (~/.claude, ~/.claude-*): remove the fleet hooks blocks
and the ghostfleet entry under mcpServers from settings.json (or restore a
settings.json.bak.*), delete the ghostfleet key from .claude.json's mcpServers
(claude mcp remove -s user ghostfleet does it), and delete
skills/ghostfleet-orchestrate.
codex, opencode, agy and cursor — these are registered ONCE, globally, not per profile (see the
comment above register_codex_mcp in install.sh for why that is correct rather than a
shortcut):
codex mcp remove ghostfleet # ~/.codex/config.toml
tmp=$(mktemp); jq 'del(.mcp.ghostfleet)' ~/.config/opencode/opencode.jsonc > "$tmp" \
&& mv "$tmp" ~/.config/opencode/opencode.jsonc # opencode's MCP entry
rm -f ~/.config/opencode/plugin/ghostfleet-event.js # opencode's event bridge
for f in hooks mcp_config; do tmp=$(mktemp); jq 'del(.ghostfleet, .mcpServers.ghostfleet)' \
~/.gemini/config/$f.json > "$tmp" && mv "$tmp" ~/.gemini/config/$f.json; done # agy's bridge + MCP
rm -f ~/.gemini/config/skills/ghostfleet-orchestrate # agy's skill
tmp=$(mktemp); jq '.hooks |= with_entries(.value |= map(select((.command // "") | contains("cursor-fleet-event.sh") | not)))' \
~/.cursor/hooks.json > "$tmp" && mv "$tmp" ~/.cursor/hooks.json # cursor's bridge
tmp=$(mktemp); jq 'del(.mcpServers.ghostfleet)' ~/.cursor/mcp.json > "$tmp" && mv "$tmp" ~/.cursor/mcp.json # cursor's MCP
rm -f ~/.cursor/skills/ghostfleet-orchestrate # cursor's skill
Then delete the symlinks in ~/.local/bin, rm -rf ~/.local/libexec/ghostfleet, and
tmux -L cf-<project> kill-server for any live fleets.
Leave any of the four registrations behind and it points at a runtime you have deleted —
each one names ~/.local/libexec/ghostfleet/mcp/fleet-mcp.mjs, and an agent that starts one
after the runtime is gone fails per tool call rather than at startup.
Support, and what to expect
This is one person's daily driver that happens to be public. Bugs are very welcome — especially on Linux and WSL2, which I do not run every day and where most of the surprises have been environmental rather than logical. There is no SLA, and features are often declined, which is not a verdict on the idea: every option here has to keep working across four agents, three platforms and several tmux versions.
The issue form asks for your OS and tmux -V before it asks what went wrong, and that
ordering is deliberate — see CONTRIBUTING.md for why, and for the one
rule worth knowing before sending a patch: a test that cannot fail proves nothing.
License
MIT © 2026 Pablo Garces