ADR-0017: Ephemeral scratchpad harnesses (harness run)
Context and Problem Statement
Harness has two ways to have a supervised process: the global config
(📝 ADR-0006, durable, dotfiles-tracked) and projects (📝 ADR-0009, durable
Compose-style, repo-local). Neither answers the most casual gesture of all:
"run me a claude opus-5 for a bit, I'll throw it away when I'm done."
Today that means either editing harness.toml (absurd for a throwaway) or
reaching for screen/tmux/shpool — which is exactly the class of tool
Harness replaces. How do we give Harness a zero-config, ad-hoc session that
needs no file, no project, and no ceremony — and that cleans up after itself?
Decision Drivers
- Zero ceremony. Creating a scratchpad must not touch any file: no
harness.tomledit, no project file, no--namerequired.harness run claude opus-5is the whole interface. - Genuinely ephemeral. Unlike projects, which persist until
downorrm(📝 ADR-0009), a scratchpad is throwaway by construction: it is never persisted tostate.jsonand vanishes on daemon restart. The two concepts must not blur. - Names are the daemon's problem. The user should never have to invent a
unique name; the daemon mints one (
claude-opus-5-x4yx-shaped: a slug of the invocation plus a short random suffix) and the client prints it. - Full supervisor, not a stripped-down one. A scratchpad gets PTY, attach,
scrollback, logs — everything a configured harness has.
harness attachon a scratchpad is the tmux-attach replacement. - Explicit teardown only. A scratchpad runs until
harness rm NAME(or daemon exit). A normal exit of the process leaves it registered, exit code inspectable, until rm: sessions do not silently disappear. - Daemon stays the supervisor (📝 ADR-0002/0005): the client verb pushes a definition over the control plane; there is no second execution engine.
Considered Options
- Option 1 — Synthetic "scratch" project. Implement
runas aproject_upagainst a reserved project name (scratch). Maximum reuse; zero new daemon machinery. - Option 2 — A dedicated
scratchprovenance class +scratch_runop. A third registration kind alongside global (config) and project (project:NAME): registered by a new control op, provenance-taggedscratch, excluded from persistence, torn down by the existingremoveop. - Option 3 — Client-side temp config. The client writes a throwaway harness.toml to a temp dir and project_ups it as a project rooted there.
Decision Outcome
Chosen option: Option 2 — a dedicated scratch provenance class, because
it keeps the three lifetimes honestly distinct — global (config-owned, durable),
project (repo-owned, durable-until-down/rm), scratch (operator-owned, ephemeral)
— while reusing the supervisor, the remove op (📝 ADR-0009), and the attach and
log planes wholesale. Option 1 was rejected because a synthetic
project would inherit project persistence semantics (or need special-casing to
avoid them) and would show up in harness down/ps project scoping; Option 3
because it smuggles a temp-file lifecycle into a gesture that must touch no
files.
The gesture and the name
harness run [flags] ARG... maps its positionals onto the harness argv the
same way a [harness.NAME] table's harness + args keys would: the first
positional selects the harness kind (crush, claude-code, codex,
generic, with claude aliased to claude-code — the word people type —
and a bare unknown word treated as a generic command to run,
so harness run claude opus-5 and harness run htop both work), the rest are
its args. The daemon mints the name: <slug>-<suffix>, where the slug is the
sanitized invocation (e.g. claude-opus-5) and the suffix is 4 random base36
characters, retried on collision with any registered name. The reply carries
the name; the client prints it, then attaches to it. harness run is
tmux new-session, not tmux new-session -d: the default completes the
gesture rather than leaving a second harness attach step. --detach opts
out (the -d equivalent), as do --json and piped or redirected stdio at
either end, since none of those has a terminal to attach to. --name
overrides the slug; uniqueness is still enforced by the suffix.
Lifecycle
scratch_run registers the supervisor with provenance scratch and starts it.
Scratchpads are never written to state.json — on daemon restart they are
gone, exactly like a tmux server dying takes its sessions. Because the
provenance value scratch doubles as the sentinel Save excludes, the
project name scratch is reserved (refused at project_up), so the
sentinel can never collide with a real project. Teardown is the
existing remove op (harness rm), which accepts any provenance-tagged
(registered) harness. A scratchpad whose process exits stays registered until
rm, with its exit state; the default restart policy for a scratchpad is no,
matching session semantics rather than service semantics.
Consequences
- Good, because
screen/tmux/shpool's exact use case — "a terminal thing for a while, then gone" — becomesharness run(which drops the operator straight in) plusharness rm, inside the one supervisor that already owns PTYs and scrollback.--detachis there for the rarer "start it and walk away" case. - Good, because the durable/ephemeral split is structural: persistence is
keyed on provenance, and
scratchprovenance is excluded from Save by construction — no flag-day, no per-callsite discipline. - Good, because
rm, attach, logs, and the TUI list needed no new code paths — scratchpads are supervisors with a provenance tag. - Bad, because a daemon restart silently discards every scratchpad (accepted: that is the semantic — and the same failure mode as the tmux server).
- Neutral, because
run's first-positional dispatch (kind vs. generic command) is a heuristic; an explicit--kindflag disambiguates the rare collision (a command literally namedcrushthat was not meant as a kind).
Confirmation
- SPEC-0011 formalizes
scratch_run, name minting (slug + suffix, collision retry), the no-persistence invariant, restart policy, and the rm teardown as testable requirements. - Acceptance tests:
runregisters and starts a uniquely-named supervisor withscratchprovenance thatlistshows; state.json after any scratchpad lifecycle contains no scratch entry; a daemon restart leaves zero scratchpads;harness rmremoves one; two identicalruninvocations never collide.
Pros and Cons of the Options
Option 1 — Synthetic "scratch" project
- Good, because project_up/project_down/remove are reused verbatim.
- Bad, because projects persist (📝 ADR-0009), so a scratch project would either persist (wrong) or need special-casing in Save and Restore that amounts to implementing Option 2 anyway.
- Bad, because
harness down/psproject scoping and the<project>/namespace would surface an operator-internal bookkeeping project to the user.
Option 2 — Dedicated scratch provenance class
- Good, because each lifetime (config/project/scratch) is a first-class
provenance with its own persistence rule, and
rmneeds no changes. - Good, because the name is minted where uniqueness is checkable — under the registry lock.
- Neutral, because it adds a third branch wherever provenance is inspected (save, restore, list projection).
Option 3 — Client-side temp config
- Good, because the daemon changes not at all.
- Bad, because the gesture then writes files (temp dirs leak, crash residue), inherits project persistence, and the "ephemeral" property becomes a lie the first time someone inspects state.json.
More Information
- Extends 📝 ADR-0009 — projects are the durable sibling; because project registrations persist, scratchpads must not be projects.
- Extends 📝 ADR-0011 — the harness-kind enum and argv synthesis
runreuses. - Related 📝 ADR-0002 —
runis a thin-client gesture over the control plane. - Related 📝 ADR-0005 — scratchpads are supervised like any harness, with a
norestart default. - Related 📝 ADR-0007 — the no-persistence rule is keyed on provenance in
state.json. - Governs SPEC-0011 — requirements and scenarios.
Related Artifacts
Direct relationships declared in YAML frontmatter (per the SDD plugin's artifact-graph convention). Run /sdd:graph chain ADR-0017 for the transitive view.