Sail turns bare-metal servers into isolated, spec-driven dev environments where AI coding agents do the work. Each project gets a hard-isolated container. The team's specs and project definitions live in a synced database. Agents are dispatched against those specs with rollback safety and cross-agent review.
Sail is not a coding agent. It is the layer around them: the environments they run in, the specs they pick up, the reviews they pass, and the shared state a team works from. It does for coding agents roughly what Kubernetes does for containers, orchestrating the work rather than performing it.
One static binary (Java 25, GraalVM native-image), no runtime dependencies.
curl -fsSL https://raw.githubusercontent.com/standardapplied/sail/main/install.sh | bashsail upgrade replaces /usr/local/bin/sail and converges the database; sail upgrade --binary <file> installs a local build the same way. Linux (amd64) runs a full host. macOS
(arm64) runs as a thin client that drives a remote host over SSH. Upgrade main first, then
nodes: sync refuses a peer behind the fleet floor before any data moves and names the remedy.
An org runs one main box. It holds the team's specs and project definitions in a SQLite control plane and is the source of truth. Every engineer also has their own box. A box pointed at main is a node: it pulls specs, project definitions and shared files from main, and pushes its own work back.
There is no GitHub in this loop and no separate board. The database is the board, and sail sync moves it over a locked-down SSH gateway using public-key auth, with three-way conflict
resolution so no one's work is overwritten. Compute is never scheduled across boxes. The star
coordinates state, not execution.
Spec bodies and shared files are content-addressed blobs, deduplicated and streamed; sail sync status reports health and sail sync gc compacts history. Three verbs remove work: archive
keeps everything, delete keeps history so sail spec restore brings it back, and prune
erases a spec everywhere, leaving one audit row. Retention is opt-in through a retention block
in main's host.yaml.
Main's database is the org. Back it up on main, and keep the copy off the box:
sudo apt-get install -y sqlite3
sudo sqlite3 /var/lib/sail/sail.db ".backup /root/sail.db.copy"A main set up again from that copy is the same main to every node. On the new main, run sudo sail init --as-main, then put the copy in place:
sudo systemctl stop sail-api
sudo install -m 660 -g sail /root/sail.db.copy /var/lib/sail/sail.db
sudo rm -f /var/lib/sail/sail.db-wal /var/lib/sail/sail.db-shm
sudo sail migrate
sudo systemctl start sail-apiEach node then re-pins main's new host key (sudo ssh-keygen -R <main-ip>, then sudo sail init --main <main-ip>; compare the fingerprint it prints with sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub on main), and its next sudo sail sync pushes whatever it has
not pushed yet; work main took after the copy and lost with it is not rebuilt from the node. A main
set up empty is a new org: every node refuses to sync with it. A copy taken before 0.54.0 counts as
a new org too, so take a fresh one after upgrading.
Mast, the desktop client, is a thin client of a box: specs, rooms and reviews over its API and event stream, terminals and files over its pty host.
Stand up the main. One idempotent command provisions the box, installs the control plane and declares it the source of truth:
sudo sail init --as-main --handle ada --name "Ada Lovelace" --email [email protected]--handle makes you main's operator: the FDE dispatch picks this box's specs for, added to the
roster as admin with --name and --email, or with the git identity the box already has. It is
asked for when left out; a re-run keeps the handle the box already has and refuses another.
--name and --email also become the box's git identity when it has none (they are asked for
when left out, and init stops before any step without them), so the projects it applies commit as
you. An identity the box already has is kept.
Authorize each engineer with the line their sudo sail init --main printed. It carries the
fingerprint of main's host key that their box pinned, and main refuses the line when that is not
its own key:
sudo sail fde add mady --role member --key "ssh-ed25519 AAAA... sail-sync@madybox" \
--main-host-key SHA256:...Join a node:
sudo sail init --main <main-ip> --name "Mady M" --email [email protected]
# provision, install sail-api, pin main's SSH host key,
# generate this box's sync key, and print the fde add line
sudo sail sync # pull specs, projects and shared files from main
sudo sail sync status --json # stored health, last success, and any failure reasonA box set up by sudo sail init is root's: sail-api runs as root, whoever invoked sudo. Run
sail there with sudo; another account's plain sail is refused and names the sudo command to
run instead.
Hand a person their box in Mast with one string. On the box, as the account sail-api runs as
(root, through sudo, on a box sudo sail init set up), for the box's own FDE (its sync handle):
sudo sail fde pair ada --host 34.1.2.3 --email [email protected] # prints a connect code, once
sudo sail fde unpair ada # revokes itThe code is everything Mast needs: where the box is and its host key, a login key made for this
pairing, and an API token for ada. It logs in as the account sail-api runs as, which sees
every project, container and secret on the box, so a box mints it for its own FDE and no other.
It is a secret that opens a shell on the box, so send it as you would a password. It lasts a year
unless --ttl-days or --no-expiry says otherwise, and its login and its token end together.
The pairing's key also becomes the workstation key the box's containers trust; a box that
already has one refuses unless --as-workstation-key says to replace it. Unpairing takes the key
back out of the login, the workstation key and every container.
A client, or any other engineer, gets a box of their own: set it up with
sudo sail init --main <main>, grant it its projects on main (sudo sail fde grant <handle> <project>), and pair on that box. A node pairs its own FDE as main's roster has it, so the
roster options (--name, --email, --role) are main's.
A scope changes in place on main: sudo sail fde update <handle> --scoped --grant <project>
narrows an FDE that joined unscoped, keeping its keys, tokens and sessions, and --unscoped
lets it see every project again. On its next sync its box holds what the new scope admits and
nothing else: a project it no longer holds leaves with its container, its directory and any
unpushed work, and so does the name and email of every FDE outside its projects.
Each project the box applies opens with a conversation already waiting: sudo sail project apply leaves the box's FDE a home room, <project>--<handle>, with the project's agent in it
(agent.member in the definition picks another installed agent, model or mode).
On a Mac or other thin client, sail client <host> points the local CLI at a box. Commands are
forwarded over SSH; no control plane runs locally.
A project is one definition: runtimes, services, repos and agent config. The database is the
source of truth and the on-disk sail.yaml is a copy. sail project apply makes the container
match the definition from any starting state: an absent container is provisioned, a stopped one
started, a running one converged in place. Run it again any time.
sail project init # author a definition
sail project apply web # make it real: create, start or converge, one verb
sail project edit web # change the definition, saved to the catalog and synced
sail project connect web # print SSH config for your editorThe synced definition is identity-free. Per-engineer fields are placeholders (${GIT_NAME},
${GIT_EMAIL}, ${SSH_PUBLIC_KEY}) resolved from your own box at provision time, so a
teammate's project commits as you and trusts only your key. On a box sudo sail init set up, the
git identity is root's, the one sudo sail init --name … --email … gave it.
# sail.yaml (name, resources and image are the only required fields)
name: web
resources: { cpu: 4, memory: 12GB, disk: 150GB }
image: ubuntu/24.04
runtimes: { jdk: 25, node: 22 }
git: { name: ${GIT_NAME}, email: ${GIT_EMAIL} } # per-developer, never synced
repos:
- { url: "https://github.com/acme/web.git", path: web }
services:
postgres: { image: postgres:16, ports: [5432] }
agent:
type: claude-code
guardrails: { max_duration: 4h, max_idle: 20m, action: snapshot-and-stop } # builds and fix runs
review_pipeline:
guardrails: { max_duration: 45m, max_idle: 20m, action: stop } # reviewers
ssh:
authorized_keys: [ ${SSH_PUBLIC_KEY} ] # per-developer, never syncedThe container's user is always dev (uid 1000, /home/dev): every agent, shell and hook runs
as it. ssh.user may be omitted and, if set, must be dev; apply refuses any other before it
creates anything. Apply records each provisioning step for the definition it ran with, so an
edit made after a failed or finished run is provisioned again from the first step, each step
installing only what is missing.
sudo sail project demo spins up a bundled zero-config demo (an Outline wiki) end to end.
Specs are the unit of work: a status, an assignee and dependencies, filterable across the whole
team. sail spec manages them on a box; agents inside a container use an in-container spec
CLI over a bound socket, so there is one source of truth and no sync glue.
sail spec create --project web --title "Stripe webhook" --assignee mady --depends-on oauth
sail spec board --project web # who is on what, and what is ready
sail spec dispatch --project web # pick the next ready spec, launch the agent, watch itdispatch honors dependencies and assignee, snapshots the container for rollback, creates the
work branch and launches the agent with its generated context. When the build stops, the review
loop runs: each stage of agent.review_pipeline has a reviewer that checks the branch, a fix
agent addresses its findings, and the loop repeats up to max_iterations before escalating to
a person. Sail is agent-agnostic across claude-code and codex, so one agent can implement and
another review. Every run is held to its lane's guardrails: a wall-clock max_duration, a
max_idle stall window and an action; an agent past a limit is killed in its container and
the spec's room says why. A passing review parks the spec in awaiting_merge. Sail never talks
to the forge: merge the pull request there and close the loop with
sail spec update <id> --status done.
sail agent review <project> shows every attempt's iterations and findings; sail agent logs <project> --review (or --fix) follows the latest reviewer's or fix agent's run live. sail spec create --from-review <spec-id> drafts a follow-up spec from a review's open findings, and
marking that follow-up done resolves them.
Every spec has a room. Agents post progress there, read it before deciding, and ask with
spec comment <id> --question --body <text>, which pages the engineer on the board.
The loop is sail's: a build, then review stages, then a fix when a stage fails. What the
agents are told comes from two prompts sail writes and no project changes: the work prompt,
read by the build and by the fix run (the spec, the room, the findings, commit and push, a
pull request, CI green), and the judge prompt, read by every review stage (the spec, what to
review, the verdict contract). How this codebase is built, tested and verified is the
repository's own AGENTS.md to say; sail writes no context file.
The one agent-facing text a project writes is a stage's brief: a paragraph saying what
that stage judges, in the definition (sail project edit web). A stage with no brief judges
under sail's default (the spec first, then correctness, security and tests). A brief that
orders a skill makes a validation stage without new machinery:
agent:
review_pipeline:
stages:
- name: review
- name: validate
brief: |
Run the project's e2e skill against this branch and report every failure as a
finding, with the command and its output as evidence. Pass only on a clean run.
gate: all_clearA skill is a folder with a SKILL.md and optional scripts, installed where both harnesses
look for skills and invoked when the agent judges it relevant: a runbook you would hand a new
hire. Share one from a local folder and sail installs it whole, for every harness the project
installs, at provisioning, at project apply and before every launch:
sail project skills add ./e2e --project web # .sail/skills/e2e/<every file>
sail project skills ls --project web # name, files, ok or invalid: <why>
sail project skills show e2e --project web # its SKILL.md
sail project skills rm e2e --project web # the folder goes at the next launch or applySail ships one skill, spec-board, so an interactive session finds the spec CLI.
- ARCHITECTURE.md: the design and its contracts, each row naming the tests that prove it.
- CONTRIBUTING.md: building, the quality gates, migrations, releases.
- AGENTS.md: the working conventions for people and agents in this repository.
- Every command has
--help; state-mutating commands take--dry-run, and all take--json.