Ferry

Security model

Ferry carries configuration. It does not carry anything that proves who you are. The rule is: no credential leaves the machine that has it.

What never leaves a machine

Logins, credential files, tokens, API keys, .env files, and whole settings files never leave the machine that has them. A login starts on the box with ferry auth, and its token stays there.

Deny rules

Before each publish, Ferry checks the carried files against the deny rules:

Scripts, such as hook scripts, pass and keep their executable bit. A match stops the sync, and nothing is published. The error names the file, never the value.

ferry sync --dry-run prints the plan and the deny list. ferry status --json has the list in denyList.

Do not rename, move, split, or encode a file to get past a deny rule. Remove the secret from the file, or remove the file from the carried set.

Per-box instruction files

A per-box instruction file, ~/.ferry/boxes/<name>/AGENTS.md, gets the same deny rules as ~/AGENTS.md, and the private key rule. It never goes into the snapshot. Ferry sends it to its box on the standard input of an SSH command, so the text is not in a command line. A match stops the sync of that box only.

Settings and MCP servers on the box

Ferry merges the carried settings keys and MCP servers into the box files on the box: with jq for JSON files and with awk for the Codex config.toml. The box sends back only a status, never the file.

Stdio MCP servers

For a stdio MCP server, Ferry carries the command, the arguments, and the names of the env keys, never their values. Set the values in the env of the server on the box. Ferry keeps them: the box merges its own entry with jq or the agent CLI, and sends back only a status or key names, never a value.

Arguments that stop the sync

A command or an argument stops the sync when it has one of these:

Ferry decodes the percent escapes of a URL before the check, so pa%73sword= is password=. The same rules apply to a URL inside another URL, as it is or with percent escapes, as in https://proxy.example/?next=https://<user>:<password>@host.

In another scheme, a user without a password is a login name and passes, as in git+ssh://git@github.com/you/server or postgresql://alice@db.example/app.

The error names the server and the rule, never the value. Set secrets in the env of the server on the box.

Servers that Ferry skips

The sync continues and names the server when Ferry skips it:

A script file passes, as in node /srv/server.js -c conf.json or python3 -m some_server. Put the script in a file that Ferry carries, or run the server through a tool on the PATH.

Ferry knows only the shells and interpreters in its list. It carries a tool that is not in the list and that runs code from its arguments, such as ssh host CODE, awk, or find -exec. The credential rules still apply to the arguments of that tool.

Ferry never installs a command. ferry status --brief names each missing env key, each command that is not on the box, and each server that Ferry did not carry. The full ferry status lists each skipped server, also a server from a macOS app bundle, with its harness, its name, and the reason, and never with a command or an argument.

SSH

Files that leave a box

ferry move --from-box and ferry adopt --from-box copy files from a box. For a skill or a project that leaves a machine, that machine checks and copies in one step:

Hashes and names

The origin URL of a project

A credential in the origin URL of a project stays on its machine. ferry move takes the user and the password, a token in the place of the user, and each secret query parameter out of the URL before the URL leaves the source. The destination clones from the URL without them, so it needs its own login for the clone. A login name stays, as in ssh://git@host and git@host:path. In an output, [credential] stands for a credential in a URL.

The limit of the rule

The rule “no credential leaves the machine that has it” holds for a box that runs an honest Ferry.

The check on the box protects against mistakes and against files that change. It does not protect against a box account that an attacker controls, because that Ferry can give any answer and any bytes. This is why the operator machine applies its own deny rules to the bytes that arrive from a box.

Ferry runs commands with your SSH user. Use a box and a user that you trust with the agents that run there.

The session scan of ferry move also has a limit. See Moving a project.

Report a vulnerability

Do not open a public issue for a security problem. Report it privately with GitHub private vulnerability reporting. See SECURITY.md for the scope.