Saphan StudioDocs
Configuration

Satisfying a vendor trust dialog headlessly

What machine trust-stamp does, what it derives permission from, and both runbooks.

What it does. The vendor claude-code CLI asks, once per resolved project directory, "do you trust the files in this folder?" — an interactive prompt with no TTY to answer it on a headless dispatch. machine trust-stamp sets the ONE key the vendor actually reads — projects["<resolved root>"].hasTrustDialogAccepted = true inside a seat's own <config-dir>/.claude.json — so that prompt never appears for a spawn against that root.

The resolved root, not the directory you name. The vendor keys trust on the git main repo root when the directory lies inside a repo (a git worktree included — the key is the main checkout, never the worktree's own path), and on the directory itself otherwise. Trust does not inherit from any ancestor directory. Naming a worktree with --root is fine and expected — the verb resolves it for you and tells you what it actually stamped:

note: --root /path/to/worktrees/wt-my-stream resolves to /path/to/main-repo — the vendor
keys trust on the resolved project root, never the literal path passed in; stamping
/path/to/main-repo

Permission is derived, never invented — one rule, two anchors. A stamp is always derived from an owner act; which act depends on the seat's transport:

  • transport=local derives from the owner's own trust map (~/.claude.json, this process's own — never a seat's): a root is admissible only if it lies inside (or equals) a directory that map already marks trusted. Outside every owner-trusted directory, it refuses by name (trust-stamp-not-derivable) with the remediation printed: run the vendor tool interactively there once and accept its own prompt, then re-run the stamp.
  • transport=ssh derives from the machine's own registry-declared worktrees root (machine admit --worktrees-root, or machine set-worktrees-root later) — never this operator's local trust map: a local absolute path and a far host's absolute path can never share a prefix, so comparing them would refuse every real remote stamp. Admitting the machine and declaring where its worktrees live is the owner act that stands in for a prompt the owner cannot physically sit at.

Rehearse it before you hand it over. machine trust-stamp is an owner act — a head composes the line, the owner pastes it — and law #33 forbids handing over a command nobody has executed in a harmless form first. The rehearsal is the saphan dry-run prefix:

saphan dry-run machine trust-stamp --workspace <ws> --machine <machine-id> \
  --identity <seat-identity> --root <directory> --actor <your name> --yes

It performs every read the real act performs and no write at all: whether the root exists and what it resolves to, whether the seat's own .claude.json already carries the key (→ STAMP UNNECESSARY, so nobody hands over a blind overwrite), whether machine and identity are in the registry and whether the seat's backend has a trust-dialog contract at all, and exactly what would be written. It also prints, as its last line, the literal command for the owner's hand with the root already resolved — copy that line rather than composing one, because a rehearsal proves you ran a command, not that you ran the right one. A refusal costs nothing here and is the same refusal, by the same class name, the real act would have given in the owner's hand.

Two things it deliberately does not do. It never prompts (it reports what the real act's consent gate would decide, from --yes and whether stdout is a TTY). And on a transport=ssh seat it makes no ssh dial, so two of the four reads — does the root exist on the far host, and does that seat's file already carry the key — print UNKNOWN with the reason. On that lane the rehearsal proves the line parses and the registry side holds; it does not prove the far host will accept it.

Runbook — the local form.

  1. Confirm the owner has, at some point, accepted the vendor's own trust prompt for an ancestor of the directory you want to stamp (interactively, once, on this host).
  2. Rehearse it first (see above) and read the four reads it prints.
  3. Run, from any directory:
    saphan machine trust-stamp --machine <machine-id> --identity <seat-identity> \
      --root <directory, or omit for the current one> --actor <your name> --yes
    --yes is required when stdout is not a TTY — a seat's own file is never edited silently. At a real TTY, omitting --yes prompts for confirmation.
  4. Read the printed message. A fresh stamp reads stamped <path>: …; an already-stamped root reads already stamped: … — nothing written (idempotent, safe to re-run).
  5. Where you meet this in practice: a dispatch onto an unstamped local seat refuses BEFORE it starts (rule R24) rather than burning a lane on the vendor's interactive prompt — and the refusal reason is the exact command from step 2. Copy it, run it, retry the dispatch.

Runbook — the remote form. --root is required for an ssh seat (this process's cwd names nothing on the far host) and must name a directory as it exists on that machine. The far host's own git resolves worktree-to-main-root, the far seat's own .claude.json is patched over the same ssh dial every other remote verb uses, and the printed message names the host. Before the first remote stamp for a machine:

  1. Measure the far host's layout (never guess it):
    ssh -F <ssh-config> <machine-id> 'echo "$HOME"'
  2. Declare that machine's worktrees root — an ABSOLUTE path on THAT machine, an ancestor of every worktree you intend to stamp there:
    saphan machine set-worktrees-root --workspace <ws> --machine <machine-id> \
      --worktrees-root <absolute parent dir on that machine>
  3. Rehearse it (saphan dry-run machine trust-stamp …). On this lane the rehearsal firmly reproduces the two registry-side refusals — an undeclared worktrees root (trust-stamp-worktrees-root-not-declared) and a backend with no profile-home convention — and prints the anchor comparison as an advisory, not a verdict: the real act judges the far host's git-resolved root, and for a worktree that is the main checkout, which may not sit beneath the declared root at all.
  4. Run the stamp exactly as the local form — the CLI surface does not change, only what it derives permission from.

A machine with no declared worktrees root refuses the stamp by name (trust-stamp-worktrees-root-not-declared), never a silent fall-back to this operator's own local trust map. A declared root that does not cover --root refuses by name too (trust-stamp-not-derivable) — widen the declared root, or point --root beneath it.

Revoking. --revoke undoes a stamp this engine previously placed — by default only when the root's derivation is no longer owner-trusted (a health check; the common case is a no-op). --force revokes unconditionally, as the owner's own explicit act.

Two honest boundaries. The pre-spawn refusal (R24) does not yet cover transport=ssh seats — an unstamped remote seat's spawn is not refused in advance; the vendor's own prompt is what a headless run against it will hit. And codex seats are out of scope by name (codex's own trust story is unmeasured).