Skip to content

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@latest

Requires 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 as approval or error.

  • 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 for swarm 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 swarm on its PATH, already pointed at the running session, plus $SWARM_AGENT, $SWARM_PEERS and 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.

TUIbubbleteaswarm CLIunix socketbrowserHTTP + WShubfleet · events · message busdev-1pty + VTargv: claude, codex, …review-1pty + VTargv: claude, codex, …docs-1pty + VTargv: claude, codex, …
  1. 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.

  2. 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.

  3. 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 run swarm send with 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.

shell
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.

swarm.yaml
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
Every key, its default, and what it does

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.

swarm.yaml
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
shell
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: true if you only want to watch.
  • Put it behind TLS or a tunnel (ssh -R, cloudflared) rather than binding 0.0.0.0 in 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.

  1. Install it

    Or build from a checkout with go build -o swarm ./cmd/swarm.

    go install github.com/emmanuel-deloget/swarm/cmd/swarm@latest
  2. Write a config

    One agent, nothing listening on a port, and every other setting present as a commented example. Then list your agents.

    swarm init
  3. Start the fleet

    Starts the agents, the TUI and the web remote. --no-tui runs 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.