Saphan StudioDocs
Reference/Commands

saphan machine

Manages the machine registry: the signed list of hosts admitted to run work, their slots, and the seats (AI assistant accounts) placed on those slots.

Manages the machine registry: the signed list of hosts admitted to run work, their slots, and the seats (AI assistant accounts) placed on those slots. Admitting, renewing, retiring and seating are the owner's recorded acts, signed with the root key; every change is a new row, and the history is never rewritten. Admitting, renewing, retiring, pinning, unpinning and every machine set-* change also ask for the owner's one-time terminal code, as each section says; machine seat and machine unseat are signed but ask for no code.

saphan machine list
saphan machine admit --machine m1 --host-ref vm-linux-1 --transport ssh --ssh-host build1.example.com \
  --csr m1.csr --admitted-by alice
saphan machine seat --machine m1 --slot 0 --identity seat-a --added-by alice
saphan machine

saphan machine admit

Admits a machine to the signed registry, once per machine, with a short-lived certificate either issued now from the machine's certificate request (--csr) or supplied ready-made (--cert). An expired certificate, or a certificate issued for a machine name other than --machine, is refused. Re-admitting a live machine with the same parameters changes nothing and succeeds; with different parameters it is refused (machine-already-admitted) and the refusal names the differences. A retired machine can be admitted again as a new row. For an SSH machine the key is tested with a real connection before anything is written. ⚠ If your own ~/.ssh/known_hosts has no entry for an SSH machine, admission accepts the host key the network presents on first contact, and it does this for every unpinned SSH machine in the registry, not only the one being admitted; connect to each host yourself and check its key first, then use machine pin. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine admit
flagtypedefaultwhat it does
--admitted-atstringThe admission time in RFC 3339; defaults to now.
--admitted-bystringThe owner performing the admission; required.
--binarystringArrayRegisters the path of an assistant program on that machine as <backend>=<absolute path>, for example claude-code=/home/me/.local/bin/claude; repeatable. Programs are never looked up on PATH.
--certstringA certificate file (PEM or DER) that was already issued, used as is; use this or --csr.
--cert-outstringWrites the admitted certificate (never a key) to this path so it can be delivered to the machine; without it only the fingerprint is kept.
--csrstringThe machine's certificate request (PEM or DER), from which a fresh short-lived certificate is issued; use this or --cert.
--device-keystringWhere --mint-device-key writes the new device private key; defaults to ~/.saphan/machine/device.key.
--force-unverifiedboolfalseWrites the machine row even if the SSH key fails its test connection, marking the row as unverified; SSH machines only.
--gomodcache-rootstringAbsolute path on that machine of the parent directory of its Go module cache, which Codex runs there must stay under; can be set later with machine renew.
--host-refstringA host reference for the machine, for example vm-linux-1; required.
--intermediate-certstringPath of the intermediate CA certificate used with --csr; defaults to ~/.saphan/trust/intermediate.crt.
--intermediate-keystringPath of the intermediate CA private key used with --csr; defaults to ~/.saphan/trust/intermediate.key.
--machinestringThe machine ID to admit, for example m1; required.
--max-sessionsint1Retired; a seat runs one job at a time, and passing this flag is refused.
--mint-device-keyboolfalseAdmits this machine in one step: creates its device key and request, issues its certificate and admits it. Local transport only; cannot be combined with --csr or --cert. The device key and certificate are written before the owner code is asked; if the confirmation is refused they stay on disk, and a retry is refused with machine-device-key-exists until you remove them.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--slotsint1How many seats the machine can hold; defaults to 1.
--ssh-hoststringThe SSH host name; required with --transport ssh.
--ssh-keystringThe SSH private key file; defaults to ~/.ssh/id_ed25519, then ~/.ssh/id_rsa.
--ssh-portint22The SSH port; defaults to 22.
--ssh-userstring<current user>The SSH user name; defaults to your current user.
--transportstringHow Saphan Studio reaches the machine: local or ssh. Required unless --mint-device-key is given, which implies local.
--ttlduration24h0m0sLifetime of a certificate issued from --csr; defaults to 24 hours.
--worktrees-rootstringAbsolute path on that machine of the directory where stream worktrees are created; can be set later with machine set-worktrees-root.

saphan machine adopt-cert

Runs on the machine itself and switches it to a newly delivered certificate (device.next.crt), after checking that the certificate is valid now and carries the same machine name. Either failure leaves the current certificate in place. Afterwards, compare the printed reference with the machine's entry in saphan machine list --json on the control host.

saphan machine adopt-cert
flagtypedefaultwhat it does
--dirstringThis machine's own trust directory, holding device.crt and device.next.crt; defaults to ~/.saphan/machine.

saphan machine csr

Runs on the machine being admitted: creates its device key, which never leaves the machine, and writes a certificate request to carry to the owner for machine admit or machine renew. It refuses to overwrite an existing key unless --reuse-key is given.

saphan machine csr
flagtypedefaultwhat it does
--csr-outstringWhere to write the certificate request; defaults to <machine-id>.csr in the current directory.
--machinestringThe machine ID the request carries; must match the ID used at admission. Required.
--outstringPath of the device private key: written when a new key is created, only read with --reuse-key. Defaults to ~/.saphan/machine/device.key.
--reuse-keyboolfalseBuilds the request from the existing device key instead of creating a new one, to renew a certificate before it expires; the key file is not modified.

saphan machine deliver-branch

Usage: saphan machine deliver-branch <machine> --repo <repo> --branch <branch>. Delivers one named local branch to the remote machine named as the argument: it updates the machine's admission mirror, then its shared clone, then fast-forwards that clone's branch so new worktrees start from the delivered state. It never force-pushes, deletes or pushes to your origin, and it refuses by name when a branch has diverged, has uncommitted work, or cannot be checked. Rehearse with saphan dry-run machine deliver-branch.

saphan machine deliver-branch <machine>
flagtypedefaultwhat it does
--branchstringThe local branch to deliver; required.
--repostringThe repository, as an absolute path or its workspace name; selects both the local source and that machine's mirror for it. Required.

saphan machine list

Lists admitted machines with their transport, slots, who admitted them and when, and the seats on each slot. It only reads the registry.

saphan machine list

saphan machine login

Logs a seat in to its assistant vendor account, in the seat's own profile directory, and then verifies the login with a real request. It finds the profile directory from the registry, so you never type the path, and runs the vendor's login over SSH with a terminal for a remote seat. It never writes to the registry; an abandoned or failed login is reported as machine-login-abandoned, and a login that completes but fails verification as machine-login-unverified, and either leaves the seat as it was. Without a terminal, a login is refused unless --api-key-stdin is given.

saphan machine login
flagtypedefaultwhat it does
--api-key-stdinboolfalseFor a metered Claude Code, Codex or Droid seat: reads one API key from standard input and binds it to this seat. Local machines only. For Claude Code and Droid the key is stored in an owner-only file in the seat's profile directory; it never appears in arguments, the registry, reports or logs.
--identitystringWhich seat to log in, when the machine has more than one; an ambiguous choice is refused.
--machinestringThe machine the seat is on; required.
--slotint0The seat's slot, if you name it; must agree with the slot the seat is recorded on.
--verify-onlyboolfalseChecks the seat's existing login without logging in again, and refuses by name if it is not usable.
--verify-timeoutduration2m0sHow long the verification request may take; defaults to two minutes.

saphan machine observe

Shows what a machine is running right now, read from the machine itself: whether it is reachable, its uptime and boot time, and the assistant processes it is running, alongside what the run record says.

saphan machine observe --machine <machine-id>
flagtypedefaultwhat it does
--jsonboolfalsePrints the result as JSON.
--machinestringThe registered machine to read.

saphan machine pin

Copies an SSH host key you already trust, from your own ~/.ssh/known_hosts, into Saphan Studio's host-key store for machines admitted before that store existed. It never scans the network or accepts a key on first contact; if you have no entry, connect to the host once yourself and re-run it. A revoked key is never adopted. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine pin
flagtypedefaultwhat it does
--allboolfalsePins every SSH machine in the registry and reports each one's outcome.
--machinestringThe machine to pin; use this or --all.

saphan machine probe

Runs one check from a fixed catalog against a toolchain the machine declares, such as gradle, and records the signed result (outcome, observed version and expiry) in the registry. It never installs anything, and a capability outside the catalog is refused.

saphan machine probe
flagtypedefaultwhat it does
--capabilitystringThe capability to check, from the fixed catalog; required.
--declared-pathstringThe absolute path the machine declares for that tool; the check proves or disproves it. Required.
--declared-versionstringThe version the machine declares, if any, kept as provenance only.
--machinestringThe machine whose registry entry decides where the check runs, locally or over SSH; required.
--probed-bystringm0Who ran the check, recorded with the result; always give it.
--root-keystringPath of the root private key used to sign the result; defaults to ~/.saphan/trust/root.key.
--workhomestringThe machine's absolute work directory, where the check runs; required.

saphan machine probe-confinement

Measures, on the machine itself, whether it can confine a run's filesystem access (Landlock on Linux, the macOS sandbox on macOS) and cut or redirect its network access, and records the result on the machine's signed row. The value can only come from this measurement; it cannot be typed in. "No" (measured and refused by the system) and "unmeasured" (the check could not run) are recorded separately, and nothing is installed or changed on the host.

saphan machine probe-confinement <machine>
flagtypedefaultwhat it does
--machinestringThe machine to measure, as an alternative to the positional argument; it must already be admitted.
--probed-bystringm0Who ran the measurement, recorded with the result; always give it.
--root-keystringPath of the root private key used to sign the result; defaults to ~/.saphan/trust/root.key.

saphan machine refresh-repo

Usage: saphan machine refresh-repo <machine> --repo <repo>. Brings one base branch up to date on the remote machine named as the argument: it pushes that branch to the machine's admission mirror, then fetches it into the shared clone and fast-forwards the clone's branch, so new worktrees start from the current base. It never force-pushes, prunes or touches your origin, and it refuses by name when the branch has diverged, has uncommitted work, or cannot be checked. Rehearse with saphan dry-run machine refresh-repo.

saphan machine refresh-repo <machine>
flagtypedefaultwhat it does
--basestringThe base branch to refresh; defaults to the branch the repository's landing worktree holds, and is refused if that cannot be determined.
--repostringThe repository, as an absolute path or its workspace name; selects both the local source and that machine's mirror for it. Required.

saphan machine renew

Updates an admitted machine without re-admitting it: issues or installs a new certificate over the same device key, changes its SSH connection details, or sets its Go module cache directory. The machine must already be in the registry, and a renewal that changes nothing is refused. Rehearse with saphan dry-run machine renew, which runs the same checks and writes nothing. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine renew
flagtypedefaultwhat it does
--certstringA certificate file (PEM or DER) already issued for the same device key; use this or --csr to replace the certificate.
--cert-outstringWrites the new certificate (never a key) to this path so it can be delivered to the machine; until the machine adopts it, it keeps presenting the old one.
--csrstringA certificate request from the same device key, from which a fresh certificate is issued; use this or --cert to replace the certificate.
--force-unverifiedboolfalseWrites the renewed row even if the SSH key fails its test connection, marking the row as unverified; SSH machines only.
--gomodcache-rootstringAbsolute path on that machine of the parent directory of its Go module cache; on its own it is still a valid renewal, and omitted it keeps the current value.
--intermediate-certstringPath of the intermediate CA certificate used with --csr; defaults to ~/.saphan/trust/intermediate.crt.
--intermediate-keystringPath of the intermediate CA private key used with --csr; defaults to ~/.saphan/trust/intermediate.key.
--machinestringThe machine to renew; required.
--renewed-atstringThe renewal time in RFC 3339; defaults to now.
--renewed-bystringThe owner performing the renewal; required.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--ssh-hoststringA new SSH host name; omitted, the current one is kept.
--ssh-keystringA new SSH private key file; omitted, the current one is kept.
--ssh-portint0A new SSH port; omitted, the current one is kept.
--ssh-userstringA new SSH user name; omitted, the current one is kept.
--ttlduration24h0m0sLifetime of a certificate issued from --csr; defaults to 24 hours.

saphan machine retire

Withdraws an admitted machine from service: it stays in the registry, marked retired, and no run can be placed on it. Retiring does not touch its certificate or keys, so the certificate stays valid until it expires. It is refused while seats are still placed on the machine or a run is recorded as live there. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine retire
flagtypedefaultwhat it does
--machinestringThe machine to retire; required.
--retired-bystringThe owner performing the retirement; required.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.

saphan machine seat

Places a seat (an assistant identity, such as seat-a) on a slot of an admitted machine, so runs can be placed there. The slot must exist and be free, and moving a seat from another machine requires --move-from. A seat has either a profile directory (Claude Code, Codex, Qwen Code, Droid, Cursor) or a configured lane (OpenAI-compatible and agent-loop backends), never both.

saphan machine seat
flagtypedefaultwhat it does
--added-atstringThe seating time in RFC 3339; defaults to now.
--added-bystringThe owner performing the seating; required.
--backendstringclaude-codeWhich assistant the seat runs: claude-code (default), codex, openai-compat, agent-loop, qwen-code, droid or cursor. A run asking for a different backend on this seat is refused.
--billing-classstringHow the seat's vendor account is billed: subscription, metered or local-energy. Omitted, it reads as unknown and is never assumed.
--config-dirstringThe seat's profile directory on that machine; defaults to $HOME/.claude-<identity> (or $HOME/.codex-<identity> for Codex). Not allowed for lane backends.
--identitystringThe seat's identity, for example seat-a; required.
--lanestringThe configured lane the seat's credential comes from on its own host; required for openai-compat and agent-loop, refused for the others.
--machinestringThe machine to seat on; required, and it must already be admitted.
--move-fromstringThe machine the identity is currently seated on, when moving it from another machine; makes the move explicit.
--rolestringexecutorThe role the seat is reserved for: master, executor (default), review, design, scribe or advisor. executor accepts any run; any other role accepts only runs declaring that role.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--slotint0The slot index on the machine; must be within its slot count.

saphan machine set-account

Records which vendor account a seat spends, as an opaque grouping key, so Saphan Studio can count live runs across all seats sharing one account. It makes no network call and costs nothing; run it once per seat, with the same key for seats on the same account. The identity must already be seated, or the command is refused. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-account
flagtypedefaultwhat it does
--accountstringAn opaque key that is equal for seats sharing an account, such as a digest of the vendor's account ID. Values containing @ or longer than 128 bytes are refused, since this is not for credentials or email addresses. Required.
--added-bystringThe owner performing the act; required.
--identitystringThe seat whose vendor account is being recorded; required.
--root-keystringPath of the root private key; defaults to the installation's own.

saphan machine set-artifact-root

Records the directory on a machine where all non-source output (runtime state, evidence, build caches, release output) is kept, outside every code repository. The path is checked on the machine itself before it is recorded: a directory inside a Git repository, or a machine that cannot be reached, is refused. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-artifact-root
flagtypedefaultwhat it does
--actorstringThe actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration.
--machinestringThe machine to update; required, and it must already be admitted.
--rootstringThe absolute directory on that machine, outside every Git repository; required.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.

saphan machine set-binary

Records the path of each assistant program on a machine, and optionally pins the checksum of its saphan-agent program. Entries are merged, so registering one backend keeps the others. With a pin, every run started through saphan-agent first measures the program on that machine and refuses on a mismatch; a machine with no pin is not checked. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-binary
flagtypedefaultwhat it does
--actorstringThe actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration.
--agent-digeststringThe expected checksum of the machine's saphan-agent, as sha256:<64 hex>; measure it on that machine with sha256sum ~/.saphan/bin/saphan-agent.
--binarystringArrayAn assistant program's path on that machine as <backend>=<absolute path>; repeatable.
--machinestringThe machine to update; required, and it must already be admitted.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.

saphan machine set-container-runtime

Records a machine's container runtime path and the image, pinned by digest, that container runs use there. This only declares the values; whether the machine can actually run the image is proved separately with machine probe --capability docker-oci. All three flags are required together. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-container-runtime
flagtypedefaultwhat it does
--actorstringThe actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration.
--imagestringThe image repository without a tag or digest, for example ghcr.io/example/dev-go; required.
--image-digeststringThe SHA-256 digest that pins the image, with or without the sha256: prefix; required.
--machinestringThe machine to update; required, and it must already be admitted.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--runtime-pathstringThe absolute path of the container runtime on that machine, for example /usr/local/bin/docker; required.

saphan machine set-price

Records a machine's hourly rate in US dollars, which Saphan Studio uses as a tie-breaker when choosing where to run work, after capability and availability. Every machine starts at 0; a negative rate, or the current rate again, is refused. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-price
flagtypedefaultwhat it does
--added-bystringThe owner performing the change; required.
--machinestringThe machine to update; required, and it must already be admitted.
--price-usd-per-hourfloat640The hourly rate in US dollars, 0 or more; required.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.

saphan machine set-repo-dir

Records the absolute path of the bare admission repository on a machine, either for the machine as a whole or, with --repo, for one repository. Once a machine names any repository this way, work for a repository it does not name is refused rather than sent to the wrong mirror. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-repo-dir
flagtypedefaultwhat it does
--actorstringThe actor recorded for this change.
--machinestringThe machine to update; required, and it must already be admitted.
--repostringWhich repository this path mirrors, by its workspace name; omitted, the path applies to the machine as a whole.
--repo-dirstringThe absolute path of the bare repository on that machine; required.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.

saphan machine set-slots

Changes how many seats an admitted machine can hold. A count below 1, equal to the current count, or one that would leave any seat on a slot at or beyond the new count is refused; the refusal names those seats. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-slots
flagtypedefaultwhat it does
--added-bystringThe owner performing the change; required.
--machinestringThe machine to update; required, and it must already be admitted.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--slotsint0The new slot count; required, at least 1, and above the highest slot that holds a seat.

saphan machine set-worktrees-root

Records the absolute directory on a machine where stream worktrees are created, from which Saphan Studio derives each run's working directory there. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine set-worktrees-root
flagtypedefaultwhat it does
--actorstringThe actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration.
--machinestringThe machine to update; required, and it must already be admitted.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--worktrees-rootstringThe absolute directory on that machine; required.

saphan machine trust-stamp

Marks a project directory as trusted in a Claude Code seat's own configuration, so the assistant does not stop at its interactive trust prompt, or removes that mark with --revoke. Only directories inside an owner-declared location are accepted: your own trusted directories for a local machine, or the machine's recorded worktrees directory for an SSH machine. All other content of the seat's file is preserved, and nothing is written without a terminal confirmation or --yes. Rehearse with saphan dry-run machine trust-stamp.

saphan machine trust-stamp
flagtypedefaultwhat it does
--actorstringWho is acting, recorded with the mark; required.
--forceboolfalseWith --revoke, removes the mark even if the directory is still inside its permitted location.
--identitystringThe seat to mark; required.
--machinestringThe machine the seat is on; required.
--revokeboolfalseRemoves a mark this command placed earlier instead of adding one.
--rootstringThe project directory to mark; defaults to the one resolved from the current directory.
--yesboolfalseWrites without an interactive prompt; required when not running in a terminal.

saphan machine unpin

Removes the stored SSH host key for one machine from Saphan Studio's host-key store, for example after the machine was rebuilt or the wrong key was pinned. It only removes: the next connection is treated as a first contact. ⚠ The next machine admit of any machine re-pins this host from the network, accepting whatever key it presents, unless you first add a checked entry to your own ~/.ssh/known_hosts and run machine pin. Running it on a machine with no pin is reported and succeeds. ⚠ This is an owner act: it prints a one-time code on this terminal and waits for you to type it back. It is refused when no terminal is attached (for example from a script or a CI job), and in the console's browser terminal it is refused in favour of the console's passkey prompt.

saphan machine unpin
flagtypedefaultwhat it does
--machinestringThe machine whose host key to remove; required.

saphan machine unseat

Frees one slot by removing the seat placed on it, as a new registry row; the identity itself keeps existing and can be seated again. It is refused if the identity is not seated on that slot (the refusal says where it is) or if a run is recorded as live there.

saphan machine unseat
flagtypedefaultwhat it does
--identitystringThe seat's identity to remove, for example seat-c; required.
--machinestringThe machine the seat is on; required, and it must already be admitted.
--removed-bystringThe owner freeing the slot; required.
--root-keystringPath of the root private key; defaults to ~/.saphan/trust/root.key.
--slotint0The slot index the identity occupies; required, and never assumed, since slot 0 is a real slot.

Global options apply to every command — see Every command.