Reference
CLI reference
Every fenzi command, its flags, and its exit codes. You only type fenzi. For an auditor, each command lists what it calls upstream in a collapsed block.
Commands
| Command | Does |
|---|---|
fenzi | With no command, a menu for the current folder. |
setup | Guided setup: checks the machine, picks the agent, stores its key, and offers to start it. |
doctor | Checks the machine and the gateway, and names the pinned OpenShell version when it is missing. |
init | Writes a starter fenzi.yaml. |
key | Stores, replaces, or removes an agent's model key on this machine. |
run | Starts an agent in a new sandbox. |
ps, connect, stop | Lists sandboxes, resumes the agent or opens a shell in a running one, and ends them. |
rm | Forgets finished sandboxes: the record, the staged changes, and the saved log. |
requests, approve, deny | Lists and decides requests for more access. |
log | Shows allow and deny decisions. |
diff, apply | Reviews and applies the agent's changes to your repo. |
policy compile, policy check, policy apply | Shows the compiled policy, runs the prover, and loads a host change onto sandboxes running from this folder. |
agents | Lists agents and builds their images. |
update, version | Installs the latest release of the CLI, and prints the one you have. |
login, logout, status | Team. Signs this machine in to a team, signs it out, and shows the sign-in. |
policy publish | Team, admins. Publishes a complete org policy to the console. |
sync | Team. One pass: fetches the newest revision, loads a host change onto running sandboxes, and sends waiting decisions. |
forward | Team. Sends decisions and requests to the console and applies its reviews, every five seconds until stopped. |
Set up
fenzi
With no command, in a terminal, it shows the current folder, the agent from setup, how many sandboxes are running, and whether changes are waiting to be applied. Then it offers the next steps: start the agent, review the changes, connect to a running sandbox, answer requests, check the machine, run setup again, or list every command. Before setup has finished once, it starts setup instead. Without a terminal it prints the list of commands.
fenzi setup
Asks questions in five steps: this machine, agent, model key, project folder, and agent image. Then it shows the policy the agent starts with in that folder, resolved as run resolves it, and names where it comes from: developer-default with the folder's fenzi.yaml over it, or the team's signed revision on a signed-in machine. Then it offers to run it. The installer starts it when a person is at the terminal, and so does fenzi with no command until setup has finished once. Answers are saved in ~/.config/fenzi/setup.json.
Every change asks first. If OpenShell is missing, it offers to install release 0.1.2 with the upstream installer and shows the command before it runs it. On a Mac with Homebrew, it offers to start a stopped gateway as a Homebrew service. It turns off automatic approval and the gateway-wide settings doctor --fix removes, only if you say yes. It stores the key as key set does and builds the image as agents build does. The build log is written to ~/.cache/fenzi/builds/.
Use the arrow keys or j and k, then Enter. Esc or the left arrow goes back to the previous question, as far as the agent step. Ctrl-C stops setup with exit code 130. Without a terminal, questions are numbered and read one line at a time; type b or < to go back from a choice, and < from a typed answer.
fenzi doctor [--fix] [--json]
Checks the runtime version against the pinned range, the gateway connection, the sandbox driver, the image builder, the approval mode, the sandbox settings Fenzi turns on, and the bad-revision setting. If the runtime is missing, it names the pinned version, skips the gateway checks, and still checks the prover, the image builder and the plan. fenzi setup installs the runtime.
Fails if the gateway approves requests automatically for every sandbox, or if it turns off the audit log or agent requests for every sandbox. Those gateway-wide values override what run sets. --fix removes them. It changes nothing else, and says what it will change first. It does not configure colima; see colima on a Mac.
What it calls upstream
openshell status, openshell gateway info, openshell settings get --global. With --fix: openshell settings delete --global.
fenzi init
Writes fenzi.yaml in the current folder, with example hosts commented out. It does not read the project. Nothing is allowed until you uncomment a host.
fenzi key list [--json] | set <agent> | remove <agent>
list shows which agents have a key stored on this machine. It never prints a key. set asks for the key without showing it, or reads one line from a pipe, and stores it in the gateway's provider store; run it again to replace a key. New sandboxes use the new key. remove deletes the stored key. It refuses while a sandbox that was started for that agent is still running, and names it: bl-web-4f2a is running with the Claude Code key; stop it first (fenzi stop bl-web-4f2a). A sandbox running another agent does not block it. See Model keys.
What it calls upstream
openshell provider get, openshell provider create --from-existing or openshell provider update --from-existing, with the key passed in the environment of that one command, never as an argument. remove calls openshell provider delete.
Run an agent
fenzi run [flags] [agent] [agent args]
Without an agent name, it runs the agent chosen in setup, or Claude Code if setup has not run. fenzi run codex picks another one. Everything after the agent's name goes to the agent: fenzi run claude --resume. Fenzi's own flags go before the agent: fenzi run --no-upload claude. To pass arguments to the agent from setup without naming it, put -- first: fenzi run -- --resume. This page is the only one that uses that form.
On a machine signed in to a team, it also forwards while the agent runs; see Forwarding decisions. If the console could not be reached during the session, it says so after the agent exits; fenzi forward sends what was queued.
It refuses to start in your home folder, at /, or in a folder that holds your home folder, before it asks the gateway anything. cd into one project folder first.
Compiles and checks the policy, builds the agent's image if needed, creates the sandbox, copies the project in, and starts the agent in your terminal. When the agent exits, it saves the log and stages the changes. In a terminal it then shows the changes and asks what to do with them. The sandbox keeps running, with the agent's session, its files and the run's approvals: fenzi connect resumes the agent where it stopped, and fenzi stop ends it. --rm deletes it instead, after the changes are staged; deleting does not touch staged or applied changes.
When a sandbox started from the same folder for the same agent is still running, run asks first whether to resume it or start a new sandbox. Resuming opens the agent's last session there, with the files as it left them; your folder is not copied again. --new skips the question and starts a new sandbox. Without a terminal it starts a new one.
Claude Code starts with --permission-mode manual, so Claude asks you before each command and the policy decides what the command can reach. Pass your own after the agent's name to use another mode: fenzi run claude --permission-mode auto.
| Flag | Meaning |
|---|---|
--rm | Delete the sandbox when the agent exits, after its changes are staged. Without it the sandbox is kept. |
--new | Start a new sandbox even when one for this folder and agent is still running. |
--keep | Keep the sandbox when the agent exits. The default now; kept for scripts that pass it. |
--name <name> | Sandbox name, at most 19 characters. Default bl-<folder>-<id>, with the folder shortened to fit. |
--policy <file> | Use this policy file instead of ./fenzi.yaml. Free only. |
--no-upload | Start with an empty working folder. |
--allow-secrets | Copy files that look like secrets without asking. The warning is still printed. |
Before it builds or creates anything, run names the files in the copy that look like secrets: .env and .env.* (not .example, .sample, or .template), *.pem, *.key, *.p12, *.pfx, SSH keys named id_rsa, id_ed25519, or id_ecdsa, .npmrc, .pypirc, .netrc, credentials*.json, service-account*.json, and *.tfvars. It reads names only. They are copied because .gitignore does not exclude them. In a terminal it asks whether to copy them, and stops with nothing created unless you say yes. Without a terminal it prints the warning and continues.
What it calls upstream
In order: openshell gateway info, to build the image with the engine the gateway's driver uses, openshell settings get --global, openshell-prover check, openshell profile import and openshell provider create on first use, openshell sandbox create --detach --policy --provider --approval-mode manual --no-auto-providers, openshell sandbox get --policy-only and the prover again, openshell settings set to turn on the JSON audit log, openshell sandbox upload, openshell sandbox exec --tty. On exit: openshell logs until the log stops growing, openshell sandbox download, and openshell sandbox delete when the sandbox is not kept.
The project is copied to /sandbox/<folder> and the agent starts there. .git is not copied.
The settings change reaches the sandbox on its next poll, and loading it closes open connections, so run waits for it before starting the agent. Settings the gateway already turns on for every sandbox are skipped. If the gateway turns one off for every sandbox, run stops before creating anything and points to doctor --fix.
For agents that support it, run adds a short guide to the agent's instructions: how to look up a refused connection and how to request access through the sandbox's policy API. Claude Code gets it through --append-system-prompt, Codex through a setting on its command line, OpenCode as a file written inside the sandbox, outside the project, and Grok through --rules. Your project is not changed. See Agents.
Exits with the agent's exit code once the agent has started.
Sandboxes
fenzi ps [--all] [--json]
Lists the sandboxes Fenzi started that are still running. --all adds finished ones, whose logs and staged changes you can still read.
What it calls upstream
openshell sandbox list --names --selector fenzi=1.
fenzi connect [name] [--new] [--shell] [-- agent args]
Opens the sandbox's agent again in your terminal, in the project folder inside the sandbox, with the same policy and the files as the agent left them. The agent continues its last session there, so it keeps the conversation: Claude Code, OpenCode and Grok with --continue, Codex with codex resume --last. When there is no session to resume, the agent fails at once, and a new session starts with a line that says so. --new starts a new session. --shell opens a shell instead. Use it after the agent exits, from a second terminal while the agent works, or when the terminal that ran the agent was closed and the sandbox is still running. Default: the latest running sandbox for the current folder; if several are running elsewhere, it asks which. Exiting leaves the sandbox running. fenzi stop ends it and brings the changes back. Arguments for the agent go after -- here, as in fenzi connect -- --resume.
What it calls upstream
openshell sandbox exec --tty, as run does.
fenzi stop [name] [--yes]
Ends a sandbox: saves the log, copies the project out to staging, then deletes the sandbox. If the copy fails, the sandbox is kept. Default: the latest running sandbox for the current folder; if several are running elsewhere, it asks which. Asks first unless you pass --yes.
fenzi rm <sandbox> [<sandbox>...] | --finished [--json]
Forgets finished sandboxes. A finished sandbox is already gone from the gateway; what stays on this machine is its record in ps --all, the changes staged when it ended, and the log saved then. rm removes those three, in that order, and says which it found. Staged changes that were not applied are lost with it; fenzi diff --sandbox <name> shows them first. --finished removes every finished sandbox. A running sandbox is refused: bl-web-4f2a is running; fenzi stop bl-web-4f2a ends it first. The others named are still removed, and the command exits 1. A name that is not a sandbox on this machine is an error, and nothing is removed.
What it calls upstream
openshell sandbox list --names --selector fenzi=1, to tell running from finished; nothing else.
Requests
fenzi requests [--all] [--sandbox <name>] [--json]
Lists the pending requests the agent filed in your running sandbox, with the first eight characters of each id, the program, and the agent's reason. Drafts the sandbox made from refused connections are counted under the list, with their hosts; --all lists them too, under their own heading. A request counts as the agent's when the sandbox log marks it as filed by the agent, or when its reason is not the generated one. In a terminal it then asks about each one: approve, deny with a reason, or decide later. Without a terminal it prints the approve and deny commands instead. If several sandboxes are running and none is in the current folder, it asks which. If the runtime's text format changes, Fenzi shows the raw text instead of a list.
What it calls upstream
openshell rule get <sandbox> --status pending and openshell logs. OpenShell prints the pending list as text, not JSON.
fenzi approve <id> [--save [--dry-run] [--policy-from <file>]] [--sandbox <name>] [--json]
Approves a request. The id can be any start of the full id that matches one request. The sandbox loads the new rule on its next poll, about ten seconds later. On Free, the approval also covers the next calls of the same kind: the request's host is added to the running sandbox's policy, read-only for a read, or the method and path for a write, checked by the prover and loaded. Other pending requests for that host that it covers are approved with it. It lasts as long as the sandbox, and fenzi policy apply keeps it. --json names it in same_kind. On Team, approve in the console. An approval made here still works, and the console shows it as drift.
--save also adds the host to the project's fenzi.yaml, so the next sandbox in that folder starts with it. Comments in the file are kept. A request that allows only GET or HEAD is saved as read-only, which allows GET, HEAD, and OPTIONS on every path. A request for another method is saved as read-write with its METHOD /path line. A request with no method or path, which lets any traffic through to the host, is saved as read-only, and the command says so. A host already in the file or in developer-default at another level is left for you to edit. Then it runs the check policy check runs and prints the result. A request in a form it cannot save is not approved, and it says why. On a signed-in machine --save is refused: the team policy is published from the console, and the message names the request's page there, <server>/console/requests/<sandbox>/<id>. Not with --json, except with --dry-run.
--save --dry-run approves nothing and writes nothing. It prints the file it would change, a unified diff of fenzi.yaml before and after, the entry it would add or that the host is already there, and the check policy check runs, made on the new content in a temporary folder. It exits 0 when the check passes, 2 when it fails, and 1 on an error. With --json it prints file, exists, before, after, diff, host, port, access, allow, already, note, from with --policy-from, and check with passed, result, checked, agent, example, and reason.
--save --policy-from <file> takes the new fenzi.yaml from the file given, for example the preview after you edited it. It checks that file first. Only when the check passes does it approve the request and write the file; when it fails, nothing is approved or written, and it exits 2. With --dry-run it previews that file instead.
fenzi deny <id> --reason "<text>" [--json]
Denies a request and keeps the reason with it.
What it calls upstream
openshell rule reject --reason.
Log and changes
fenzi log [flags]
| Flag | Meaning |
|---|---|
--deny | Denies only, with the sandbox's reason for each. |
--sandbox <name> | One sandbox. Default: the latest sandbox for the current folder. Outside a project, the latest one overall, and the command says which. requests, approve, deny, diff, and apply choose the same way. |
--since <duration> | Only decisions newer than this, for example 10m or 2h. Running and finished sandboxes. |
--follow | Keep printing new decisions as table rows, for a running sandbox. With --deny, denies only. With --raw, every line as the runtime writes it. |
--raw | Everything the sandbox logged, as the runtime wrote it: settings changes, policy loads, closed connections. |
--json | The decisions as one JSON list instead of the table. With --follow, one JSON object per line as each decision arrives. Not with --raw. See Output for scripts. |
Reads the gateway's log stream for a running sandbox, and the copy saved at exit for a finished one. Fenzi writes that copy before it deletes the sandbox, because deleting the sandbox deletes the gateway's log. The table is network decisions only: time, allow or deny, program, destination, and rule. A deny's reason is in --deny. Query strings are left out. A blocked file write is not in the log; see What the log does not hold. A reset line is a connection closed because the policy changed; the program reconnects, and --deny leaves it out.
fenzi diff [--sandbox <name>] [--stat]
Lists staged changes against your working tree with line counts, including deleted files, then shows each file's changed lines. Long output goes through $PAGER, or less. --stat prints the list only.
fenzi apply [--sandbox <name>] [--yes]
Lists the staged changes, then asks: apply them, show them, or keep them for later. Without a terminal it asks [y/N] on one line. --yes applies without asking. It refuses if a file changed on your side since the agent started, and lists those files.
When an agent exits, or after fenzi stop, the same list and question appear in the terminal. Keeping the changes for later leaves your folder as it was. After an apply, a sandbox that keeps running is compared against what was applied, so the next stop brings back only what the agent changed since.
Policy
compile and check read the policy fenzi run would use unless you name a file: ./fenzi.yaml merged over the built-in default, or the team's signed revision on a signed-in machine. --agent picks the agent to compile for (default claude). --org treats the file as a complete policy instead of a project file.
fenzi policy compile [file] [-o out.yaml]
Prints the OpenShell policy the file compiles to. Needs no gateway.
fenzi policy check [file] [--boundary file] [--json]
Runs the policy prover and prints the result with a counterexample if there is one. Needs no gateway. Exits 2 unless the result is within_boundary.
What it calls upstream
openshell-prover check.
fenzi policy apply [--policy file]
Free plan. After you edit ./fenzi.yaml, brings the change to the sandboxes already running from this folder. It resolves the policy the way run does and compares it with the one each sandbox started with, which run saves in the sandbox's run folder. A change to hosts only is compiled, checked with the prover, and loaded onto the running sandbox; the line says when policy list shows Loaded, and the new policy replaces the saved one. A change to workspace access or paths is fixed when a sandbox is created: nothing changes, and the line says it takes effect on the next run. A sandbox started by a release that did not save its policy is left as it is. Exits 2 when the prover does not return within_boundary; nothing is loaded then. On a signed-in machine it refuses: the team policy comes from the console, and fenzi sync loads a published host change.
What it calls upstream
openshell sandbox list --names --selector fenzi=1, openshell-prover check, openshell policy set --wait, openshell policy list.
Agents
fenzi agents [--json]
Lists the agents in the catalog, their pinned versions, and their test status.
fenzi agents build <agent>
Builds or rebuilds the agent's image with Docker or Podman, whichever the gateway uses. See Agent images.
Update
fenzi update [--check] [--json]
Reads https://fenzi.dev/dl/latest.txt. If it names a newer release, downloads the binary for this machine, checks it against that release's sha256 list, and replaces the running command. Nothing changes if the checksum does not match. --check only reports. Running sandboxes are not touched. Commands also mention a newer release on their own, at most once a day; see The update check.
fenzi version [--json]
Prints the release of this binary.
Team commands
These commands are for the Team plan. All but login need a machine signed in to a team.
fenzi login [--server URL]
Prints a code and a page to open, https://fenzi.dev/device unless --server names another console. Signed in to the console, you enter the code, check the machine name, and approve. The machine caches the signed policy. A later fenzi run uses that policy. fenzi.yaml in the project does not add hosts on top of it.
fenzi logout
Forgets the sign-in and the cached policy. A sandbox that is already running keeps the policy it started with.
fenzi status [--json]
Shows who is signed in, the team, and the cached revision. If the console cannot be reached, or signed this machine out, it says so and keeps the cached policy after checking the signature again. If the console sends a revision that fails the signature check, or one older than the cached revision, the machine keeps the cached one, checks it again, and says so: the server sent r3, which failed the signature check; still on r2. The heartbeat carries the same note to the console's Drift page. With no policy cached, such a revision is refused and the command fails. run, sync, and forward behave the same way.
fenzi policy publish <file>
Sends a complete org policy to the console. It compiles it for every agent it names, runs the prover, and refuses it unless the result is within_boundary. Only an admin can publish. The new revision is cached on this machine. The same action is on the console's Policy page, which also shows the check before you publish.
fenzi sync
One pass: it fetches the team's newest policy revision, loads any host change onto this machine's running sandboxes, and sends the decisions waiting to go. If the new revision only changes hosts, the pass compiles it for each running sandbox and checks it with the prover, as run does before it creates a sandbox. Only a result of within_boundary is loaded, and the pass says when policy list shows Loaded. Otherwise, or when the prover is not installed, the sandbox keeps the revision it has and the pass says why. A change to paths, workspace access, or agents still needs a new sandbox: those sandboxes keep the policy they started with, and the next run creates a new one.
fenzi forward [--once] [--every <duration>]
Sends this machine's decisions, pending requests, and a report on the machine to the team console, and applies the reviews made there. The console does not connect to this machine. Without --once it repeats the pass every five seconds until you stop it; --every 30s changes that. If the console is unreachable, records wait on this machine and the pass still exits 0. What one pass does, gaps, and the local queue are on Forwarding decisions.
Output for scripts
--json prints one JSON document on standard output and nothing else there. Errors stay on standard error with the same exit codes. The text output does not change. Field names are lower case with underscores, as in key_env.
fenzi ps --json[
{
"name": "bl-web-4f2a",
"agent": "claude",
"folder": "/Users/dana/web",
"created": "2026-10-03T10:41:52Z",
"running": true
}
]| Command | The document |
|---|---|
doctor | checks, one per line of the text output, each with name, status (ok, fail, or skip), and detail; and ready, false when a check failed. |
version | version. |
agents | A list, one per agent: name, display, version, key_env, status, available. |
ps | A list, one per sandbox: name, agent, folder, created (RFC 3339, empty for a sandbox Fenzi did not start), running. |
requests | A list, one per request: id in full, destination, binary, rationale, draft. Drafts are in it only with --all. Empty when nothing waits. If the runtime's text format changed, it exits 1 rather than print the raw text. |
approve, deny | id, sandbox, result (approved or denied), note. approve --save --dry-run prints the preview described above instead. |
log | A list of decision records, the ones the forwarder ships before it adds the machine and policy revision: time, sandbox, class, action (Allowed or Denied), destination, then process, rule, and reason when the runtime logged them. A reset is a denied record whose reason says the policy changed. --deny keeps denies only, without resets. --follow prints one object per line as each decision arrives, and nothing for other log lines. |
key list | A list, one per agent: name, display, key_env, stored, available. Never a key. |
rm | removed, one per sandbox forgotten: name, staged_changes, saved_log; and refused, one per sandbox still running: name, reason. Exits 1 when anything was refused, as without the flag. |
status | signed_in, email, role, team, machine, server, console_url, policy_revision, notes. Signed out, signed_in is false and the rest is empty. |
policy check | passed, result (the prover's word, such as within_boundary), checked, agent, example, reason, not_checked. Exits 2 unless it passed, as without the flag. |
update | current, latest, available, and installed with the path written after an install. --check only reports. |
log --raw and policy compile do not take it. --raw is the runtime's own lines, and compile already prints YAML. Each says so and exits 1.
Exit codes
| Code | Meaning |
|---|---|
0 | Success. |
1 | Error. The message says what failed. |
2 | The policy check did not return within_boundary. Nothing started or loaded. |
3 | OpenShell is missing, outside the pinned range, or not connected. |
run and connect return the agent's own exit code after the agent has started. Ctrl-C at a question returns 130.
Files
| Path | Holds |
|---|---|
./fenzi.yaml | The project policy. |
~/.config/fenzi/enrollment.json | This machine's sign-in and the cached signed policy. |
~/.config/fenzi/forward.json | Where the forwarder stopped reading each sandbox's stream, and which finished sandboxes' saved logs it has sent. |
~/.config/fenzi/spool.jsonl | Decision records waiting to upload. Team only. Removed when empty. |
~/.cache/fenzi/staging/ | Changes copied out of sandboxes, waiting for apply. |
~/.cache/fenzi/runs/<sandbox>/ | One folder per run: the compiled policy, the boundary, the policy the sandbox actually got, resolved.yaml, the policy document it started with, and sandbox.log, the log saved when the agent exited. |
Model keys are not in any of these. They are in the gateway's provider store.