Saphan StudioDocs
Reference/Command reference

saphan run

Spawn one mandated run through the engine's spawn door (runner-v1; operator verb)

saphan run

Spawn one mandated run through the engine's spawn door (runner-v1; operator verb)

saphan run [flags]

Spawn a process under mandate and record the run: one door for agent sessions and arbitrary executables alike — one mandate enforcement, one transcript discipline, one cost ledger, one identity seam.

Backends (CONTRACT_runner.md §5, PLAN_OPENAI_COMPAT.md, PLAN_agent-loop_v0): proc (substrate — any executable), claude-code (headless Claude Code CLI), codex (headless Codex CLI), openai-compat (in-process HTTP to a local craftsman speaking the OpenAI chat-completions dialect — LiteLLM proxy, or Ollama/vLLM directly; --base-url replaces --binary, which this backend does not use), agent-loop ('saphan-agent loop' — our own tool-calling agent loop, no vendor binary on its critical path; --lane names the config-ladder alias whose credential it resolves on ITS OWN execution host, --max-tokens/ --max-iterations/--inner-budget mirror its own flags). The model is mandatory for LLM backends and set by the runner, never left to session defaults; effort is enforced where the backend exposes it and its absence is recorded, never silently defaulted (agent-loop exposes no wire field for it at all, same class as openai-compat).

Refusals R1–R7 are RECORDED RunRecords (exit 3), never fallbacks: a plan-profile dispatch to a backend without two-pass refuses (R1, including openai-compat — chat completions are stateless); ephemeral modes are illegal for two-pass (R2); a resume targets the recorded session key only (R3, openai-compat has none to target); a missing model or a pinned-digest mismatch refuses (R4); bypass flags are outside the mandate vocabulary (R5); an effort-strict mandate refuses on codex until probe P4 lands, and always on openai-compat, which has no wire field for effort (R6); write access outside the workdir without a signed disposition refuses (R7, adr-0020); a --config permission class's commands.allow, present only for proc, refuses a binary+argv it does not list (R9) — a class only NARROWS what proc could already do, it never grants (T1 deny-by-default; internal/permissions). A schema-v2 class's Path/EnvMint is not R9's proc-only narrowing: it mints PATH and any declared env_mint into EVERY backend's composed environment, uniformly (spawn-capability-reach §1, adapter.go's compose() chokepoint).

--laws names the standing-law index every LLM spawn must deliver to the head (headless-law-injection L0): a headless dispatch fires no SessionStart hook of its own, so the engine reads the file FRESH at every spawn — including every resume — inlines it into the prompt, and records its SHA-256. A file that is missing, unreadable, not a regular file, or ZERO BYTES refuses R12 (recorded, exit 3, no process spawned): a head that cannot be handed its laws is not dispatched at all — an empty floor is not a floor. An OMITTED --laws on an LLM backend is a malformation (exit 1, the same class as an omitted brief), not a refusal; --laws on proc is unrepresentable (there is no head to bind).

Spawn authority (order §3): v0 = THIS operator verb. Timestamps are machine clock only. Captures land under .saphan/work/<stream>/runs/<run-id>/ with an engine-written gitignore cover. Secret VALUES never appear on any surface (§1.6): --env passes variable NAMES through, values are read at spawn and stored nowhere.

Options

OptionTypeDefaultDescription
--actorstringunsetmandate holder's id — never the process identity (falls back to env SAPHAN_ACTOR, then ~/.saphan/config's identity.actor)
--allow-unknown-binary-provenanceboolfalsetransport=ssh only: proceed when the remote binary's digest could not be established (no sha256sum/shasum on the far host, or the identity probe's dial failed) — the record still carries binary_sha256 "unknown", never a silent local substitute (R13); does NOT waive a confirmed absence (the binary does not exist, or is a directory, on the far host)
--argstringArray[]proc argv entry (repeatable, ordered)
--backendstringunsetbackend: proc | claude-code | codex | openai-compat | agent-loop | qwen-code (required, no default)
--base-refstringunsetbase branch name measured by remote admission-repo freshness checks (normally engine-composed from the order)
--base-urlstringunsetopenai-compat only: endpoint root (LiteLLM-proxy root or Ollama/vLLM root); /chat/completions is appended (required for openai-compat)
--binarystringunsetabsolute path (resolved on this host) or $HOME/… (resolved on the execution host for transport=ssh, on this host otherwise) — PATH resolution is forbidden (required, except openai-compat, which has none)
--binary-sha256stringunsetpin the binary digest; a mismatch refuses (R4 — no silent substitution)
--branchstringunsetstream branch this dispatch cuts its worktree on — the ssh dispatch-out act far side, git worktree add in the stream's own repository local side; absent = no worktree is created and the workdir must already exist (normally engine-composed from the order)
--briefstringunsetkickoff brief path for LLM backends (the path rides in the prompt, §7-S3)
--budget-usdfloat640claude-code only: --max-budget-usd limit
--configstringunsetpermission class (schema v1 or v2, internal/permissions): inline JSON, or with --file a path to a named class file (T2: production classes are named files) — commands.allow narrows what a proc spawn may run (R9, proc-only); a v2 entry's path/env_mint mints PATH/env into EVERY backend's spawn, not just proc; absent = no class, today's behavior unchanged (T1 deny-by-default)
--contactstringunsetWHO TO ASK about this stream — the order's contact: header line, verbatim; a ROUTING HINT and an unverified claim, never an identity and never an input to any authorisation or gating decision (--actor is who the run runs as); omitted records NO DECLARATION
--cost-estimate-usdfloat640OPTIONAL owner-quoted cost estimate for this run; absent = quoted stays NULL, never invented (honest-unknown, contract §7-S1); recorded on the run and read back as quoted/actual/drift by saphan cost
--dry-run-argstringArray[]proc: declared dry-run argv entry — declares the two-pass capability (repeatable)
--effortstringunsetmandate.effort — enforced where the backend exposes it; absence recorded
--effort-strictboolfalserefuse when the backend cannot enforce effort (R6; codex until probe P4)
--egress-policyboolfalsedeclare that this run is judged by the fleet's egress policy document (<state-dir>/egress/policy.json, the file the console's egress page edits). The run's outbound network is then routed through a per-run SOCKS5 door this engine mints, serves and unlinks, and the policy's compiled digest lands on the run's record. REFUSES BEFORE THE SPAWN, at $0, if the document is absent, unreadable or malformed, if the host has no measured egress rung, or on any lane that cannot carry a door (ssh, vendor-delegated confinement, a container spawn) — it never starts quietly with an open network. Omitted = UNCHANGED behavior: no policy, no cut, no door, and the same open network every spawn on this build gets
--envstringArray[]environment variable NAME to ADD to the backend's baseline env (repeatable, additive-only — never removes the baseline; values are never recorded, §1.6)
--ephemeralboolfalseno-persist vendor mode; ILLEGAL for plan profile (R2)
--fileboolfalsetreat --config's value as a path to a named class file rather than inline JSON
--force-dyadboolfalseoverride R8d's (stream, role) dispatch lock — an owner ACT, never a silent bypass: the refusal R8d would have returned is appended onto this run's own record (enforcement_note) before the spawn proceeds
--git-user-emailstringunsetgit author/committer EMAIL this leg commits as, injected as GIT_AUTHOR_EMAIL/GIT_COMMITTER_EMAIL (must be given with --git-user-name); omitted = filled from config-v1's run.git_user_email
--git-user-namestringunsetgit author/committer NAME this leg commits as, injected as GIT_AUTHOR_NAME/GIT_COMMITTER_NAME (must be given with --git-user-email); omitted = filled from config-v1's run.git_user_name, and an empty pair REFUSES at R50 on a far or seated spawn rather than letting the leg commit as its own host
--headroom-preflightboolfalseprobe the fleet for a free place BEFORE this spawn (R17) and feed the result into this run's own headroom gate — OPT-IN: costs ~$0.06 per applicable place plus each place's own wall-clock (law #8 property 3's 8-minute floor); omitted = today's behavior, Deps.Headroom stays nil for this run
--identitystringunsetcast-surface: executor identity seated on --machine whose CLAUDE_CONFIG_DIR binds this session; omitted = auto-resolved iff --machine carries EXACTLY one seated identity, else refused (never guessed)
--inner-budgetduration0sagent-loop only: explicit inner budget under --wall-clock (0 = derived from it, §1.6)
--lanestringunsetagent-loop only: config-ladder lane alias whose credential the child resolves on ITS OWN execution host (never composed into this spec, §1.6)
--lawsstringunsetABSOLUTE path to the standing-law index the head must receive — REQUIRED for LLM backends; its content is inlined into the prompt at every spawn and its SHA-256 is recorded (R12 refuses an unreadable file)
--machinestringunsetcast-surface: admitted machine id to run this spawn on — the TRANSPORT comes from that machine's own registry row (ssh dials it, local runs it right here, RS-2b + order local-seats), never from this flag; omitted = no machine, no seat, today's plain local behavior unchanged; "auto" (order machine-auto-placement, Q1) hands placement to the engine — dispatch.Resolve picks the seat (capability rung, then free-live, then price) and reserves it atomically before this run ever spawns, never with --identity/--slot
--max-iterationsint0agent-loop only: cap on model turns (0 = the loop's own default of 25)
--max-tokensint0agent-loop only: per-request --max-tokens (0 = the endpoint's own default; R18 refuses an explicit value under 256)
--modelstringunsetmandate.model — mandatory for LLM backends (order §4.1)
--no-channel-skeletonboolfalsedo NOT emit the role-owned channel skeleton into --workdir before spawn (default: emitted, rider M1) — for benchmark/non-fleet runs, where a fleet-dispatch artifact in the workdir pollutes the measurement; fleet dispatch (serverd) is unaffected, this flag exists only at this operator verb
--outside-write-dirstringArray[]request write access outside the workdir — refuses without a signed disposition (R7, adr-0020)
--profilestringunsetcapability profile: plan | execute (required, no default)
--provisionstringArray[]provision <source>=<workdir-relative-dest>, where <source> is a file or a DIRECTORY (repeatable; a directory is walked sorted and every member lands, the executable bit survives; a file is recorded as dest@sha:<12>, a tree as dest@tree:<manifest digest>:<members>)
--require-capabilitystringArray[]capability required by this work (repeatable; e.g. loopback-listener)
--require-egress-cutboolfalserefuse to start unless the machine this run lands on has a MEASURED egress lever (machine probe-confinement records the ladder: netns -> cgroup v2 -> separate uid on Linux, seatbelt (deny network*) -> separate uid on macOS). A host with no rung refuses BEFORE the spawn, class egress-unavailable — it never starts quietly with an open network. Omitted = the ADMISSION question is not asked, and it is not the same as the network being open: the cut itself follows the run's EGRESS POLICY, not this flag, so a run that declares one is walled in either way and a run that declares none gets the open network either way. What this flag adds is the refusal BEFORE the spawn on a host measured to have no rung
--resumestringunsetresume the RECORDED vendor session key (two-pass PASS 2; R3: never a fork, never --last)
--reviewedstringunsetthe BARE slug of the stream this spawn REVIEWS (no -review, no -rN suffix) — REQUIRED with --role review and refused with any other role (R52); it is substituted into the injected _REVIEW_STATUS.md H1 rule, so the leg derives nothing: both fleet patterns are representable (a leg on its own <reviewed>-review stream, and a leg spawned into the reviewed stream's own worktree, where this equals --stream)
--rolestringunsetfleet role this spawn carries: master | executor | review | design | scribe | advisor (spec.Validate's closed vocabulary, READ from it — never retyped here); omitted = no role, R8d's dispatch lock is a no-op for this spawn (there is nothing to key it on) — but R25's placement check is NOT a no-op: an UNRESERVED seat (registered executor, the default) still places it, a RESERVED seat (registered anything else) REFUSES it exactly as it refuses a mismatched role
--slotint0cast-surface: slot index on --machine to reserve (rule R8c); omitted = derived from the resolved --identity's own seat (machine seat)
--streamstringunsetstream slug the run belongs to (required)
--trackstringunsetthe TRAIN this stream belongs to — the order's track: header line, verbatim; omitted records NO DECLARATION (SQL NULL), which is a different fact from --track "", and neither is ever inferred from the stream slug
--version-argstringunsetproc only: flag that prints the version string (absent = version not captured)
--wall-clockduration0sMANDATORY wall-clock limit; on expiry the process group is SIGKILLed (e.g. 10m)
--workdirstringunsetabsolute workdir; the child cannot write outside it (adr-0020 §2 rule) (required)

Examples

Everything below this line is written by hand and is not regenerated.

The smallest useful run: an ordinary program under mandate

saphan run --stream nightly-suite --backend proc \
  --binary /usr/local/go/bin/go --arg test --arg ./... \
  --workdir /srv/checkouts/api --profile execute --wall-clock 20m \
  --workspace /srv/saphan

This runs a program, not an agent: no model, no brief, no laws. It exists on the record with its own transcript, its exit status and its cost, which is the point — the same door, the same accounting, whether the work was done by a model or by go test.

An agent session

saphan run --stream add-rate-limits --backend claude-code \
  --binary /usr/local/bin/claude --model <a model your configuration declares> \
  --brief /srv/worktrees/wt-add-rate-limits/_MASTER_ORDER.md \
  --workdir /srv/worktrees/wt-add-rate-limits \
  --laws /srv/saphan/laws/LAWS_INDEX.md \
  --profile execute --wall-clock 90m --cost-estimate-usd 8 \
  --env HOME --env PATH --env USER --workspace /srv/saphan

Four things about that line are worth reading twice.

  • --model is mandatory and is never a session default. The run records which source answered for it, so a model choice is always attributable.
  • --laws is required for an agent backend and is read fresh at every spawn, including a resume; its digest lands on the record. An unreadable or empty file refuses before anything is spawned — an empty floor is not a floor.
  • --brief must live inside the workdir. The session receives only its path and reads it from inside its own confinement; a brief outside is refused by name.
  • --cost-estimate-usd is your quote, and the spend cap is derived from it rather than chosen separately.

A refusal here is a recorded outcome, not a fallback. If a precondition is missing the run refuses by name and nothing spawns — read the name rather than retrying blind.

On this page