Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

CLI reference

Usage: ferry [OPTIONS] <COMMAND>
CommandSummary
initWrite ferry.toml and report what this machine supports
baseBuild an immutable warm base that new agents fork from
spawnCreate an agent: a git worktree plus a forked warm target dir
runRun a command in an agent’s build context
shellStart an interactive shell in an agent’s build context
envPrint an agent’s paths and environment for wiring into agent tools
listList bases and agents with their status and disk usage
rmRemove an agent’s worktree and build state
gcDelete bases that no agent uses and that are not current
doctorCheck platform capabilities and print fixes for what is missing
shimPrint the directory holding Ferry’s cargo shim
hookHook handlers for agent tools (reads the tool’s JSON on stdin)
setupChange the system so Ferry’s namespace sandbox works (asks first)
helpPrint this message or the help of the given subcommand(s)

Global options

These work with every command, before or after it.

OptionMeaning
-C <DIR>Run as if ferry was started in DIR
--jsonPrint machine-readable JSON on stdout
-h, --helpPrint help (-h prints a summary)
-V, --versionPrint version

Commands run against the git repository containing the current directory (or the -C directory). Outside one, doctor still reports the machine-level checks plus a failing repository check, and the hook handlers use the directory named in their JSON input; every other command fails.

Output, JSON and exit codes

Data goes to stdout; progress, warnings and errors go to stderr. Colour is used only on a terminal. Errors look like this:

error: no warm base exists for this repository
  hint: run `ferry base` to build one

with caused by: lines in between when there is an underlying error.

--json is accepted by every command. It switches init, base, spawn, env, list, rm, gc, doctor (also with --apparmor-profile), shim and setup apparmor to a single JSON document on stdout; the shapes are listed under each command. Ferry is pre-1.0, so fields may still change between releases. --json has no effect on run and shell, which replace themselves with your command, or on the hook handlers, which speak the agent tool’s protocol.

Exit status is 0 on success and 1 on error. doctor exits 1 when a check fails. run and shell exit with the status of the command they run.

ferry init

Usage: ferry init [OPTIONS]

Write ferry.toml and report what this machine supports.

OptionMeaning
--forceOverwrite an existing ferry.toml

Writes the default configuration to ferry.toml in the root of the main checkout, then prints the same plan as ferry doctor. Without --force, an existing file is left alone with a warning.

JSON: { "config": <path>, "written": <bool>, "doctor": <doctor report> }.

ferry base

Usage: ferry base [OPTIONS]

Build an immutable warm base that new agents fork from.

OptionMeaning
--rev <REV>Commit to build (any git revision) [default: HEAD]
--coldBuild from scratch instead of seeding from the newest compatible base
--watchKeep running and build a base each time --rev moves to a new commit (not with --cold)
--interval <SECS>With --watch: seconds between polls of --rev [default: 2]
--gcWith --watch: run ferry gc after each new base

Checks out the commit into the store, runs the warm commands, and makes the base current, so new agents fork from it. If a base with the same commit and fingerprint already exists, it is made current without building. Otherwise, unless --cold is given, the new base is seeded from the newest ready base with the same fingerprint, so only what changed is rebuilt. The seed must have the same manifests, lockfiles, Cargo configs, toolchain files, Cargo flag variables and CARGO_INCREMENTAL, and agree on whether CI or TF_BUILD is set. Otherwise its warm build must have been observed ([base] prune, the default): the units the new build does not use are then deleted, and the success line says how many (pruned N units). Build output goes to stderr.

Warnings are printed when the namespace sandbox is unavailable (builds then run without canonical paths), when a Cargo config above the repository (or an untracked one in it) will not apply to Ferry’s builds, when the warm commands wrote files into the source tree, which agents will not have (build scripts writing outside OUT_DIR), when clippy is not installed (the generated clippy command is skipped), when the generated clippy command failed (the base is kept), and when a refresh seeded across such changes was rebuilt from scratch because Ferry could not tell which units the warm commands used.

JSON: { "base": {...}, "reused": <bool>, "warnings": [...] }, where base has id, commit, created_unix, build_secs, freshness, isolation, toolchain, commands (the ones that ran), command_secs (seconds per command), incomplete (clippy commands that failed), ferry_version, dirtied, remap_paths, unit_inputs (the hash seeding compares), seeded_from, observed (Ferry saw every cargo call of the warm build) and pruned ({ "units", "bytes" } deleted from the seed’s units, or null).

ferry base --watch

Runs in the foreground until interrupted (run it in a terminal tab, a systemd or launchd user service, or a devcontainer’s postStartCommand), so the current base follows --rev (default HEAD; --rev origin/main follows fetches; a revision that names no commit at startup is refused). It polls the revision, and once a new commit has been seen on two polls in a row it runs ferry base --rev <commit> in a child process. Polls are skipped while a rebase, merge, cherry-pick, revert or bisect is in progress. A build for a commit that fails keeps the previous base current and is not retried until the revision moves again; a build is never cut short when the revision moves, the newest commit is built next. Only one watcher runs per repository; a second one is refused. With --gc, unused bases are deleted after each new base, which matters on filesystems without reflinks, where every base holds a full copy of the target dir.

JSON: one object per line, with event set to building (commit), built (commit, report: what ferry --json base prints), failed (commit, error), collected (report: what ferry --json gc prints) or gc-failed (error).

ferry spawn

Usage: ferry spawn [OPTIONS] <NAME>

Create an agent: a git worktree plus a forked warm target dir.

Argument or optionMeaning
<NAME>Agent name (letters, digits, ‘-’, ‘_’, ‘.’)
--from <REF>Revision the agent’s branch starts at [default: HEAD]
--path <DIR>Where to create the worktree (default: the worktree directory, see [agent] worktree-dir)
--branch <BRANCH>Branch name (default: the configured branch prefix plus NAME)

Creates the branch and worktree, stamps the files that are identical to the base, and forks the base’s target dir. It never builds. The base is the current one if it is at --from, else any ready base at that commit, else the current base; files that differ from the chosen base are rebuilt on the first build. Fails if the agent, the branch or a non-empty worktree directory already exists, or if there is no base. A failure rolls back what the spawn created: the worktree, the branch if it still points at --from, and the fork.

Without --path, the worktree goes in the worktree parent directory (see Configuration), which is not inside the store unless you configured it that way. A relative --path is relative to the current directory. The directory must not exist or must be empty.

JSON: { "agent": {...}, "backend_reason": <string>, "fork": { "files", "bytes", "elapsed_secs" }, "elapsed_secs": <number>, "warnings": [...] }, where agent has name, state, base, from, branch, worktree, backend, isolation, freshness, created_unix, stamp and origin.

ferry run

Usage: ferry run [OPTIONS] [NAME] -- <COMMAND>...

Run a command in an agent’s build context.

Without NAME, uses the agent whose worktree contains the current directory.

ArgumentMeaning
[NAME]The agent
<COMMAND>...The command and its arguments, after --

Enters the agent’s sandbox and replaces itself with the command. The working directory is the current directory if it is inside the agent’s worktree, and the worktree root otherwise; either way it is a host path. PATH starts with the shim directory, and FERRY_AGENT and FERRY_WORKTREE are set. Every cargo the command runs, directly or from scripts, goes through the shim and builds in the agent’s target dir.

ferry run a1 -- cargo test -p parser
ferry run a1 -- claude
ferry run -- cargo build      # inside a1's worktree: NAME comes from the directory

ferry shell

Usage: ferry shell [OPTIONS] [NAME]

Start an interactive shell in an agent’s build context.

Like ferry run [NAME] -- $SHELL (or /bin/sh when SHELL is unset). Without NAME, uses the agent whose worktree contains the current directory.

ferry env

Usage: ferry env [OPTIONS] [NAME]

Print an agent’s paths and environment for wiring into agent tools.

Without NAME, uses the agent whose worktree contains the current directory. Prints shell lines you can eval:

export FERRY_WORKTREE=/home/you/.local/share/ferry/worktrees/myproject-d5c4fd3a48754db4/a1
export PATH=/home/you/.cache/ferry/bin:"$PATH"
# cargo in /home/you/.local/share/ferry/worktrees/myproject-d5c4fd3a48754db4/a1 now builds in agent a1's sandbox; other commands: ferry run a1 -- <command>

It never prints CARGO_TARGET_DIR: agent tools pass their environment to sub-agents working in other worktrees, and the shim picks the target dir per call instead.

JSON: { "name", "worktree", "branch", "base", "backend", "isolation", "build_target_dir", "host_target_dir", "shim_dir", "run_prefix" }. build_target_dir is the target dir as builds see it (the canonical path in the namespace sandbox). host_target_dir is the fork’s host path for the reflink and copy backends and null for overlay, whose target exists only inside the sandbox. run_prefix is ["ferry", "run", <name>, "--"].

ferry list

Usage: ferry list [OPTIONS]

List bases and agents with their status and disk usage.

Bases: * marks the current base; BUILT is its age (or incomplete); DISK its size; USED BY the number of agents forked from it. Agents: BASE (with (old) when it is not the current base), FORK (backend), SANDBOX (namespace or none), DISK, STATUS and WORKTREE.

DISK for an agent is the build state it holds beyond the base: the overlay upper layer, or for reflink clones the files rewritten since the fork (shared extents cannot be seen through stat), or the whole copy. STATUS is running while any process started through ferry run, ferry shell or the cargo shim for the agent is alive (JSON busy, with the pids Ferry can attribute in running), incomplete when spawning did not finish (remove it with ferry rm), worktree missing, or idle.

JSON: { "bases": [...], "agents": [...] }. A base has id, ready, current, meta, agents and disk_bytes; an agent has the spawn agent fields plus base_is_current, worktree_exists, private_bytes and busy and running (PIDs).

ferry rm

Usage: ferry rm [OPTIONS] <NAME>

Remove an agent’s worktree and build state.

Refuses if the worktree has uncommitted changes or processes are still running in the agent’s sandbox. The branch is kept if it has new commits.

OptionMeaning
--forceDiscard uncommitted changes and ignore running processes

Untracked files count as uncommitted changes, except the Cargo.lock that spawn copied from the base when the repository does not track one (as long as it is unchanged). Processes started for the agent are detected on every backend. The branch is deleted only if it still points at the commit the agent started from; otherwise Ferry keeps it and says how many commits it carries.

JSON: { "name", "worktree", "branch", "branch_deleted", "branch_commits", "rescued_ref" }, where rescued_ref names the ref that keeps commits a --force removal found on no branch (otherwise null).

ferry gc

Usage: ferry gc [OPTIONS]

Delete bases that no agent uses and that are not current.

OptionMeaning
--dry-runShow what would be deleted

First removes abandoned agents created by the Claude Code hook: clean worktree, no new commits, nothing running, and not git worktree locked by Claude Code. Agents created with ferry spawn are never removed. Then deletes every base that is neither current nor used by an agent, including incomplete ones left by interrupted builds, and skips bases that are being built.

The human-readable output lists reaped agents and removed and skipped bases.

JSON: { "removed": [...], "busy": [...], "kept": [...], "reaped_agents": [...], "warnings": [...], "freed_bytes": <number>, "dry_run": <bool> }.

ferry doctor

Usage: ferry doctor [OPTIONS]

Check platform capabilities and print fixes for what is missing.

OptionMeaning
--apparmor-profilePrint an AppArmor profile that allows this ferry binary to use user namespaces (Ubuntu 24.04+)

Prints one line per check (✓ ok, · information, ! warning with a fix: line, ✗ failure), then the plan: which fork backend agents will use and whether their first edit can reuse incremental state. See Platform support for every check.

--apparmor-profile prints the profile for the binary’s resolved path, and warns on stderr if that path, or a directory above it, is writable by a non-root user; it refuses paths with characters AppArmor reads as patterns. With --json it prints { "binary", "profile", "warning" }. See AppArmor on Ubuntu.

JSON: { "checks": [{ "name", "status", "detail", "fix" }], "capabilities": {...}, "plan": {...} }. capabilities has os, kernel, store_fs, reflink, userns, overlay, sysctls and probe_label (the namespace probe’s AppArmor label, such as ferry (enforce), or null); plan has backend, backend_reason, isolation, freshness and incremental_reuse.

ferry shim

Usage: ferry shim [OPTIONS]

Print the directory holding Ferry’s cargo shim.

Put it first on PATH (export PATH="$(ferry shim):$PATH"): every cargo call made inside an agent worktree then runs in that agent’s sandbox with its own target dir, and cargo elsewhere runs unchanged.

The directory is <store parent>/bin; ferry shim creates it and points its cargo link at the running ferry binary. Run it inside a repository.

JSON: { "shim_dir": <path> }.

ferry hook

Usage: ferry hook [OPTIONS] <COMMAND>

Hook handlers for agent tools (reads the tool’s JSON on stdin).

CommandMeaning
claude-worktree-createClaude Code WorktreeCreate: spawn an agent, print its worktree path
claude-worktree-removeClaude Code WorktreeRemove: remove the agent unless it has uncommitted work
claude-session-startClaude Code SessionStart: put the cargo shim on PATH via $CLAUDE_ENV_FILE

See Claude Code for what each handler reads and does.

ferry setup apparmor

Usage: ferry setup apparmor [OPTIONS]

Let ferry create user namespaces on Ubuntu 24.04+: install it to a root-owned path and load its AppArmor profile, with one sudo call.

OptionMeaning
-y, --yesRun without asking; the plan is still printed
--dry-runPrint the plan and exit without changing anything (not with --yes)
--uninstallUnload and remove Ferry’s profile instead
--bin-dir <DIR>Where to install ferry when its current path cannot be profiled (default /usr/local/bin)

It first checks whether anything is needed. It does nothing, and exits 0, on macOS, when the running ferry can already create user namespaces, when the kernel has no kernel.apparmor_restrict_unprivileged_userns sysctl, and when that sysctl is 0. It refuses, and exits 1, when run as root, inside a Ferry agent or a container, without the AppArmor 4.0 userspace, or when a file in /etc/apparmor.d that Ferry did not write already defines a profile named ferry or unpriv_ferry. It never reads the current repository or its ferry.toml.

Otherwise it prints, on stderr: why the setup is needed (the sysctl, the probe’s failure and its AppArmor label), each step, what the profile allows, the full profile, the root script and the exact sudo command. The steps, each listed only when needed:

  1. Install the running binary as <bin-dir>/ferry, owned by root with mode 0755, unless it already sits at a root-owned path a profile can name or <bin-dir>/ferry is already a root-owned copy of it. A copy your user owns, or a symlink, is replaced. Root copies the binary from /proc/<pid>/exe, so nothing your user can write is copied.
  2. Write /etc/apparmor.d/<path with . for /> (for /usr/local/bin/ferry, usr.local.bin.ferry), check that it parses, and remove older Ferry profile files such as /etc/apparmor.d/ferry.
  3. Load it with apparmor_parser -r; AppArmor loads it again at every boot.

Then, without sudo, it checks that the profiled binary can create a user namespace, points the cargo shim at it, and warns if the first ferry on PATH is another copy.

SituationWhat happens
stdin is a terminal, no --yesasks Run it? [y/N]; only y or yes runs it
--yesruns without asking; with stdin not a terminal, sudo gets -n, so a needed password fails at once
stdin is not a terminal, no --yesrefuses: refusing to run sudo without confirmation: stdin is not a terminal
--json without --yesrefuses, like stdin not being a terminal
--dry-runprints the plan and exits 0

Running the upgraded copy is the upgrade path: after cargo install upgrades ~/.cargo/bin/ferry, run ~/.cargo/bin/ferry setup apparmor. It installs the new binary and leaves the profile alone. A bare ferry setup apparmor with /usr/local/bin first on PATH runs the old installed copy. That copy is set up already, so it reports already-done and says when ~/.cargo/bin/ferry is a different build. With --uninstall it unloads and removes every Ferry profile file (one sudo call, same confirmation rules); it leaves the installed binary and any /etc/apparmor.d/local/ferry in place and says so.

JSON: { "action": "install" | "uninstall", "outcome", "reason", "binary", "profile_path", "steps": [...], "command": [...], "verified": { "userns", "overlay", "label" } | null, "warnings": [...] }. outcome is one of not-applicable, not-needed, already-done, refused, planned (a dry run), done and failed; command is the full sudo argv. Exit status is 1 for refused and failed. See AppArmor on Ubuntu.

Ferry under other names

The ferry binary also runs as:

  • cargo, through the shim link in <store parent>/bin. It resolves the agent from the working directory and runs the real cargo (the next cargo on PATH that is not Ferry) in that agent’s sandbox, or unchanged outside agent worktrees. It exits with status 127 if no real cargo is found.
  • ferry-rustc, a RUSTC_WORKSPACE_WRAPPER used only by the experimental remap-paths mode.