Run a fleet of agents in your terminal.
claude, codex, anything with a CLI — each in its own virtual terminal, all driven from one place: a TUI, a browser, or the swarm command itself.
The agents get that same command, so they can talk to each other without you relaying messages.
go install github.com/emmanuel-deloget/swarm/cmd/swarm@latestRequires Go 1.25+ · Linux and macOS · swarm init writes you a config
swarm run starts the fleet, the TUI and the web remote. The header counts the fleet by state, so a prompt waiting on you is visible without hunting for it.
Features
What it gives you
swarm knows nothing about any particular agent. An agent is a command line — everything below follows from that.
One window for the fleet
A list with live state, the selected agent’s terminal beside it, and a mosaic view showing every agent at once.
A state per agent
Derived from what the agent prints:
working,idle— quiet long enough that it is probably waiting for you — plus whatever your regexps name, such asapprovalorerror.Input from anywhere
Type into an agent from the TUI, from
swarm inject, or from a browser. Send key presses. Stage a file or an image and inject its path.A message bus
swarm send dev-3 "…"reaches an agent whether you type it or another agent does. Push types it into the recipient’s prompt; pull leaves it forswarm inbox.Incoming webhooks
Declarative rules turn an HTTP delivery into a bus message, so the fleet reacts to a pull request or a ticket without you relaying it. Signature-checked, on its own port.
Remote control over HTTP
Token-protected, with no JavaScript terminal library: swarm already emulates the terminals and sends ready-made HTML, then only the lines that changed.
Agents that talk to each other
Every agent gets
swarmon itsPATH, already pointed at the running session, plus$SWARM_AGENT,$SWARM_PEERSand a shared directory.Real ptys, not pipes
Each agent runs in a real pty, so it behaves exactly as it would in your terminal: job control,
^C, terminal queries, the alternate screen.
How it works
One hub, one pty per agent
Everything that drives the fleet — the TUI, the swarm command, a browser — talks to the same hub. The hub owns the fleet, the events and the message bus.
A real pty per agent
Not a pipe: an agent gets a terminal, so job control,
^C, terminal queries and the alternate screen all work the way they do when you run it yourself.A terminal emulator kept in sync
swarm keeps a virtual terminal in step with each pty, which is what makes a snapshot of any agent possible at any moment — instead of replaying a byte stream that may start mid-sequence. The TUI renders it as ANSI, the web server as HTML lines, and sends only the ones that changed.
One socket, many callers
The control socket lives in
.swarm/<session>.sock. Your other terminals reach it, and so do the agents — which is why an agent can runswarm sendwith no arguments of its own.
The CLI
Drive the fleet from any terminal
Every command talks to a running swarm over a Unix socket — from another terminal, from a script, or from an agent.
Look at what the fleet is doing without stopping it. swarm screen prints what a terminal shows right now; swarm attach hands the window over until you press the detach key.
A target is an agent name, @group, @role, all, or a comma-separated list of those. Every command that acts on agents takes one.
swarm ls # the fleet and its state
swarm status @dev # more detail
swarm screen dev-1 # what that terminal shows right now
swarm attach dev-1 # take it over in this window
swarm logs dev-1 -f # recorded output, escapes stripped
swarm restart dev-3
Configuration
One file, and most of it optional
swarm init writes a starter swarm.yaml: one agent, nothing listening on a port, and every other setting present as a commented example. Uncomment what you need.
session: default # picks the control socket
workdir: . # default working directory
shared: .swarm/shared # where injected files land
defaults: # inherited by every agent
cols: 200
rows: 50
idle_after: 3s # quiet this long → "idle"
delivery: push # bus messages typed into the prompt
web:
enabled: true
addr: 127.0.0.1:7777
token: "" # empty → a fresh one at every start
groups: # usable as @dev anywhere
dev: [dev-1, dev-2]
agents:
- name: dev-1
role: dev # a role is a target too: @dev
command: [claude] # any argv; the only required field
- name: dev-2
role: dev
command: [codex]
- name: review-1
role: review
command: [codex]
delivery: pull # do not interrupt; it runs swarm inbox
Found by walking up
No -c flag? swarm walks up from the working directory trying swarm.yaml, swarm.yml, .swarm.yaml, .swarm.yml — so any subdirectory of your project works. There is no global config: a swarm belongs to a project.
Groups and roles are targets
A role is declared on the agent, a group in one line; both become targets. swarm send @dev "…" reaches the pair without you naming them, and adding a third agent to the group changes nothing else.
Patterns give an agent a state
A regexp matched against the tail of the agent’s screen. When it matches, the agent gets a badge, optionally raises an event, and can even be answered — only for prompts you would always answer the same way.
patterns:
- match: "Run this command\\? \\(y/n\\)"
state: approval
notify: true
reply: "y" # auto-answer — trusted prompts only
Webhooks
The fleet reacts without you
swarm knows nothing about GitHub or any other sender. A rule names conditions on paths into the delivery, and renders a message from the same paths.
Rules, not integrations
A path addresses the decoded JSON body — or a header, when it starts with header. — and walks objects and arrays: data.commits.0.message. A value matches exactly, or "*" for mere presence, or ~ followed by a regexp. Conditions are ANDed; every matching rule fires.
hooks:
enabled: true
addr: 127.0.0.1:7778
secret_path: .swarm/hook-secret
signature_header: X-Hub-Signature-256
rules:
- name: review-requested
when:
event: pull_request.review_requested
data.member_id: "6aa593d4-…"
to: review-1
message: "a review was asked of you on {data.repository}"
unmatched: # only when no rule matched
to: triage-1
message: "unhandled event ({event}) — worth a rule?"
to: is deliberately not templated. A payload must never choose which agent it wakes up — route by member with one rule per member, so the config decides what an identifier means and an unknown one wakes nobody.
Why nothing happened
A webhook that does nothing looks the same from outside whether it never arrived, was refused, matched no rule, or reached a stopped agent. Every delivery is recorded in full — and each rule says which condition failed and what was there instead.
The body is on one line so it can be pasted into a file and replayed offline with swarm hook test, which goes through the same matching code the listener uses. The digest covers the raw body and is checked before the payload is even decoded: an endpoint that accepts unsigned payloads once accepts them always.
Coordination
Agents that talk to each other
Every agent gets swarm on its PATH, already pointed at the running session — so it can look at the fleet and reach the others on its own.
- $SWARM_AGENT
- its own name
- $SWARM_ROLE
- its role
- $SWARM_PEERS
- the other agents
- $SWARM_SHARED
- a directory every agent can read and write
- $SWARM_SOCKET
- the control socket, used automatically
swarm ls # the others, and their state
swarm send review-2 "please review PR 42"
swarm send @dev -file report.md "findings"
swarm inbox -wait 30s # block until something arrives
swarm run writes .swarm/AGENTS.md describing the fleet and these commands — point your agents’ instructions at it and they can coordinate without you.
Remote control
The same fleet, in a tab
With web.enabled, swarm run prints a URL carrying a token. The page shows the agent list, the selected terminal — live, and you can type into it — a grid view, the event log, and a composer.
There is no JavaScript terminal library in it: swarm already emulates the terminals, so the server renders HTML lines and sends only the ones that changed. Uploading a file stages it and injects its path, exactly as swarm inject -file does.
- Past localhost, treat that URL as a shell on your machine.
- Set
web.read_only: trueif you only want to watch. - Put it behind TLS or a tunnel (
ssh -R,cloudflared) rather than binding0.0.0.0in the open.
Get started
Three commands to a fleet
Requires Go 1.25+ and a Unix-like system — Linux or macOS. There is no Windows build: swarm needs ptys, Unix sockets and process groups.
Install it
Or build from a checkout with
go build -o swarm ./cmd/swarm.go install github.com/emmanuel-deloget/swarm/cmd/swarm@latestWrite a config
One agent, nothing listening on a port, and every other setting present as a commented example. Then list your agents.
swarm initStart the fleet
Starts the agents, the TUI and the web remote.
--no-tuiruns it headless if you would rather drive it from the CLI or the browser.swarm run
MIT licence. Issues and pull requests welcome — CI runs the suite on Linux and macOS, plus go vet, gofmt, golangci-lint and govulncheck.