Ferry

Commands

This page has the --help text of each Ferry command and subcommand, from the Ferry source. Run ferry <command> --help to get the text of your version.

The ferry sherlock commands exist only when the Sherlock integration is on and sherlock is on the PATH.

ferry

Usage: ferry [options] [command]

Ferry keeps a remote Linux agent box in the same shape as this machine.

Use ferry init to record a Tailscale host or OpenSSH destination and seed the
private snapshot.

Ferry never copies logins. Vendor sessions stay on the machine that created
them. Ferry starts a login on the box and you finish it in a browser here.

Options:
  -V, --version                  output the version number
  --box <name>                   select a box of the config; repeat it to select
                                 more boxes
  --json                         print JSON on stdout: one result envelope, or
                                 one event for each line for watch, tunnel, and
                                 expose. Progress goes to stderr
  -h, --help                     display help for command

Commands:
  init [options]                 Record a Tailscale host or SSH destination,
                                 seed the snapshot, and convert this machine
  install [options]              Install the supported agent tools on the
                                 configured box
  update [options]               Update the agent tools on the configured box
                                 and on this machine
  uninstall [options]            Remove Ferry's local state and restore paths
                                 changed by init
  auth [options] [provider]      Start a login on the configured box without
                                 copying credentials
  sync [options]                 Publish the snapshot and apply it to the
                                 selected boxes, or to all boxes
  history [options]              List the recent snapshot commits and the paths
                                 each one changed
  revert [options] <commit>      Undo one snapshot commit on this machine and on
                                 all boxes
  move [options] <path>          Continue a project on a box, on this machine
                                 with --from-box, or on another box with both
  adopt [options] <skill>        Copy a skill that an agent wrote on a box to
                                 this machine
  tunnel [options] [ports...]    Open box ports on this machine until Ctrl-C,
                                 list the ports that listen on the box, or
                                 follow the ports of ferry expose
  expose [options] <command...>  Run a command on the box and announce its port
                                 to ferry tunnel --follow
  status [options]               Inspect link, snapshot, managed paths, and box
                                 logins without writing
  doctor                         Check SSH, Tailscale, snapshot access, linger,
                                 and services, and print a fix for each failure
  integrations                   List the integrations of each box, whether each
                                 one is enabled, its parts, and the local app
                                 versions
  sherlock                       Add Sherlock database connections that tunnel
                                 through a box
  tools                          List the tools, the version policy of each one
                                 and of each box, and the versions on this
                                 machine
  watch                          Watch the portable set and sync accepted
                                 changes
  menubar                        Install or remove the macOS menu bar app that
                                 shows what needs action on the boxes
  self-update                    Update Ferry on this machine to the latest
                                 release.

                                 Ferry updates in the same way as it was
                                 installed: with npm, or with the
                                 release installer in the directory of this
                                 binary. It restarts installed
                                 watch and tunnel services that point at this
                                 Ferry. On macOS, it also updates
                                 an installed release menu bar app. It writes
                                 the Ferry agent skill of the new
                                 version to ~/.agents/skills/ferry, unless ferry
                                 init --no-skill turned it off
                                 or the skill folder has local changes. Then run
                                 ferry update to put the new
                                 version on the boxes.

                                 A release is visible before its build attaches
                                 the files. When the latest
                                 release does not have its files yet, Ferry says
                                 that the release is not ready
                                 and does not start the update. This is not an
                                 error. Run the command again
                                 some minutes later.

                                 On a terminal, each command also asks to update
                                 when a newer release is
                                 there. Ferry reads the latest release at most
                                 once a day. It does not ask
                                 with --json, with CI set, or with
                                 FERRY_NO_UPDATE_CHECK=1, and it does not
                                 ask for a release that does not have its files
                                 yet.
  whoami                         Print the role of this machine: the operator
                                 machine or a Ferry box.

                                 On a box, Ferry also prints the box name from
                                 the last ferry sync, and the
                                 parts of the generated instruction file
                                 ~/.ferry/box/AGENTS.md in their
                                 order: the Ferry header, the per-box
                                 instructions from
                                 ~/.ferry/boxes/<name>/AGENTS.md on the operator
                                 machine when the box has
                                 them, and the shared ~/AGENTS.md. The operator
                                 machine is the source of
                                 truth. On a box, change a Ferry-managed file on
                                 the operator machine, not
                                 on the box. This command runs on the operator
                                 machine and on a box install.

                                 The managed paths of a box are the paths that
                                 the last ferry sync linked
                                 there: the instruction files, the skill roots,
                                 and the other roots of the
                                 harnesses that are on for the box, with the
                                 custom harnesses of the config.
                                 A box that an earlier Ferry synced has no such
                                 record until the next sync.
                                 Until then, Ferry lists the paths of the
                                 built-in harnesses that are links
                                 of Ferry on the box.
  box                            List, add, and remove the boxes of the config
  skills                         Install skills into the global harness roots
                                 that Ferry manages

JSON output (--json):
  stdout has only JSON. Progress and the text lines go to stderr. Ferry
  shows no prompt: a confirmation fails with confirmation-required unless
  you give --yes. The SSH host keys of init and box add need
  --accept-host-keys: --yes does not trust them, and error.details.hostKeys
  lists them. A command that runs and exits prints one envelope:
    {"schemaVersion":1,"command","ok","result","warnings","error"}
  error is null, or {"code","message","hint"}. On failure, ok is false and
  the exit code is not 0. A sync-busy error names the box and the command
  that holds its lock, also in error.details.box and error.details.owner
  ({"pid","command","earlierVersion","otherProgram"}). A failed update, or a failed sync of more than
  one box, keeps the outcome of each box in result. watch, tunnel,
  tunnel --follow, and expose print one event for each line. Each event
  has "type". An error event also has "code", "message", and "hint".

  Error codes: usage, config-missing, config-invalid, unknown-box,
    box-required, box-offline, box-command-failed, forward-failed,
    confirmation-required, missing-values, deny-rule-match, refused,
    sync-busy, sync-failed, update-failed, login-failed, command-failed,
    failed.
  Event types: watch-started, synced, sync-failed, sync-refused,
    content-refused, config-error, update-started, update-failed,
    status-failed, watch-stopped, following, forward-opened, forward-closed,
    forward-failed, connection-lost, tunnel-closed, exposed, exited, login,
    error.

  The help of each command gives its result. The ferry agent skill
  describes the full contract.

ferry init

Usage: ferry init [options]

Record a Tailscale host or SSH destination, seed the snapshot, and convert this
machine.

The snapshot URL is an empty private git repository. Ferry does not create it.
For an SSH snapshot URL, load a key that can push to it into your SSH agent.
Ferry forwards the agent to the box to test read access, and asks before it
trusts the Git host key on the box. Ferry never falls back from Tailscale to
direct SSH.

With box tables, init runs again for the box of --box, else default_box, else
the only box. Use ferry box add to add a box.

Init writes the Ferry agent skill of this version to ~/.agents/skills/ferry,
so the snapshot carries it to the boxes. ferry self-update writes the skill
of the new version. Ferry does not change a skill folder with local changes.

Add a custom harness in ~/.ferry/config.toml. A repeat init keeps it:

  [[harness]]
  id = "opencode"
  name = "OpenCode"
  skill_root = ".config/opencode/skills"
  instruction_file = ".config/opencode/AGENTS.md"

Options:
  --host <host>                    Tailscale host name or IP address
  --ssh-user <user>                SSH user on the host
  --ssh-destination <destination>  explicit OpenSSH destination
  --snapshot-url <url>             private snapshot git URL
  --dry-run                        print the init plan without writing or
                                   connecting
  --accept-host-keys               trust the SSH host keys of the snapshot host
                                   on the box without a confirmation prompt
  --no-skill                       do not install the Ferry agent skill. Ferry
                                   records the choice, and self-update then
                                   skips the skill too
  -h, --help                       display help for command

With --json: { dryRun, leftovers, published, skill: { action, path, message } }, or with --dry-run { dryRun, leftovers, plan }.

ferry install

Usage: ferry install [options]

Install gh, jq, the agent CLIs, the tools of the config, and Ferry on the box.

Ferry prints the plan for each tool and asks before it runs a command on the
box. gh and jq come from apt, so Debian or Ubuntu is the tested target. Ferry on
the
box is the version of this machine. It is a box install that runs only
ferry expose and ferry whoami. Run ferry tools --help for the tool config.

Options:
  --yes       run without a confirmation prompt
  -h, --help  display help for command

With --json: { plan: [{ tool, policy, version, action, command, dependsOn }], gitIdentity }.

ferry update

Usage: ferry update [options]

Update the agent tools on the boxes and on this machine.

On this machine, Ferry updates only the agent CLIs that are installed. It does
not update Ferry itself. Each box gets the tool versions of its policy, the
Ferry version of this machine, and Paseo when the integration is on. The Paseo
update restarts the daemon, which stops the agents on the box.

With [update] watch = true in ~/.ferry/config.toml, ferry watch runs this
update once each day for the tools with the "latest" policy. The gh update
runs sudo apt on the box, and the watch has no terminal for a password. Add
this rule on the box with sudo visudo -f /etc/sudoers.d/ferry:

  <ssh-user> ALL=(root) NOPASSWD: /usr/bin/true, \
    /usr/bin/apt update, /usr/bin/apt install gh -y

ferry status shows "Box sudo: PASSWORDLESS" when the rule works.

Options:
  --yes       run without a confirmation prompt
  --dry-run   print the update plan without running it
  -h, --help  display help for command

With --json: { dryRun, boxes: [{ name, ok, error, offline, skipped, plan, integrations }], operator, updated, failed }, also on failure. skipped is the reason that Ferry did not change the box.

ferry uninstall

Usage: ferry uninstall [options]

Remove Ferry's local state and restore paths changed by init

Options:
  --yes       run without a confirmation prompt
  -h, --help  display help for command

With --json: { removed, restored }.

ferry auth

Usage: ferry auth [options] [provider]

Start a login on the configured box without copying credentials.

Tools: gh, claude, codex, cursor, and each [tools.<id>] table with login keys
(see ferry tools --help). Ferry starts the vendor login on the box and prints
a URL, and a code if the tool has one. Finish the login in a browser
on this machine. When codex gives no device code, Ferry forwards local port
1455 to the box for up to 120 seconds. Pi has no remote login. Run pi on the
box and use /login.

ferry auth gh also creates ~/.ssh/id_ed25519 on the box if it is missing, and
adds it to your GitHub account with the title "<box host> (ferry)", so agents
on the box can push. Delete that key in GitHub to revoke it.

--mcp <server> logs in to a remote MCP server on the box. Give the provider,
as in ferry auth codex --mcp linear, or the name that ferry status shows, as
in ferry auth --mcp codex/linear. Ferry forwards the localhost callback port,
such as 3118 for Claude, for up to 300 seconds. The port must be free on
this machine.

Without a provider, Ferry lists the tools and their login: startable,
manual SSH flow, or off.

Options:
  --mcp <server>  start the MCP server login of the provider CLI on the box,
                  such as linear or codex/linear
  -h, --help      display help for command

With --json: { providers: [{ id, login }] } without a provider, where login is "startable", "manual", or "off", else the login result { kind, provider, ... }. A "login" event line with the URL comes before the envelope. A login that needs the code from the browser reads it as one line on stdin.

ferry sync

Usage: ferry sync [options]

Publish the snapshot and apply it to the selected boxes, or to all boxes.

A file that looks like a secret stops the sync before the publish. The error
names the file, never the value. Ferry skips a hook that refers to a home path
that the box does not have, and an MCP server that is neither a remote HTTPS
server nor a stdio command. Ferry also skips a stdio MCP server that refers to
a home path, runs an inline shell or interpreter script, runs a shell or an
interpreter with options that Ferry cannot classify, or runs from a macOS app
bundle. Sync prints a line for each skipped hook and server. A stdio MCP server
carries its command, its arguments, and the names of its env keys, never their
values.
Ferry syncs up to 4 boxes at the same time. A failed box does not stop the
other boxes. Sync also writes the ferry PATH block in ~/.profile on the box.

Sync writes ~/.ferry/box/AGENTS.md on each box from three parts: the Ferry
header, then ~/.ferry/boxes/<name>/AGENTS.md of this machine for that box
only, then your ~/AGENTS.md. One blank line separates the parts. A per-box
file that looks like a secret stops the sync of that box only.

If a plugin in enabledPlugins comes from a marketplace that
extraKnownMarketplaces does not list, run claude plugin marketplace add for it
once on this machine.

Options:
  --dry-run                print the plan without writing
  --force                  back up live managed paths before Apply links them
  -m, --message <message>  snapshot commit message
  -h, --help               display help for command

With --json: { dryRun, published, boxes: [{ name, ok, step, error, skipped, plan, applyPlan, discarded }] }, also on failure of more than one box. skipped is the reason that Ferry did not connect to the box.

ferry history

Usage: ferry history [options]

List the recent snapshot commits and the paths each one changed.

Ferry reads the local snapshot checkout in ~/.ferry/store. Give a commit id to
ferry revert to undo that commit.

Options:
  -n, --limit <count>  the number of commits (default: 20)
  -h, --help           display help for command

With --json: { commits: [{ commit, date, subject, paths }] }, newest first.

ferry revert

Usage: ferry revert [options] <commit>

Undo one snapshot commit on this machine and on all boxes.

Ferry undoes the commit as git revert does, and later commits stay. The
skills, AGENTS.md, and extra roots on this machine link into the snapshot, so
they change with it. Ferry writes the reverted settings keys back into the
local settings files and keeps all other keys. Then Ferry syncs all boxes.

Ferry stops and changes nothing when a later commit changes the same lines,
or when this machine has changes that are not in the snapshot. Run ferry sync
first. Run ferry history for the commit ids.

Arguments:
  commit      the snapshot commit to undo

Options:
  --dry-run   print what the revert changes without writing
  --no-sync   do not sync the boxes after the revert
  -h, --help  display help for command

With --json: { dryRun, commit, subject, tip, paths, settings: [{ file, keys }], sync }. sync is the sync result, or null with --dry-run or --no-sync.

ferry move

Usage: ferry move [options] <path>

Continue a project on a box, on this machine with --from-box, or on another box
with both.

The path must be inside the home directory. The destination uses the same path
relative to its home. Ferry refuses unpushed commits, uncommitted changes to
tracked files, and a destination path that exists. The destination clones from
origin with its own SSH key, so run ferry auth gh for a box first. Ferry
carries the untracked and ignored files that pass the deny rules, skips build
output such as node_modules and dist, and checks each file with SHA-256.
Between two boxes, the files go through a temporary directory on this machine,
and nothing stays here. With --remove, the source copy goes to ~/.Trash on
macOS, else to ~/.ferry/trash. Run --dry-run first.

Ferry also carries the Claude and Codex sessions of the project and the Claude
project memory, so claude --resume and codex resume find them on the
destination. A session file there stays, unless the source has the same file.
Ferry skips a session that fails the deny rules and names the file and the
rule. The source keeps its sessions.

The deny rules run on the source machine. It reads each file one time and
sends only the bytes that pass. With --from-box, the Ferry on the box does
this, so the box needs a release of Ferry from ferry install or ferry update.
--dry-run copies no file.

Arguments:
  path               project folder inside the home directory

Options:
  --from-box <name>  move the project from this box. Without --to-box, the
                     destination is this machine
  --to-box <name>    move the project to this box. Without it and --from-box,
                     Ferry uses default_box or the only box
  --dry-run          print what Ferry would carry, refuse, and skip without
                     changes
  --remove           after verification, move the source copy to a trash
                     directory
  --include-env      also carry .env files that pass the token and secret rules
  --no-sessions      do not carry the agent sessions and the project memory
  --allow-secrets    also carry sessions, and with --include-env .env files,
                     that hold tokens or secrets
  --yes              carry .env files with secrets without a confirmation prompt
  -h, --help         display help for command

With --json: { path, source, destination, dryRun, git, carry, refused, skipped, notes, trash, sessions }.

ferry adopt

Usage: ferry adopt [options] <skill>

Copy a skill that an agent wrote on a box to this machine.

ferry status lists the box-only skills of each box: skills in a harness skill
root or in the box checkout that the snapshot does not have. The Ferry on the
box runs the deny rules on the skill. A skill that fails a deny rule does not
reach this machine: Ferry copies no file of it. Else Ferry copies the files
that pass and shows the file list of a new skill or the diff against the copy
on this machine. The box needs a release of Ferry from ferry install or ferry
update. After the confirmation, Ferry writes the skill to
the same skill root on this machine and moves the box copy to
~/.ferry/backups on the box. Then run ferry sync. It publishes the skill,
links it on this machine, and links it on all boxes.

Arguments:
  skill              the skill name that ferry status lists

Options:
  --from-box <name>  copy the skill from this box
  --yes              adopt the skill without a confirmation prompt
  -h, --help         display help for command

With --json: { box, name, source, destination, replaces, files: [{ path, executable }], skipped, diff, adopted, boxBackup }, or null when cancelled.

ferry tunnel

Usage: ferry tunnel [options] [command] [ports...]

Open box ports on this machine until Ctrl-C, list the ports that listen on the
box, or follow the ports of ferry expose.

Local ports bind to 127.0.0.1 only. The box end is 127.0.0.1 on the box, so a
dev server that listens only on ::1 does not answer. A plain tunnel does not
reconnect. With --follow, the local port is the box port when it is free, else
the next free port, and Ferry connects again 5 seconds after a drop. Run
ferry tunnel install to run --follow as a user service.

Put a host before the box port to forward to a host that the box can reach,
such as a database that accepts connections only from the box network. A
numeric first part is a box port. The box resolves the host name. Put an IPv6
address in brackets. Ferry first checks that the box can connect to the host,
and stops with an error when it cannot. On a box without bash, timeout, or
/dev/tcp, Ferry cannot always make this check. Then it prints a warning and
opens the tunnel.

  5432                   127.0.0.1:5432 on the box, local port 5432
  5432:15432             127.0.0.1:5432 on the box, local port 15432
  db.example:5432        db.example:5432 from the box, local port 5432
  db.example:5432:15432  db.example:5432 from the box, local port 15432
  [fd00::1]:5432         [fd00::1]:5432 from the box, local port 5432

A plain tunnel works as the child process of another program. It never
prompts: OpenSSH runs in batch mode. The local port accepts connections after
the SSH connection is ready. SIGTERM closes the tunnel with exit code 0.

--follow writes its forwards to ~/.ferry/tunnels/<box>.json when it connects,
after each change, and when the connection drops. The menu bar app reads the
file. Fields: schemaVersion (1), box, pid, connected (false after a drop, with
no forwards), updatedAt, and forwards: [{ name, cwd, boxPort, localPort }].
name and cwd are missing when the entry of ferry expose has none. Ferry
removes the file when --follow stops on Ctrl-C or SIGTERM.

Arguments:
  ports       box port, box:local to pick another local port, or
              host:port[:local] for a host that the box can reach, such as 3000,
              3000:4000, or db.example:5432

Options:
  --list      list the TCP ports that listen on the box, with process names
  --follow    open a forward for each port that ferry expose announces on the
              box, and close it when the port goes away
  -h, --help  display help for command

Commands:
  install     Install and start a user service that runs ferry tunnel --follow
              for one box
  uninstall   Stop and remove the tunnel user service of one box

With --json: events forward-opened, forward-closed, forward-failed, following, connection-lost, tunnel-closed. With --list, one envelope: { box, listeners: [{ port, address, process }] }.

ferry tunnel install

Usage: ferry tunnel install [options]

Install and start a user service that runs ferry tunnel --follow for one box.

The box is --box, then default_box, then the only box. The service always
runs with --box <box>, so a later default_box does not change it. Each box has
its own service:

  macOS  ~/Library/LaunchAgents/dev.ferry.tunnel.<box>.plist
         log: ~/Library/Logs/ferry-tunnel-<box>.log
  Linux  ~/.config/systemd/user/ferry-tunnel-<box>.service
         log: journalctl --user -u ferry-tunnel-<box>.service -f

The service writes ~/.ferry/tunnels/<box>.json, as ferry tunnel --follow does.
The menu bar app shows its ports.

The service starts again each time it exits. The service records the path of
this Ferry, the current PATH, and SSH_AUTH_SOCK. PATH must find ssh, and
tailscale for a Tailscale box. Run the command again after you move Ferry or
change these values. A successful ferry self-update restarts the service when
it points at the updated Ferry.

Options:
  -h, --help  display help for command

With --json: { manager, path }.

ferry tunnel uninstall

Usage: ferry tunnel uninstall [options]

Stop and remove the tunnel user service of one box.

The box is --box, then default_box, then the only box. Ferry removes the file
that ferry tunnel install wrote. The macOS log stays.

Options:
  -h, --help  display help for command

With --json: { manager, path, removed }.

ferry expose

Usage: ferry expose [options] <command...>

Run a command on the box and announce its port to ferry tunnel --follow.

Ferry writes ~/.ferry/exposed/<pid>.json before the command starts and
removes it when the command exits. At the start, Ferry removes the entries
whose pid does not run. Ferry forwards SIGINT, SIGTERM, and SIGHUP to the
command and exits with its exit code. Put the command after --.

For example, a service script in paseo.json on the box:

  "web": {
    "type": "service",
    "command": "ferry expose -- bun run dev --port $PASEO_PORT"
  }

Arguments:
  command     the command to run, after --, such as -- bun run dev

Options:
  --port <n>  the port of the command. The default is $PASEO_PORT
  -h, --help  display help for command

With --json: events exposed and exited. The output of the command goes to stderr.

ferry status

Usage: ferry status [options]

Inspect link, snapshot, managed paths, and box logins without writing.

The Tools part of each box shows one state for each tool: ok; drift, run
ferry update; missing, run ferry install; hidden, a login shell on the box
does not find the tool, run ferry sync; skipped, the tool has no target;
unknown, Ferry cannot read the box version. Box-only skills lists the skills
on each box that the snapshot does not have. ferry adopt --from-box copies one
to this machine. Skipped MCP servers lists each stdio MCP server of this
machine that Ferry does not carry, with its harness, its name, the reason, and
what to change. It never prints a command or an argument of a server. With
--json, result is the status report, schema version 2, and its skippedMcp has
that list. The ferry agent skill describes its fields.
Install the skill with ferry skills add dlhck/ferry --skill ferry.

--brief checks only the link, the free disk, memory, and load, the logins, the
MCP logins, the carried stdio MCP servers, and the tools of each box, and the
hooks of this machine that run a home file Ferry does not carry. It prints one
line for each item that needs action, with the Ferry command that fixes it.
ferry watch writes the same report to ~/.ferry/status.json.

The probe reads the free disk of the box home file system, the available
memory, and the load average in its SSH command. --brief shows an item when
the free disk is below both 10% and 5 GiB, or the available memory is below
10%. Set other limits in [status] of ~/.ferry/config.toml with
disk_free_percent, disk_free_gib, and memory_available_percent. A limit of 0
turns its part of the check off. When one disk limit is 0, the other decides.
The load is only in the JSON report.

Options:
  --brief     check only the link, the disk and memory, the logins, the MCP
              logins, and the tools, and print what needs action
  -h, --help  display help for command

With --json: the status report, schema version 2. With --brief, { schemaVersion: 1, checkedAt, boxes: [{ name, host, online, error, summary, issues: [{ kind, name, state, summary, message, command }], resources: { disk, memory, load } }] }.

ferry doctor

Usage: ferry doctor [options]

Check SSH, Tailscale, snapshot access, linger, and services, and print a fix for
each failure.

Ferry runs each check, also after a check fails, and changes nothing. It
checks that the SSH agent has a key, that this machine can read the snapshot
and push to it (git push --dry-run), and that the installed watch and tunnel
services run this Ferry. For each box, it checks that the box responds over
SSH with host key checks on, that Tailscale reaches a Tailscale box, that the
box can read the snapshot with the forwarded agent or the deploy key of
git_auth = "box", and that linger is on when a Ferry service runs on the box.

For each box, it also shows the lock of the box on this machine. A held lock
names the command that holds it and its pid, and is not a failure. The check
fails only for the lock of a Ferry version before 0.10.0 whose pid is alive.
When a Ferry process has the pid, the fix is ferry watch install, or to stop
that process. When a different program has the pid now, the fix removes the
lock file.

The exit code is 1 when a check fails. With --json, result has one entry for
each check, also on failure.

Options:
  -h, --help  display help for command

With --json: { schemaVersion: 1, ok, checks: [{ id, box, status, message, fix }] }, also on failure. status is ok, failed, or skipped.

ferry integrations

Usage: ferry integrations [options] [command]

List the integrations of each box, whether each one is enabled, its parts, and
the local app versions

Options:
  -h, --help                display help for command

Commands:
  enable [options] <name>   Install and start an integration on the box, then
                            turn it on in the config
  disable [options] <name>  Stop and remove an integration on the box, then turn
                            it off in the config

With --json: { boxes: [{ name, destination, integrations: [{ id, description, enabled, parts, available, localVersion, localSource, connectSteps }] }] }.

ferry integrations enable

Usage: ferry integrations enable [options] <name>

Install and start an integration on the box, then turn it on in the config.

paseo: Ferry installs Node 22 or later and the Paseo CLI at the version of the
local Paseo app, then starts the user service ferry-paseo.service. The daemon
listens on 127.0.0.1:6767 with the relay off by default. It has no password, so
use it
only on a box with one user. To connect Paseo Desktop, add the Remote SSH host
ssh://<box destination>. With Paseo on, sync carries the Paseo agent profiles,
managed Git and npm plugins, the portable fields of agents.providers,
agents.metadataGeneration.providers, daemon.appendSystemPrompt, and the
portable daemon.terminalProfiles. move registers the project in Paseo on the
box and imports each carried session as a Paseo agent. move --from-box does
the same in the Paseo of this machine.

For relay pairing, set paseo_relay = true in [integrations] of
~/.ferry/config.toml. A [box.<name>.integrations] table can override it.
Run ferry integrations enable paseo --box <name> again to apply a change.
A changed service config restarts the daemon and stops its agents.

Enable writes the whole ferry-paseo.service again, so a line that you added
to it by hand is gone. Put such a line in a drop-in file on the box, for
example ~/.config/systemd/user/ferry-paseo.service.d/local.conf.

The service has OOMPolicy=continue. When the kernel kills an agent process
that ran out of memory, the daemon and the other agents continue. Enable and
sync add the line to an older service without a restart of the daemon.

To carry daemon.autoArchiveAfterMerge, set paseo_auto_archive = true in
[integrations] of ~/.ferry/config.toml. A [box.<name>.integrations] table can
override it. Sync applies it with paseo daemon reload, without a restart.

An integration without a box part runs only on this machine. For it, Ferry
changes only the config and adds its commands when it can run here.

sherlock: needs the sherlock executable on this machine. Ferry adds ferry
sherlock add, and ferry status checks each connection that it added.

Arguments:
  name        integration name, such as paseo

Options:
  --dry-run   print the box commands without connecting or writing
  --yes       run without a confirmation prompt
  -h, --help  display help for command

With --json: { integration, action, dryRun, plan, output, enabled, connectSteps }.

ferry integrations disable

Usage: ferry integrations disable [options] <name>

Stop and remove an integration on the box, then turn it off in the config.

Ferry never removes ~/.paseo on the box. An integration without a box part
changes only the config.

Arguments:
  name        integration name, such as paseo

Options:
  --purge     also uninstall the integration package on the box
  --yes       run without a confirmation prompt
  -h, --help  display help for command

With --json: { integration, action, dryRun, plan, output, enabled, connectSteps }.

ferry sherlock

Usage: ferry sherlock [options] [command]

Add Sherlock database connections that tunnel through a box

Options:
  -h, --help            display help for command

Commands:
  add [options] <name>  Add a Sherlock connection whose tunnel is ferry tunnel
  help [command]        display help for command

ferry sherlock add

Usage: ferry sherlock add [options] <name>

Add a Sherlock connection whose tunnel is ferry tunnel.

Ferry runs sherlock connection add with
--tunnel-command "ferry tunnel --box <box> <target>:{{port}}". Sherlock opens
the tunnel on the first query and closes it when it is idle. The box is --box,
then default_box, then the only box.

The target is a box port, such as 5432, or a host and port that the box can
reach, such as db.example:5432.

On a terminal, Ferry asks for the password and gives it to Sherlock on stdin.
Sherlock stores it in the keychain of this machine. With --password-stdin,
Sherlock reads the password from the stdin of Ferry. Ferry never stores the
password and never changes the Sherlock config file. Ferry records the name,
box, and target in ~/.ferry/sherlock.json for ferry status.

With --json, Ferry asks nothing, so give --password-stdin or --password-env.
With --json: { name, box, target, tunnelCommand }.

Arguments:
  name                  connection name in Sherlock

Options:
  --target <target>     box port or host:port that the box can reach, such as
                        5432 or db.example:5432
  --type <type>         postgres, mysql, mssql, or redis
  --database <name>     database name
  --username <user>     database user
  --ssl <mode>          off, require, or verify
  --password-stdin      Sherlock reads the password from stdin
  --password-env <var>  Sherlock reads the password from this environment
                        variable at query time
  --force               replace a Sherlock connection with the same name
  -h, --help            display help for command

ferry tools

Usage: ferry tools [options]

List the tools, the version policy of each one and of each box, and the versions
on this machine.

Ferry has recipes for gh and the agent CLIs claude, codex, pi, and cursor.
Define each other tool in ~/.ferry/config.toml. Ferry does not scan projects.

A policy is "operator" (the version on this machine), "latest", or an exact
version. The default is "latest" for an agent CLI and "operator" for a tool.
With "operator", Ferry skips a tool that this machine does not have.
[box.<name>.tools] sets the policy for one box.

"off" turns off gh or an agent CLI (claude, codex, pi, cursor). install,
update, and the daily watch update skip it, ferry auth refuses it, and
ferry status shows it as off. Ferry does not uninstall it from the box. An
off agent also turns off its harness: sync does not read it on this machine
and does not write it on the box, and removes the links that Ferry made
there before. Ferry never removes other files there. A box policy can turn
the tool on again. "off" is not valid in a [tools.<id>] table. To remove
such a tool, delete its table.

  [tools]
  gh = "latest"
  codex = "0.156.1"
  pi = "off"

  [box.b.tools]
  pi = "latest"

  [tools.pnpm]
  version = "operator"
  local = "pnpm --version"
  box = "pnpm --version"
  latest = "npm view pnpm version"
  install = 'npm install -g --prefix "$HOME/.local" pnpm@{version}'
  path = [".local/bin"]
  depends = ["node"]

  [tools.northflank]
  local = "northflank --version"
  install = "npm install -g @northflank/cli@{version}"
  auth_status = "northflank list projects"
  auth_login = "northflank login --do-not-open-browser"
  auth_hosts = ["northflank.com"]

local and install are required. local prints the version on this machine, box
prints the version on the box, and latest prints the newest version, which the
"latest" policy needs. update is the update command, and the default is
install. {version} is the only placeholder. path adds home directories to the
box PATH, and depends names the tools to install first. A comment must be on
its own line. Run ferry sync after a path change.

The auth keys let ferry auth <id> log the tool in on the box. auth_status
passes when the tool is logged in. auth_login starts a login that prints a URL
and finishes after the browser step, with no input on the box. auth_hosts names
the hosts that URL may have. Ferry passes the URL on with its fragment and
query, which can hold the login session. Give all three keys or none.

Options:
  -h, --help  display help for command

With --json: { tools: [{ id, name, kind, install, policy: { policy, default }, boxes, operatorVersion }] }.

ferry watch

Usage: ferry watch [options] [command]

Watch the portable set and sync accepted changes.

The watch syncs all boxes one second after a change stays stable. A box that
fails retries with its own backoff, up to 60 seconds. The watch reads the
config in each cycle. With [update] watch = true, it also runs ferry update
once each day. Run ferry update --help for the sudo rule on the box.

At the start, every 5 minutes, and after each sync, the watch runs
ferry status --brief for all boxes and writes the report to
~/.ferry/status.json.

Options:
  -h, --help  display help for command

Commands:
  install     Install and start the watch user service

With --json: events watch-started, synced, sync-failed, sync-refused, content-refused, config-error, update-started, update-failed, status-failed, watch-stopped.

ferry watch install

Usage: ferry watch install [options]

Install and start the watch user service.

The macOS service is ~/Library/LaunchAgents/dev.ferry.watch.plist. Its log
is ~/Library/Logs/ferry-watch.log. The Linux service is
~/.config/systemd/user/ferry-watch.service. Read its log with
journalctl --user -u ferry-watch.service -f.

The service records the path of this Ferry, the current PATH, and
SSH_AUTH_SOCK. PATH must find git, ssh, and tailscale for a Tailscale box.
Run the command again after you move Ferry or change these values. A
successful ferry self-update restarts the service when it points at the
updated Ferry.

Options:
  -h, --help  display help for command

With --json: { manager, path }.

ferry menubar

Usage: ferry menubar [options] [command]

Install or remove the macOS menu bar app that shows what needs action on the
boxes

Options:
  -h, --help         display help for command

Commands:
  install [options]  Install and start the macOS menu bar app
  uninstall          Stop and remove the macOS menu bar app
  help [command]     display help for command

ferry menubar install

Usage: ferry menubar install [options]

Install and start the macOS menu bar app.

The app shows the report of ~/.ferry/status.json: offline boxes, logins, MCP
logins, and tool drift. ferry watch writes the file, so run ferry watch
install too. Click an item with a Ferry command to run it in Terminal.

The app also shows the ports of each running ferry tunnel --follow, from
~/.ferry/tunnels/<box>.json. Click a port to open it in the browser.

Ferry downloads ferry-menubar-macos.zip of the release of this Ferry,
verifies it against SHA256SUMS of the release, and unpacks it to
~/Applications/Ferry Menu Bar.app. When the release does not have these
files yet, Ferry stops and the installed app stays. --app installs a local build
of
macos/build.sh, a .app directory or its zip, without a checksum. A
development build of Ferry needs --app.

The app starts at login with ~/Library/LaunchAgents/dev.ferry.menubar.plist.
It records the path of this Ferry as FERRY_PATH, the current PATH, and
SSH_AUTH_SOCK. Run the command again after you move Ferry or update it.

Options:
  --app <path>  install a local build of macos/build.sh: a .app directory or its
                zip
  -h, --help    display help for command

With --json: { app, path, version, ferryPath }. version is null with --app.

ferry menubar uninstall

Usage: ferry menubar uninstall [options]

Stop and remove the macOS menu bar app.

Ferry stops the app, and removes ~/Library/LaunchAgents/dev.ferry.menubar.plist
and ~/Applications/Ferry Menu Bar.app. The log stays.

Options:
  -h, --help  display help for command

With --json: { app, path, removed }.

ferry self-update

Usage: ferry self-update [options]

Update Ferry on this machine to the latest release.

Ferry updates in the same way as it was installed: with npm, or with the
release installer in the directory of this binary. It restarts installed
watch and tunnel services that point at this Ferry. On macOS, it also updates
an installed release menu bar app. It writes the Ferry agent skill of the new
version to ~/.agents/skills/ferry, unless ferry init --no-skill turned it off
or the skill folder has local changes. Then run ferry update to put the new
version on the boxes.

A release is visible before its build attaches the files. When the latest
release does not have its files yet, Ferry says that the release is not ready
and does not start the update. This is not an error. Run the command again
some minutes later.

On a terminal, each command also asks to update when a newer release is
there. Ferry reads the latest release at most once a day. It does not ask
with --json, with CI set, or with FERRY_NO_UPDATE_CHECK=1, and it does not
ask for a release that does not have its files yet.

Options:
  -h, --help  display help for command

With --json: { current, latest, updated, state, services: [{ service, action, message }], skill }. state is updated, up-to-date, or not-ready. not-ready: the release latest does not have its files yet, and Ferry did not start the update. skill is the message of the skill update, or null. The output of the installer goes to stderr.

ferry whoami

Usage: ferry whoami [options]

Print the role of this machine: the operator machine or a Ferry box.

On a box, Ferry also prints the box name from the last ferry sync, and the
parts of the generated instruction file ~/.ferry/box/AGENTS.md in their
order: the Ferry header, the per-box instructions from
~/.ferry/boxes/<name>/AGENTS.md on the operator machine when the box has
them, and the shared ~/AGENTS.md. The operator machine is the source of
truth. On a box, change a Ferry-managed file on the operator machine, not
on the box. This command runs on the operator machine and on a box install.

The managed paths of a box are the paths that the last ferry sync linked
there: the instruction files, the skill roots, and the other roots of the
harnesses that are on for the box, with the custom harnesses of the config.
A box that an earlier Ferry synced has no such record until the next sync.
Until then, Ferry lists the paths of the built-in harnesses that are links
of Ferry on the box.

Options:
  -h, --help  display help for command

With --json: { role: "operator" or "box", box, instructions: { file, sources: [{ part: "header", "box", or "shared", path }] }, managedPaths: { instructionFiles, skillRoots, roots } }. box is null on the operator machine and before the first sync of a box. instructions is null on the operator machine and on a box without the generated file. sources has the merged parts in order, and path is the file on the operator machine. On a box, managedPaths has the paths that the last sync linked there.

ferry box

Usage: ferry box [options] [command]

List, add, and remove the boxes of the config

Options:
  -h, --help               display help for command

Commands:
  list                     List the boxes, their transport and destination, and
                           the default box
  add [options] <name>     Check a new box like ferry init, then add it to the
                           config
  remove [options] <name>  Remove a box from the config. With --uninstall,
                           remove Ferry from the box first
  default <name>           Set default_box, the box of install, auth, move,
                           tunnel, and integrations enable|disable without --box
  help [command]           display help for command

ferry box list

Usage: ferry box list [options]

List the boxes, their transport and destination, and the default box

Options:
  -h, --help  display help for command

With --json: { boxes: [{ name, transport, destination, default }] }.

ferry box add

Usage: ferry box add [options] <name>

Check a new box like ferry init, then add it to the config.

The first box add on a [host] config moves [host] to [box.default] and sets
default_box = "default". Ferry asks before it writes.

Ferry also creates the empty file ~/.ferry/boxes/<name>/AGENTS.md on this
machine and prints its path. Write instructions for this box only there.
ferry sync puts them into the instruction file of the box, after the Ferry
header and before your ~/AGENTS.md. The file never goes into the snapshot.

With --git-auth box, Ferry never forwards your SSH agent to the box. Ferry
creates ~/.ssh/ferry_snapshot on the box and tests read access to the
snapshot with it. If the test fails, Ferry prints the public key and does not
change the config. Add the key as a read-only deploy key on the snapshot
repository, then run the command again. To change an existing box, set
git_auth = "box" in its [box.<name>] table and run ferry init --box <name>.

Arguments:
  name                             box name: 1 to 32 characters from a-z, 0-9,
                                   and -

Options:
  --host <host>                    Tailscale host name or IP address
  --ssh-user <user>                SSH user on the host
  --ssh-destination <destination>  explicit OpenSSH destination
  --git-auth <mode>                agent forwards your SSH agent to the box git;
                                   box uses a read-only deploy key on the box
                                   (choices: "agent", "box")
  --yes                            change a [host] config to box tables without
                                   a confirmation prompt
  --accept-host-keys               trust the SSH host keys of the snapshot host
                                   on the box without a confirmation prompt
  -h, --help                       display help for command

With --json: { name, transport, destination, gitAuth, migrated, instructionFile }.

ferry box remove

Usage: ferry box remove [options] <name>

Remove a box from the config. With --uninstall, remove Ferry from the box first.

Without --uninstall, Ferry does not connect to the box and does not change it.

With and without --uninstall, Ferry stops and removes the tunnel user service
of the box on this machine, which ferry tunnel install wrote. Your per-box
instruction file ~/.ferry/boxes/<name>/AGENTS.md stays. Ferry prints its path.
A later box with the same name gets its text. Delete the file when you do not
need it.

With --uninstall, Ferry reads the box, prints the plan, and asks you to type
the box name. Then it does these steps on the box, in this order:

  1. It stops and removes each ferry-*.service user service, such as
     ferry-paseo.service. A stopped service stops its agents.
  2. It removes each skill link, instruction file link, and root link that
     points into ~/.ferry/store or to ~/.ferry/box/AGENTS.md. When ferry sync
     --force moved a file of yours to ~/.ferry/backups, Ferry moves the
     newest backup of that path back. Else the path stays absent.
  3. It removes the ferry PATH block of ~/.profile.
  4. It removes ~/.ferry/box, ~/.ferry/exposed, ~/.ferry/store, the Ferry
     binary ~/.local/bin/ferry, and the marker ~/.ferry/box.json.

Then Ferry removes the box from the config. Ferry keeps these on the box:
the logins and credentials, the SSH keys, the project directories,
~/.paseo, and the tools that Ferry installed: gh, jq, the agent CLIs, the
Paseo CLI, and the tools of the config. It also keeps the settings keys, MCP
servers, and plugins that ferry sync merged into the config files of the
box, ~/.ferry/trash, and each backup that it did not move back.

With --uninstall, Ferry holds the sync lock of the box from before it
connects until the box is out of the config. During that time, a sync that
includes the box fails with the code sync-busy, also a sync of ferry watch.
The watch syncs again later. When a sync for the box runs, the command fails
with that code and changes nothing. Run it again when the sync ends.
--dry-run takes no lock.

When Ferry cannot reach the box, or a box step fails, the box stays in the
config. Run the command without --uninstall to remove the box from the
config only.

Without --uninstall, Ferry refuses the last box of the config, because the
box keeps Ferry. With --uninstall, Ferry removes the last box too, also the
box default of a [host] config. Then the config has no [host] table and no
[box.<name>] table, and the rest of the config stays. ferry sync, ferry
status, ferry install, and ferry watch fail and name ferry box add, until
you add a box with ferry box add <name>. ferry init adds a box too.

Arguments:
  name         box name

Options:
  --uninstall  remove Ferry from the box, then remove the box from the config
  --yes        with --uninstall, do not ask for the box name
  --dry-run    with --uninstall, print the plan and change nothing
  -h, --help   display help for command

With --json: { name, defaultBoxRemoved, tunnelService, instructionFile }. tunnelService is the file of the tunnel user service that Ferry removed from this machine, or null. instructionFile is the per-box instruction file that stays on this machine, or null. With --uninstall, also uninstall: { dryRun, plan: { home, services, links: [{ path, link, backup }], profileBlock, paths }, remaining }, or null when cancelled. The plan paths are relative to the box home. backup is the file that Ferry moves back to path, or null. remaining has the names that stay in ~/.ferry on the box. A dry run changes nothing.

ferry box default

Usage: ferry box default [options] <name>

Set default_box, the box of install, auth, move, tunnel, and integrations
enable|disable without --box

Arguments:
  name        box name

Options:
  -h, --help  display help for command

With --json: { defaultBox }.

ferry skills

Usage: ferry skills [options] [command]

Install skills into the global harness roots that Ferry manages

Options:
  -h, --help                        display help for command

Commands:
  add [options] <source> [args...]  Run npx skills add as a global copy install.

                                    Ferry adds -g and --copy unless you pass
                                    them.
                                    Global copy installs match Ferry's snapshot
                                    model. The skill is a real
                                    directory in a global harness root, and the
                                    next sync links it to the store.
                                    Put arguments after -- to keep Ferry from
                                    reading them.
                                    Run ferry sync or ferry watch to publish the
                                    skill.
  help [command]                    display help for command

ferry skills add

Usage: ferry skills add [options] <source> [args...]

Run npx skills add as a global copy install.

Ferry adds -g and --copy unless you pass them.
Global copy installs match Ferry's snapshot model. The skill is a real
directory in a global harness root, and the next sync links it to the store.
Put arguments after -- to keep Ferry from reading them.
Run ferry sync or ferry watch to publish the skill.

Arguments:
  source      skill source, such as owner/repo or a git URL
  args        other npx skills add arguments, passed through unchanged

Options:
  --project   install into the current project and do not add -g
  -h, --help  display help for command

With --json: { argv }. The output of npx goes to stderr.