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 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.
| flag | type | default | what it does |
|---|---|---|---|
--admitted-at | string | The admission time in RFC 3339; defaults to now. | |
--admitted-by | string | The owner performing the admission; required. | |
--binary | stringArray | Registers 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. | |
--cert | string | A certificate file (PEM or DER) that was already issued, used as is; use this or --csr. | |
--cert-out | string | Writes the admitted certificate (never a key) to this path so it can be delivered to the machine; without it only the fingerprint is kept. | |
--csr | string | The machine's certificate request (PEM or DER), from which a fresh short-lived certificate is issued; use this or --cert. | |
--device-key | string | Where --mint-device-key writes the new device private key; defaults to ~/.saphan/machine/device.key. | |
--force-unverified | bool | false | Writes the machine row even if the SSH key fails its test connection, marking the row as unverified; SSH machines only. |
--gomodcache-root | string | Absolute 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-ref | string | A host reference for the machine, for example vm-linux-1; required. | |
--intermediate-cert | string | Path of the intermediate CA certificate used with --csr; defaults to ~/.saphan/trust/intermediate.crt. | |
--intermediate-key | string | Path of the intermediate CA private key used with --csr; defaults to ~/.saphan/trust/intermediate.key. | |
--machine | string | The machine ID to admit, for example m1; required. | |
--max-sessions | int | 1 | Retired; a seat runs one job at a time, and passing this flag is refused. |
--mint-device-key | bool | false | Admits 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-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--slots | int | 1 | How many seats the machine can hold; defaults to 1. |
--ssh-host | string | The SSH host name; required with --transport ssh. | |
--ssh-key | string | The SSH private key file; defaults to ~/.ssh/id_ed25519, then ~/.ssh/id_rsa. | |
--ssh-port | int | 22 | The SSH port; defaults to 22. |
--ssh-user | string | <current user> | The SSH user name; defaults to your current user. |
--transport | string | How Saphan Studio reaches the machine: local or ssh. Required unless --mint-device-key is given, which implies local. | |
--ttl | duration | 24h0m0s | Lifetime of a certificate issued from --csr; defaults to 24 hours. |
--worktrees-root | string | Absolute 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.
| flag | type | default | what it does |
|---|---|---|---|
--dir | string | This 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.
| flag | type | default | what it does |
|---|---|---|---|
--csr-out | string | Where to write the certificate request; defaults to <machine-id>.csr in the current directory. | |
--machine | string | The machine ID the request carries; must match the ID used at admission. Required. | |
--out | string | Path of the device private key: written when a new key is created, only read with --reuse-key. Defaults to ~/.saphan/machine/device.key. | |
--reuse-key | bool | false | Builds 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.
| flag | type | default | what it does |
|---|---|---|---|
--branch | string | The local branch to deliver; required. | |
--repo | string | The 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 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.
| flag | type | default | what it does |
|---|---|---|---|
--api-key-stdin | bool | false | For 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. |
--identity | string | Which seat to log in, when the machine has more than one; an ambiguous choice is refused. | |
--machine | string | The machine the seat is on; required. | |
--slot | int | 0 | The seat's slot, if you name it; must agree with the slot the seat is recorded on. |
--verify-only | bool | false | Checks the seat's existing login without logging in again, and refuses by name if it is not usable. |
--verify-timeout | duration | 2m0s | How 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.
| flag | type | default | what it does |
|---|---|---|---|
--json | bool | false | Prints the result as JSON. |
--machine | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--all | bool | false | Pins every SSH machine in the registry and reports each one's outcome. |
--machine | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--capability | string | The capability to check, from the fixed catalog; required. | |
--declared-path | string | The absolute path the machine declares for that tool; the check proves or disproves it. Required. | |
--declared-version | string | The version the machine declares, if any, kept as provenance only. | |
--machine | string | The machine whose registry entry decides where the check runs, locally or over SSH; required. | |
--probed-by | string | m0 | Who ran the check, recorded with the result; always give it. |
--root-key | string | Path of the root private key used to sign the result; defaults to ~/.saphan/trust/root.key. | |
--workhome | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--machine | string | The machine to measure, as an alternative to the positional argument; it must already be admitted. | |
--probed-by | string | m0 | Who ran the measurement, recorded with the result; always give it. |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--base | string | The base branch to refresh; defaults to the branch the repository's landing worktree holds, and is refused if that cannot be determined. | |
--repo | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--cert | string | A certificate file (PEM or DER) already issued for the same device key; use this or --csr to replace the certificate. | |
--cert-out | string | Writes 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. | |
--csr | string | A certificate request from the same device key, from which a fresh certificate is issued; use this or --cert to replace the certificate. | |
--force-unverified | bool | false | Writes the renewed row even if the SSH key fails its test connection, marking the row as unverified; SSH machines only. |
--gomodcache-root | string | Absolute 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-cert | string | Path of the intermediate CA certificate used with --csr; defaults to ~/.saphan/trust/intermediate.crt. | |
--intermediate-key | string | Path of the intermediate CA private key used with --csr; defaults to ~/.saphan/trust/intermediate.key. | |
--machine | string | The machine to renew; required. | |
--renewed-at | string | The renewal time in RFC 3339; defaults to now. | |
--renewed-by | string | The owner performing the renewal; required. | |
--root-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--ssh-host | string | A new SSH host name; omitted, the current one is kept. | |
--ssh-key | string | A new SSH private key file; omitted, the current one is kept. | |
--ssh-port | int | 0 | A new SSH port; omitted, the current one is kept. |
--ssh-user | string | A new SSH user name; omitted, the current one is kept. | |
--ttl | duration | 24h0m0s | Lifetime 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.
| flag | type | default | what it does |
|---|---|---|---|
--machine | string | The machine to retire; required. | |
--retired-by | string | The owner performing the retirement; required. | |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--added-at | string | The seating time in RFC 3339; defaults to now. | |
--added-by | string | The owner performing the seating; required. | |
--backend | string | claude-code | Which 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-class | string | How the seat's vendor account is billed: subscription, metered or local-energy. Omitted, it reads as unknown and is never assumed. | |
--config-dir | string | The seat's profile directory on that machine; defaults to $HOME/.claude-<identity> (or $HOME/.codex-<identity> for Codex). Not allowed for lane backends. | |
--identity | string | The seat's identity, for example seat-a; required. | |
--lane | string | The configured lane the seat's credential comes from on its own host; required for openai-compat and agent-loop, refused for the others. | |
--machine | string | The machine to seat on; required, and it must already be admitted. | |
--move-from | string | The machine the identity is currently seated on, when moving it from another machine; makes the move explicit. | |
--role | string | executor | The 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-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--slot | int | 0 | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--account | string | An 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-by | string | The owner performing the act; required. | |
--identity | string | The seat whose vendor account is being recorded; required. | |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--actor | string | The actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--root | string | The absolute directory on that machine, outside every Git repository; required. | |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--actor | string | The actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration. | |
--agent-digest | string | The expected checksum of the machine's saphan-agent, as sha256:<64 hex>; measure it on that machine with sha256sum ~/.saphan/bin/saphan-agent. | |
--binary | stringArray | An assistant program's path on that machine as <backend>=<absolute path>; repeatable. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--actor | string | The actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration. | |
--image | string | The image repository without a tag or digest, for example ghcr.io/example/dev-go; required. | |
--image-digest | string | The SHA-256 digest that pins the image, with or without the sha256: prefix; required. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--root-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--runtime-path | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--added-by | string | The owner performing the change; required. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--price-usd-per-hour | float64 | 0 | The hourly rate in US dollars, 0 or more; required. |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--actor | string | The actor recorded for this change. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--repo | string | Which repository this path mirrors, by its workspace name; omitted, the path applies to the machine as a whole. | |
--repo-dir | string | The absolute path of the bare repository on that machine; required. | |
--root-key | string | Path 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.
| flag | type | default | what it does |
|---|---|---|---|
--added-by | string | The owner performing the change; required. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--root-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--slots | int | 0 | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--actor | string | The actor recorded for this change; defaults to SAPHAN_ACTOR, then your Saphan configuration. | |
--machine | string | The machine to update; required, and it must already be admitted. | |
--root-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--worktrees-root | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--actor | string | Who is acting, recorded with the mark; required. | |
--force | bool | false | With --revoke, removes the mark even if the directory is still inside its permitted location. |
--identity | string | The seat to mark; required. | |
--machine | string | The machine the seat is on; required. | |
--revoke | bool | false | Removes a mark this command placed earlier instead of adding one. |
--root | string | The project directory to mark; defaults to the one resolved from the current directory. | |
--yes | bool | false | Writes 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.
| flag | type | default | what it does |
|---|---|---|---|
--machine | string | The 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.
| flag | type | default | what it does |
|---|---|---|---|
--identity | string | The seat's identity to remove, for example seat-c; required. | |
--machine | string | The machine the seat is on; required, and it must already be admitted. | |
--removed-by | string | The owner freeing the slot; required. | |
--root-key | string | Path of the root private key; defaults to ~/.saphan/trust/root.key. | |
--slot | int | 0 | The 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.