---
name: cc-acceptance-surface-provenance
description: "Claude Code's only acceptance surface is UserPromptSubmit — it proves who pressed Enter, never who typed the text; measured UPS coverage + the transcript instrument that measures it."
metadata: 
  node_type: memory
  type: project
  originSessionId: 19d48c98-d787-4436-90c2-da3f52339ae1
  modified: 2026-09-12T20:27:05.397Z
---

2026-09-12 — assessed spt-core's INPUT-PROVENANCE-CONTRACT (#300, doyle via todlando) against claude-spt.

**Verdict sent:** claude-spt can do capture-at-commit, one-call report, never-substitute, report-once.
It CANNOT establish the ORIGINATING controller.

⭐⭐ **`UserPromptSubmit` fires at COMMIT — the wrong END of the window.** Between keystrokes and the
hook sits CC's input box: a client-side, editable, **unbounded-lifetime** buffer. A handoff can fall
anywhere inside compose time. Worse, `spt rc` drives the SAME PTY, so two controllers can CO-AUTHOR
one submission and the harness records one string with no per-character origin. No adapter fix exists.

⭐ Capture and report both happen INSIDE the one hook invocation, so the contract's hard part
(carrying an association through a queue) does not arise at all. The hard part is upstream of the hook.

⭐⭐ **ORDERING: MEASURED ENQUEUE, n=3 (2026-09-12).** I had first refused to assert this. Then built
the probe: force own endpoint `state idle` MID-TOOL-CALL, self-send so core PTY-types a stub + Enter
into the input box while the turn is demonstrably still running, poll `hook-trace.log` for
`BEGIN UserPromptSubmit`. Fired at **+0.5s / +0.9s / +1.0s after Enter, turn still executing**, and
**exactly ONE fire per submission** — no second fire when the harness folded it in. So capture-before-
the-queue IS satisfiable here. Probe: `scratchpad/enqueue_probe.py`.
⭐⭐ **The lesson: I could generate the population MYSELF.** I twice wrote "needs an operator typing
into a busy session" — wrong. spt's own PTY delivery IS keystrokes into that same box; forcing the
idle route made an operator-gated measurement self-serviceable. Ask what already drives the surface
before declaring a measurement blocked on a human.
⭐ Still UNMEASURED and NOT asserted either way: `hook.rs` (~:3149) claims a stub typed into the
**Stop→next-turn** boundary is folded with NO UPS at all. My probe landed mid-tool-call, a different
window. Don't let the n=3 result be read as refuting it. See [[v0200-stub-delivery-shipped]].

⭐⭐ **L6, sent as an addendum and arguably worse than L1: NOT EVERY ACCEPTANCE IS A USER.** spt's own
idle delivery TYPES INTO CC's INPUT BOX, so peer msgs / notifies / wakes arrive through
UserPromptSubmit wearing a prompt's clothes (ADR-0007 stubs) — indistinguishable at the harness level
from an operator pressing Enter. A naive capture would stamp CORE'S OWN INJECTED TEXT with the seat
holder: a provenance forgery generated by the TRANSPORT. claude-spt's existing user-input gate
(`now_signal_arg` / the USER_INPUT discriminator, `hook.rs` ~:4091) already separates them — but it is
ADAPTER-SIDE and PATTERN-BASED on today's shapes, so an unannounced new injection shape starts lying
silently. Flagged as a coupling the contract should own, not my pattern list.

**The instrument (reusable):** walk a session `.jsonl`; a real submission is a `type:"user"` entry with
no `tool_result`; it is UPS-covered if an `attachment` with `hookName` starting `UserPromptSubmit`
appears before the next user entry. Scripts: `scratchpad/pair_audit.py`, `stub_audit.py`.

**Measured on this node (20 transcripts):** 302 submissions, 227 UPS-covered. All 75 uncovered are
harness-synthesised entries OR **built-in local slash commands** (`/compact`, `/reload-plugins`
mid-session are the clean cases) — those fire NO UserPromptSubmit. Separately: 133 PTY-typed stub
submissions, 103 covered; all 30 uncovered are **safe-mode sessions (hooks off by design)**, not a
defect. So "no token" must be a FIRST-CLASS outcome, not an edge case.

⭐⭐ **Instrument limit I stated in the report rather than hid:** this observes hook OUTPUT, not hook
INVOCATION — a UPS that fired and emitted nothing is invisible. Positive control given alongside the
verdict (227/302 and 103/133 DID pair), per [[v02532-verdict-not-catchall]].

**Offered to doyle as feasibility input, not built:** the adapter's per-acceptance reports FENCE the
timeline into compose intervals; core (which owns the rc attachment) can refuse by name any submission
whose interval SPANS a seat change. An interval history check, not a sample of "who is attached now" —
which the contract forbids outright.

## RULED 2026-09-13 (doyle) — standing, not a workaround to revisit

The missing **submission-bound carrier** and the missing **origin discriminator** are **NOT adapter
implementation defects**, and *a new core token-read verb alone would supply neither*. doyle owns the
contract/transport decision.

**Standing behaviour for claude-spt: REPORT WITHOUT PROVENANCE where the carrier is absent.** Never
substitute a current-controller lookup (a fetch at hook time is still a sample of who is attached
NOW, not a value that travelled with the text — same hook invocation or not), and never substitute
payload-shape heuristics. No workaround is wanted; do not build one.

⭐ The citable asymmetry that made the carrier gap concrete: `PreToolUse` carries a harness-minted
`tool_use_id`; `UserPromptSubmit` carries **no** per-event identifier. The harness can mint per-event
identity and does not do so for submissions.
