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.
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-mainAuthorize each engineer with the public key their sail init --main printed:
sail fde add mady --role member --key "ssh-ed25519 AAAA... sail-sync@madybox"Join a node:
sudo sail init --main <main-ip> # provision, install sail-api, generate this box's sync key,
# and print the fde add line to run on main
sail sync # pull specs, projects and shared files from main
sail sync status --json # stored health, last success, and any failure reasonHand a person their box in Mast with one string. On the box, as the account sail-api runs as
(with sudo where that is root):
sail fde pair ada --host 34.1.2.3 --email ada@example.com # prints a connect code, once
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 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.
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.
# 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 syncedsudo 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.