# JIT plan — one custody-proved identity per hook turn

> Scope: close the routes of `REQ-HAZARD-INHERITED-IDENTITY-ADOPTION` (KNOWN-HAZARDS §7.4) that
> survived the v0.25.x env scrub. Adapter-side only; no spt-core dependency, no release-blocking
> coupling to doyle's `REQ-WHOAMI-EXPLICIT-SID-REFUSAL`.

## What is still open

The v0.25.x fix scrubbed `$SPT_ENDPOINT_ID` / `$SPT_AGENT_ID` from the `whoami` child, on the
probe shape where the descendant carries an `OWL_SESSION_ID` matching **no** perch. The real
descendant carries the ancestor's **real** one, and two routes run straight through:

- **Route (d) — the session leg is itself an inheritance leg.** `self_id` asks whoami with
  `$OWL_SESSION_ID` *if present*, and only falls back to the payload's `session_id` when it is
  absent (`hook.rs:840`). A process launched from a perched agent's tool call inherits that
  variable, so the descendant never asks about its own session at all. **Observed 2026-07-26**
  (perri, live node): the hook binary handed a payload `session_id` belonging to a different live
  perch answered as `perri` — resolved, marked busy, and ran `perri`'s poll and msg-park drain.
  With `$SPT_ENDPOINT_ID` and `$SPT_AGENT_ID` scrubbed and the inherited real `OWL_SESSION_ID`
  left in place, `spt whoami --json` returns `{"id":"perri",…}`; with `OWL_SESSION_ID` pointed at
  a sid matching no perch, `{"id": null}`.
- **Route (e) — the raw ambient endpoint id, unproved, at ~10 side-effecting sites.**
  `verified_env_id` is the only reader that proves custody (carrier sid == payload sid). Everywhere
  else `$SPT_ENDPOINT_ID` is read directly and then WRITTEN against: the msg-park drain and commit,
  the wake-park read/clear, the digest cursor, the role-edit round trip, and the Stop leg. A
  descendant does not merely read the ancestor's state — it consumes the ancestor's parked message
  bodies and clears its wake directive.

Why this matters more than a read error: both routes are write-capable, and the loss is silent on
the victim's side. The ancestor's park is drained into a session that will never render it.

## The shape of the fix

1. **Identity resolves from the session the payload names.** `self_id` passes the payload
   `session_id` to whoami; the ambient `$OWL_SESSION_ID` is never an identity input. It stays SET on
   the child call (that is the leg being answered) but takes the resolved value, so an inherited one
   cannot answer. An empty payload sid resolves EMPTY — callers already no-op on that.
   - Anti-regression: for any session the launcher spawned, the ambient value IS the payload sid
     (`$CLAUDE_ENV_FILE` is per session and SessionStart re-exports), so the legitimate path is
     unchanged. The existing test asserting "env sid wins over payload" encodes the defect and flips.
2. **One custody-proved id per turn, threaded — not re-read.** Each handler resolves once
   (`resolved_id`) and every side effect takes THAT id. The raw `$SPT_ENDPOINT_ID` reads become a
   single proved resolution; where a handler legitimately needs "am I spt-hosted?" (skill arm
   selection, `register_verb`) the ambient *presence* is still fine — it selects wording, writes
   nothing.
3. **Loud, once.** When the ambient endpoint id or session id is refused because it fails custody,
   log one line naming both sids. The v0.25.x scrub is invisible in argv and was only found by
   probing; this one should announce itself.

## Tasks

- [ ] Extend `REQ-HAZARD-INHERITED-IDENTITY-ADOPTION` in `traceable-reqs.toml` with routes (d)+(e)
      (registry first, per the working rules), and update KNOWN-HAZARDS §7.4 with the new probe.
- [ ] `self_id` takes the payload sid only; update the two tests that assert the old precedence.
- [ ] Thread the proved id through `handle_user_prompt_submit`, the Stop leg, and the SessionStart
      bringup paths; leave presence-only reads alone.
- [ ] Unit: a descendant fixture (ambient id + ambient sid + a foreign payload sid) drains no park,
      clears no wake directive, writes no state, and resolves EMPTY.
- [ ] Int: extend `ci/hooks/poll-int.sh` — run the hook with the ambient env of ANOTHER live perch
      and assert it touches only the throwaway one. (The probe itself hit this defect; the scrub in
      `hookrun` is the workaround, and this assertion is what makes the fix binding.)
- [ ] Gate: `sh ci/run-gates.sh` PASS + `traceable-reqs check` green, red-proven per route first.

## Open question for the operator

Ship as its own release, or ride the next one? The exposure needs a descendant Claude Code session
launched from a perched agent's own tool call — common in agent workflows (`cc`, `claude -p`,
nested sessions), rare in operator workflows.
