Course · Term 18 of 30
Headless Runs
Every lesson in this course builds one system: a stablecoin reserve attestation registry that discovers issuers, reads the PDFs they publish, and refuses to publish a field it could not read. What it is and where this lesson sits.
Removing the human does not simply make a run unattended. It changes what the run is allowed to do, because every place the code would have asked is now a place it must decide.
The failure it fixes
The interactive loop has a person in it, and that person is load-bearing in ways nobody writes down. They approve tool calls. They notice a question. They stop a run that has clearly gone wrong. Take them out and the code does not become autonomous; it becomes a program whose specification has silent holes in it.
The dangerous hole is permissions. An interactive config marks a risky tool
ask, which is a perfectly good answer when someone is there. Run that same
config headless and ask has to mean something. Every implementation that
treats it as allow has just granted the run the exact tool surface its author
flagged as needing approval, and the config file still reads as cautious.
The second hole is ambiguity. An attestation whose composition table does not parse is, interactively, a question. Headless, if it is not explicitly a refusal, it becomes a guess. The run completes either way.
Break it first
Take a config that is fine interactively and start a headless run with it:
$ npx tsx scripts/terms/demo-18.ts
--- configs that must refuse to start
ok an 'ask' permission: permissions contain "ask" for Write, but a
headless run has nobody to ask.
ok no budget: a headless run needs a positive budget declared before it starts
assertHeadlessSafe throws before anything executes. An ask in a headless
config is not a nuance to interpret at the call site; it means the config was
written for a different execution mode and has not been reviewed for this one.
The mechanism
src/headless.ts runs a step list against a declared permission map, putting
term 15's hook in front of every step that passes the permission check. Four
behaviours are the opposite of their interactive defaults.
An undeclared tool is refused, not defaulted. The set of tools a run may use has to be something a person decided, not a consequence of what the code happened to call.
A question becomes a refusal with a recorded reason. There is no one to ask, so the only honest alternatives are refuse or guess.
Unresolved work forces a non-zero exit. The run completed. It did not succeed, and nobody was watching to tell the difference.
The artifact is the entire output. Whatever is not in it did not happen, including the reason for every refusal.
Build it
ran PublishRegistryRecord permitted and passed every hook rule
REFUSED PublishRegistryRecord field(s) published while listed unreadable
REFUSED Bash tool "Bash" is denied by config
REFUSED SendEmail tool "SendEmail" is not declared in permissions
human interventions : 0
refusals : 4
unresolved : 3
exit code : 1
.claude/settings.json carries the permission map, every entry allow or
deny, with a comment saying why none of them is ask.
The receipt
artifacts/18-headless.json. Twelve tests and three mutations, each failing
exactly the tests that should catch it:
undeclared tool defaults to allow -> 2 failed | 10 passed
"ask" accepted in a headless config -> 2 failed | 10 passed
unresolved no longer forces non-zero exit -> 2 failed | 10 passed
Where it overlaps
Line D is execution surface at its sharpest: this is the term where work stops happening in a place a person is looking at. Line E is the trust boundary, and this is the term that earned its mark in the Stage 1 recount, because headless removes the approver and the reachable tool surface widens as a direct consequence. Line F is determinism: with no human to vary the run, the config and the hook are the only things deciding, and both are deterministic. Line G is feedback, because the artifact is now the only channel the run has.
This term closes the Stage 3A slice. Term 19, next in build order, fans these runs out in parallel, and the pairing of term 15's hook with that fan-out is interstitial II.
Sharp edges
askis invalid, not coerced. Any implementation that maps it to allow or deny at the call site has made a policy decision in the wrong file.- Zero refusals is a suspicious result, not a clean one. A run over real issuer PDFs that refused nothing has probably not checked anything; the demo's own run refuses four of eight on purpose.
humanInterventions: 0is asserted, not assumed. It is in the artifact so that a run which secretly prompted somewhere fails the gate rather than passing quietly.- Determinism is tested. The same input twice must produce byte-identical artifacts, or the run cannot be bisected when it starts producing bad records.
- The permission map is not a security boundary. It constrains a cooperating runner. A tool that writes by another route is outside it, which is the same limit term 15 has and an argument for a narrow tool surface.
A personal teaching project, in development. The registry it builds is a teaching artifact and is not an assurance opinion about any issuer. Not affiliated with or endorsed by any employer, or by any vendor whose tools it describes. Where a unit depends on a specific flag or path, it names the version it was verified against. All units.