[](https://github.com/estejosh/ferryman/actions/workflows/ci.yml)
[](https://github.com/estejosh/ferryman/actions/workflows/container.yml)
[](https://github.com/estejosh/ferryman/pkgs/container/ferryman)
[](LICENSE)
[](COMMERCIAL.md)
**Self-hosted, local-first team coordination for a fleet of AI agents.**
Your agents coordinate by writing signed files into a folder that
[Syncthing](https://syncthing.net) carries between machines you own. There is no
server in the middle, no port to forward, no cloud account — and the
coordination lives in its own private repository, kept separate from the work
itself.
### Nothing to type
Download one file from [the latest release](https://github.com/estejosh/ferryman/releases/latest)
and double-click it:
| | |
|---|---|
| Windows | **Ferryman-Setup.cmd** |
| macOS, Linux | **Ferryman-Setup.command** |
It installs Ferryman, asks which folder to coordinate, and opens the dashboard. From
there it is a web page — there is no second step and no command to remember.
### Or from a terminal
```sh
# macOS and Linux
curl -fsSL https://raw.githubusercontent.com/estejosh/ferryman/main/scripts/install.sh | sh
cd your-project && ferry enable --email you@example.com # setup, all of it
ferry dashboard # everything else is here
```
Windows, in PowerShell:
```powershell
irm https://raw.githubusercontent.com/estejosh/ferryman/main/scripts/install.ps1 | iex
```
No Rust toolchain, no compile. Both scripts verify the release checksum and
install for the current user. Ferryman needs [Syncthing](https://syncthing.net/downloads/)
running to reach your other machines; it says so plainly if it can't find it.
**Or don't run it yourself.** [docs/INSTALL_PROMPT.md](docs/INSTALL_PROMPT.md) is a
block you paste into any coding agent — it installs Ferryman, enables the
project, wires the Syncthing folder and starts working without asking anything.
`ferry enable` never prompts under `--json` or when it is not attached to a
terminal, is safe to run twice, and reports in JSON. At a terminal it does ask one
question — whether to set up the web dashboard — because that is a decision with a
password attached and an agent cannot make it for you.
## Why this shape
Most tools for coordinating AI agents put a server in the middle. Everything
flows through it, it has to be reachable, and it has to be trusted with all of
it.
Ferryman doesn't. **Machines write files; a synced folder carries them.** A work
order, a result, a review — all of them are just files appearing in a directory
your machines already share. Nothing is "sent". There is no connection to
establish and nothing to be down.
```mermaid
flowchart LR
subgraph D["desktop"]
DF["channel folder"]
end
subgraph L["laptop"]
LF["channel folder"]
end
subgraph F["friend's box"]
FF["channel folder"]
end
DF <-. syncthing .-> LF
LF <-. syncthing .-> FF
DF <-. syncthing .-> FF
```
*Three machines, one synced folder, nothing in the middle. Nothing here has to
be up, reachable, or trusted.*
Two consequences worth caring about:
- **It works anywhere.** A laptop on cellular, a box at a friend's house, a
machine behind a router you don't control — if it can sync a folder, it's in
the fleet.
- **It stays private.** The channel is your own repository on your own machines.
No third party holds your agents' conversation.
## What you get
- **Signed everything.** Each agent has its own key, so every message, order,
result and review carries a fingerprint. On a team you can tell *which* agent
did what, not merely which machine. Keys are pinned on first sight: a machine
that has never met an agent trusts whatever the channel shows it first, and
refuses any later change to that key. So the signatures prove continuity, not
identity, until you check a fingerprint out of band — which is worth doing once,
per agent, when you add a machine you did not set up yourself.
- **Review and revision.** Accept the work, or send it back with notes. Revisions
are judgement, not failure — a job sent back five times has failed zero times.
- **Shared memory** the fleet agrees on — proposed by agents, approved before it
counts, so one confused agent can't poison what everyone believes.
- **An audit trail** of every decision, hash-chained and backed by private Git.
- **Approval gates** for anything that shouldn't happen unsupervised — including
from your phone over Telegram, bound to a hash of exactly what was approved.
- **Your phone as a terminal.** `ferry channel telegram` turns a message into a
signed order and sends the result back when a worker submits it, so the fleet
is reachable from the one device you always have. One Telegram group, a topic
per project — Ferryman builds the topics and remembers which is which.
- **A master, grants, and short-lived lease tokens.** Authority is explicit,
signed, and expiring — a leaked worker credential stops working on its own.
- **A web dashboard** to watch tasks, ledger, cost and learnings, and to
approve or send work back from a browser — and to set secrets once, sealed to
exactly the machines that should receive them.
- **MCP server (beta).** `ferry mcp serve --as ` lets Claude Desktop, Claude Code
or Codex join a project, catch up, take orders, talk and ask you for approvals, in
about 630 tokens of tool definitions. See [docs/MCP.md](docs/MCP.md).
- **Recovery you can rehearse.** Encrypted continuity packs, and a drill command
that proves you can come back from a wiped machine.
- **Opt-in sandboxing.** Run each worker inside a fresh podman or docker
container, with a network-egress policy (`net = none | open | `) — one
config line per project, at your direction.
- **A project cost estimator.** `ferry cost plan` sizes a project against
per-engine list rates (editable in a `rates.toml`, no rebuild needed) so you can
price work before committing to it. It is an **estimate from a token heuristic**,
not a meter reading, and the rates are hand-maintained constants — check them
against your provider's current pricing. `ferry cost project` is the other half:
it totals the usage workers actually recorded, per engine.
## Two repositories, on purpose
```
your-project/ <- the work. Ferryman never touches this.
your-project-ferryman/ <- the channel. Coordination and shared memory only.
```
Your agents already share the work repository. The channel carries the
conversation *about* the work — what to do, what got done, what needs changing,
what the fleet learned — never the work itself. That separation is the point:
Ferryman can't corrupt, expose, or even have an opinion about your code.
## Agents that check each other
Ferryman's protocol requires that an agent receiving a checkable claim — a bug,
a root cause, a proposed fix — **verify it against the real code before acting
on it**, rather than trusting the report. That rule came from running this
thing, not from a design document: it repeatedly caught agents confidently
reporting things that were not true, before the error spread downstream. If you
are going to let a fleet of models work unsupervised, this is the part that
matters.
## What it looks like

A real recording, not a mockup — replay it from
[`docs/assets/demo.cast`](docs/assets/demo.cast). Both agents here read and
write one folder, which is exactly what Syncthing gives each machine. Nothing is
running: no server, no daemon, no token.
## From your phone
```bash
export TELEGRAM_BOT_TOKEN=... # from @BotFather
export TELEGRAM_APPROVER_ID=... # your numeric user id; ask @userinfobot
ferry channel telegram --workspace ~/your-project-ferryman --agent you
```
Send a line and it becomes an open order; `/to ` addresses one
machine; `/status` and `/agents` read the channel back. Results arrive in the
same chat with their signature verdict.
For more than one project, point it at a map instead and it serves a whole
Telegram group — a topic per project, each wired to that project's channel:
```bash
ferry channel telegram --map ~/ferryman-comms/.tgferryman --agent you
```
The first run writes the map from the channels it finds, creates a topic for
each one, and writes down the ids — Telegram has no way to list topics, so that
file is the only record. See [docs/TELEGRAM_TOPICS.md](docs/TELEGRAM_TOPICS.md).
Only that one user id is obeyed — Telegram authenticates `from.id` server-side,
and the bridge refuses to start without an id to check, because a bridge that
started without one would take orders from whoever found the bot. Keep the token
in the environment or a mode-600 `EnvironmentFile`, never in the channel: it
syncs.
It writes the same signed artifacts `ferry channel order` writes, so it is not a
second control plane. Stop it and the fleet does not notice.
**Never type a credential into it.** A Telegram cloud chat is not end-to-end encrypted, so a
token typed into one is stored on Telegram's servers, syncs to every device signed into that
account, and stays in that history indefinitely. Nothing in the code can tell a task from a
token, so this one is on you.
Secrets themselves are meant to move — that is what sealing is for. Set them with
`ferry channel secret set`, or in the dashboard, and they are encrypted to the specific
machines that should receive them before they ever leave the machine that typed them. What
must not travel is the plaintext, and the bridge is the one path that cannot carry ciphertext.
## ferry-deadman — a sub-product in this repository
[`crates/ferry-deadman`](crates/ferry-deadman) is its own thing, built and tested
here but useful with no Ferryman anywhere: **timelocked succession for any git
repository.** It seals an archive of a repo — `git bundle --all` plus any files you
name — so that it becomes decryptable only after a future point in time, enforced by
the [drand](https://drand.love) threshold beacon rather than by a server, a company
or a person. While you keep running `ferry-deadman heartbeat`, the deadline keeps
moving away from you. The day you stop, the mathematics opens the envelope.
Ferryman's part is transport: a channel can carry the sealed artifact to your
successors' machines, and it is ciphertext the whole way. Neither half needs the
other. See its [README](crates/ferry-deadman/README.md).
## Documentation
| Guide | What it covers |
|---|---|
| [Running in a container](docs/CONTAINER.md) | podman and Docker, single or multi-project |
| [How the channel works](docs/COMMUNICATIONS.md) | delivery, failover, health |
| [MCP server (beta)](docs/MCP.md) | connect Claude Desktop, Claude Code or Codex |
| [Architecture](docs/ARCHITECTURE.md) | boundaries and design constraints |
| [Threat model](docs/THREAT_MODEL.md) | what it defends against, and what it does not |
| [Writing a worker (server mode)](docs/WRITING_A_WORKER.md) | the lease-based worker protocol behind `ferryman-server` |
| [Engine setup](docs/ENGINE_SETUP.md) | pointing the worker at Claude Code, OpenCode, Codex or anything else — including API keys via `credentials.json` |
| [Focus](docs/FOCUS.md) | telling the swarm which projects matter this month: a signed tier per project that shares out the improve budget and the workers' attention |
| [Dashboard team and agent access](docs/DASHBOARD_TEAM_ACCESS_MODEL.md) | how the dashboard models human teammates vs agents; what is enforced today vs previewed |
| [Getting started](docs/GETTING_STARTED.md) | install → first task walkthrough |
## Keeping a fleet on the same build
```sh
# in any repository you use Ferryman in
curl -fsSL https://raw.githubusercontent.com/estejosh/ferryman/main/scripts/ferry-up.sh | sh
```
Installs or updates `ferry`, attaches the repository if it isn't attached, and then
tells you where you stand: version **and commit** before and after, whether your
signing key is unchanged, and a signature check on every artifact. Safe to run twice,
and it never rotates a key — it prints the fingerprint before and after so you can see
that rather than take it on faith. Windows: `ferry-up.ps1`, same thing.
Run it on every machine, then compare the commit in `ferry --version`. If they differ,
they aren't running the same Ferryman — which is the cause of a whole class of problems
that otherwise look like bugs.
## Building from source
```sh
sudo apt-get install -y libdbus-1-dev pkg-config # Debian/Ubuntu (Linux only)
cargo build --release --workspace
cargo test --workspace
```
macOS and Windows use their native keychains and need nothing extra.
## Soak testing — please break it
**Ferryman is in soak testing, and that is a request, not a disclaimer.**
It works. It is also new, and the last set of problems in software like this is
only ever found by other people running it on machines we do not have, for longer
than we have. So: run it, and tell us what happened.
```sh
ferry soak # counts and category labels. Prints; sends nothing unless you ask.
```
Then [open an issue](https://github.com/estejosh/ferryman/issues/new?template=soak_report.md)
or email `lafamiliahale@gmail.com`. **"I ran it for a week and nothing broke" is a
real report and we want it** — it tells us which platforms and shapes of fleet are
holding up, which is not knowable any other way.
If you would rather not copy and paste, set `FERRYMAN_SOAK_URL` and run
`ferry soak --send`. That is per invocation and opt-in twice over: there is no config
key that sends on its own, no timer, and a downloaded release has no endpoint set.
`ferry soak --dry-run` prints exactly what `--send` would transmit, from the same
value, so the two cannot disagree.
`ferry soak` carries no file paths, task text, prompts, results, agent output or
credentials. It is built out of values whose *type* cannot hold them rather than
filtered afterwards, and the whole thing is
[one readable file](crates/ferryman-ops/src/soak.rs) if you would rather check that
than trust it.
Security issues go through
[private vulnerability reporting](https://github.com/estejosh/ferryman/security/advisories/new),
not public issues.
## Current status — honest about where this is
**Solid.** The channel, the Syncthing transport, project attachment, approval
gates, shared memory, the audit trail, the dashboard, master/grants, lease
tokens, continuity packs and the container. Messages, orders, results and
reviews all travel as files and cross networks without anything reachable. All
covered by the test suite.
**Working, but young.** The agentic loops — `ferry agent run` and
`ferry agent review`, where one agent picks work up and another judges what
comes back — work end to end and are new. They have been exercised against a
real agent CLI across a shared channel, not only in tests, but not yet run for
weeks by strangers, which is the only thing that finds the last problems.
**Not built yet.** PostgreSQL, RBAC, and workflow graphs are design targets, not
implementations.
**Scope orders to a handful of tool calls.** This is the single most useful thing
to know before your first task. "List the first three `unwrap()`s in this file"
completes reliably; "list every place that could panic in this file" often does
not. Bounded, specific orders are what the loop is good at, and they are also what
a reviewer can actually judge. With Claude Code as the engine there is a known
failure behind this rule, written up under [Known
issues](#known-issues) with the eight causes already eliminated.
### Known issues
Listed because you will hit some of these, and finding them written down beats
discovering them. Every one is something we know about and intend to fix; none of
them loses work or leaks anything.
- **A machine first set up with an older Ferryman can sit idle forever after
upgrading.** Its Syncthing still writes each channel to the old
`/.ferryman/ferryman` while the worker reads the channel its own
`.ferryman/bridge.toml` names, so orders arrive on the machine but in a folder
nobody reads. The issuer sees "not delivered" while Syncthing reports 100%
synced. Moving the folders by hand fixes it, and the order of the steps matters:
see
[Upgrading](docs/UPGRADING.md#known-issue-a-machine-set-up-with-an-older-ferryman-never-sees-new-work).
- **Costs read as `$0.00` for engines that don't account.** Workers now record
the token usage an engine prints and `ferry cost project` totals it per
engine; Claude Code's JSON output is accounted today, and prose-only engines
stay at zero rather than a guess. `ferry cost plan` remains what it was: an
estimate from a token heuristic, labelled as one.
- **`ferry agent status` answers "why is nothing happening"** - worker liveness,
the task it holds with heartbeat age, and the exact claim-gate decision
(paused, outside working hours, you're typing, memory floor) with the setting
that causes it.
- **Engine prices and quality scores are hand-typed constants.** `ferry cost rates`
prints a table of list prices with no as-of date, an unrecognised engine is priced
at a mid-range commercial rate — including a local model, which costs nothing —
and the `quality` column is a static hint, not a measurement, wherever no
outcomes have been recorded yet. Measured confidence *is* real and always shows
its sample size (`0.67 · 1/1 accepted`); the priors beside it are opinion.
- **`ferry ask` reports its sources as signed without verifying them.** The ledger
half is genuinely verified; agent-profile and task claims are read from the
channel and attributed by filename. On a fleet you control this is cosmetic. Do
not rely on it as provenance until it verifies, and prefer
`ferry channel log` / `ferry channel tasks`, which do.
- **An addressed order reports as `claimed` before anyone picks it up.** `--to
fang` shows as claimed by fang immediately, whether or not fang has
started, because the holder is the assignee and no claim file is required. So
"waiting for that machine" and "that machine is working" currently look the same.
- **`checkin = "off"` in `agent.toml` does nothing.** PRIVACY.md mentions it; no
code reads it. Nothing sends automatically anyway — the check-in only ever runs
when you run `ferry license checkin` — so leaving the URL unset is the control
that actually works.
- **The SBOM omits the tray.** `sbom.cdx.json` covers the workspace, and
`ferryman-tray` is excluded from the workspace with its own lockfile, so its
dependencies are missing from it. The tray is optional and not installed by the
install scripts.
- **`ferry bench --timeout-secs` is accepted and ignored.** The benchmark uses a
fixed 300s per task.
- **External MCP tool output is not marked as third-party** when it reaches an
agent's prompt. Treat any MCP server you connect as something whose output can
influence your agents, and prefer read-only ones.
- **Claude Code in `-p` mode aborts on substantial tasks.** Pointed at
`@anthropic-ai/claude-code` as the engine, small tasks complete and larger ones fail
with `Execution error` on stdout — `is_error: true`, `terminal_reason:
aborted_streaming`, `stop_reason: tool_use`, typically at turn 8–12. It is
intermittent and gets likelier the more work a task needs: "list the first three
`unwrap()`s in this file" completes reliably, "list every place that could panic in
this file" mostly does not. Eliminated as causes, each tested directly: tool
permissions, the git worktree the worker runs in, the scrubbed child environment,
Ferryman's stall watchdog and timeout, output volume (a run producing 6,709 output
tokens succeeded while one producing 2,094 failed), the publishing notice in the
prompt (identical failure with and without it), and the `renice +10` applied to the
child. Failed runs cost nothing — they abort before or during the first request. The
practical rule until this is understood: **scope orders to a handful of tool calls**.
Bounded, specific tasks are what this is good at anyway.
- **Windows has less test coverage than Linux and macOS.** CI runs all three, but
several suites are Unix-only, and the two most recent platform bugs were both
Windows-only and both found by running on a real machine rather than in review.
- **Nobody maintaining Ferryman runs it on a Mac day to day.** CI builds and tests
macOS on every push and it is green, but green tests are not the same as use. If
you run Ferryman on a Mac, a soak report is worth more from you than from anyone
else.
Fixed in `0.4.0` and worth knowing if you ran an earlier build: a worker could kill
its own running task in an unrecoverable retry loop; a peer could forge another
machine's signing key through several CLI paths; the container runner put
credentials on the process argument list; the audit ledger reported itself tampered
after ordinary two-machine use; and shell task sources never worked on Windows. See
the changelog.
**Sandboxing is yours to turn on.** By default a worker runs with the privileges
of the account that started it. Point `sandbox` at an image in `agent.toml` (or
`ferry enable --sandbox IMAGE`) and Ferryman runs each worker inside a fresh
podman or docker container instead, with a network-egress policy so a hermetic
task can be cut off from the network entirely. The container path is built; the
per-platform bind-mount wrinkles (SELinux, macOS, WSL) are still being smoothed
out. If you don't direct it to sandbox, it doesn't.
## What Ferryman sends
Nothing, on its own. There is no timer, no background sender, and no daemon that
phones home. `ferry license checkin` is a command you run, and a downloaded
release has no endpoint configured at all — it sends nothing until you set
`FERRYMAN_CHECKIN_URL` yourself.
When you do run it, this is the entire payload:
```json
{
"deployment_id": "a41f...",
"emails": ["you@example.com"],
"seats": 1,
"computers": 2,
"mobile_devices": 0,
"over_limit": false,
"version": "0.5.3",
"sent_at": "2026-08-12T09:00:00Z"
}
```
`deployment_id` is a random number generated on your machine — not derived from
your hardware, hostname or network. There is no field for a task, a prompt, a
file name, or anything your agents produce, so no amount of misuse can put one
there. `ferry license checkin --dry-run` prints the exact bytes and sends
nothing. [PRIVACY.md](PRIVACY.md) walks the payload field by field.
The same is true of `ferry soak`: it sends a report only if you set
`FERRYMAN_SOAK_URL` **and** pass `--send`, per invocation. Never automatically.
## License
Ferryman is **source-available** under the [Usufruct License (UFL) v2.2](LICENSE),
Operational Scope: **Seat-Limited** (tag `UFL-S-1a`): free for any non-production
use, and free in production for up to **2 people, on 2 computers and 2
phones/tablets** — production use beyond that threshold requires a paid,
separate license from the Licensor.
**Agents are unlimited and never counted.** One person running twenty agents
across two computers is one Seat. Beyond the free threshold it is $60 per
additional seat per year, dropping with volume — priced per human, not per
machine or agent. See [COMMERCIAL.md](COMMERCIAL.md).
Free production use asks for a contact email. **Ferryman never sends anything on
its own.** There is no timer and no background sender: `ferry license checkin` is
a command you run, and a downloaded release has no endpoint configured at all, so
it sends nothing until you set `FERRYMAN_CHECKIN_URL` yourself. Whether you are
inside the free tier is computed on your own machine — run `ferry license status`
and it counts your seats, computers and phones locally and lists what it counted.
See [What Ferryman sends](#what-ferryman-sends) for the whole payload.
UFL is not on the SPDX license list; cite it as `LicenseRef-UFL-2.2-S`. See the
canonical license text and generator at
[estejosh/UFL-Usufruct-License](https://github.com/estejosh/UFL-Usufruct-License).
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md) and [SECURITY.md](SECURITY.md). Agents and
operators working *on* Ferryman itself should read [the operator
brief](docs/OPERATOR_BRIEF.md) first.
## Acknowledgments
Ferryman is provider-neutral and runs no models itself. The reference agent
worker performs inference through an external agent CLI. This project was first
piloted on **[honemesh.net](https://honemesh.net)**, credited for the inference
work that shaped it.
Ferryman bundles [Syncthing](https://syncthing.net) (MPL-2.0), unmodified — see
[THIRD_PARTY.md](THIRD_PARTY.md).