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:
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=localderives 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=sshderives from the machine's own registry-declared worktrees root (machine admit --worktrees-root, ormachine set-worktrees-rootlater) — 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:
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.
- 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).
- Rehearse it first (see above) and read the four reads it prints.
- Run, from any directory:
--yesis required when stdout is not a TTY — a seat's own file is never edited silently. At a real TTY, omitting--yesprompts for confirmation. - Read the printed message. A fresh stamp reads
stamped <path>: …; an already-stamped root readsalready stamped: … — nothing written(idempotent, safe to re-run). - 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:
- Measure the far host's layout (never guess it):
- Declare that machine's worktrees root — an ABSOLUTE path on THAT machine, an ancestor of
every worktree you intend to stamp there:
- 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. - 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).