Context
PR #17463 (Resolves #17442) registers a SessionStart hook in .codex/hooks.json so a Codex seat re-reads the core mailbox preflight on startup, resume and compact — the three moments a seat can lose the fact that its institutional tools never attached. Every other acceptance item on #17442 is unit-proven at L2. This one is not, and cannot be from a Claude seat: it is Codex harness wiring, and only a Codex seat sees whether the hook fires.
@neo-gpt-emmy's review of PR #17463 established the correct disposition: a named agent is not a residual owner — a person-shaped assignment carries no lifecycle — and a post-merge L3 arm belongs on an existing open ticket distinct from the close target, because Resolves #17442 closes that one on merge. This ticket is that owner. The author's first attempt to keep #17442 open with Refs instead was rejected by agent-pr-body-lint, which enforces the operator rule (#12367) that every agent PR body carries Resolves #N.
The Problem
The verification is not "does Codex honour its own documented SessionStart event" — that is the vendor's contract, not ours. The falsifiable claim is the matcher expression, and it is the only one of the three registrations that has one:
"SessionStart": [{ "matcher": "startup|resume|compact", "hooks": [...] }],
"UserPromptSubmit": [{ "hooks": [...] }],
"Stop": [{ "hooks": [...] }]Three outcomes, and two of them are silent:
- the matcher is honoured — the hook fires on all three sources and the arm is met;
- the matcher key is not part of Codex's
SessionStart schema and is ignored — the hook still fires, on every source, which is what we want anyway;
- the matcher is honoured but the expression is wrong — no source ever matches, the hook never fires, and the seat sees nothing.
Outcome 3 is indistinguishable from "no seat has resumed yet." A hook that silently never fires looks exactly like a hook that has not been reached, which is the same class of defect #17442 exists to fix: absence of a signal read as absence of the condition.
The Architectural Reality
.codex/hooks.json — three registrations; only SessionStart carries a matcher.
.codex/hooks/codex-context.mjs (158 LOC) — one module, two paths, selected by an explicit --session-start flag rather than by payload shape, so the branch is assertable from a non-Codex seat.
test/playwright/unit/hooks/codexContextHook.spec.mjs — proves the module emits correctly when invoked; it cannot prove invocation.
- Sibling:
.codex/hooks/codex-lane-state-stop.mjs (434 LOC) on the un-matched Stop registration, which is the working precedent for a matcher-free entry.
The Fix
A Codex seat, on the first startup / resume / compact after PR #17463 merges, reports what it saw. If the line is present, this ticket closes on that comment. If it is absent, the fix is one of:
- drop the
matcher key so SessionStart matches its two siblings' shape, or
- correct the expression to whatever Codex's schema accepts for that event.
Either is a one-line change to .codex/hooks.json, which is what makes this a leaf rather than an epic.
The exact string to look for:
Core preflight: call list_messages({status:'unread'}) now. Missing or erroring is degradation, not an empty inbox — run /self-repair before resuming the lane.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
.codex/hooks.json → SessionStart[0].matcher |
Codex hook event schema |
Fires the core preflight on startup, resume and compact |
Drop the key, matching the Stop / UserPromptSubmit entries that already work without one |
.codex/hooks/codex-context.mjs main() docblock |
live Codex seat report on this ticket |
Decision Record impact
none.
Acceptance Criteria
Out of Scope
- The content of the preflight sentence, the classifier, and the
AGENTS.md trigger — all delivered and unit-proven under #17442.
- Claude-side
SessionStart equivalents. .claude/hooks/ is a separate registration surface with its own schema.
- Any general "verify every hook registration fires" sweep. One registration introduced one unverified key; a sweep is a different ticket with a different premise.
Avoided Traps
- Do not treat this as verifying Codex's documented behaviour. That would make it unfalsifiable busywork. The claim under test is our
matcher expression, which is why the fallback in the ledger is removal rather than a fix.
- Do not assign a person. It is scoped to whichever Codex seat resumes first; the ticket is the record, and a named assignee would recreate exactly the residual-ownership defect that produced this ticket.
- Do not widen to a hook-verification framework. A mechanical prover for harness-side registration would need the harness, which is the thing we cannot reach from here — the reason this arm is L3 in the first place.
Related
- #17442 — the close target of PR #17463, which this ticket takes the live arm from.
- PR #17463 — the implementation;
@neo-gpt-emmy's review is the authority for this ticket's shape.
- #12402 — the same pattern, one harness over: a post-merge L3 arm preserved as its own ticket so it survives the parent's auto-close. Its scope is Claude sibling wake routing, which is why the arm here needed a ticket of its own rather than a row on that one.
Live latest-open sweep: checked latest 20 open issues at 2026-08-21T14:34:01Z, plus a resources/content/issues/ keyword sweep for SessionStart / hook-fire / hook-registration (5 hits, all CLOSED and all Kimi/OpenCode wake-route tickets), plus a 30-message A2A in-flight claim scan; no equivalent found.
Origin Session ID: ab15d2b8-eb14-4237-ad18-ce48584b2d07
Retrieval Hint: query_raw_memories("codex SessionStart matcher hook fires live seat post-merge L3 residual owner")
Context
PR #17463 (
Resolves#17442) registers aSessionStarthook in.codex/hooks.jsonso a Codex seat re-reads the core mailbox preflight on startup, resume and compact — the three moments a seat can lose the fact that its institutional tools never attached. Every other acceptance item on #17442 is unit-proven at L2. This one is not, and cannot be from a Claude seat: it is Codex harness wiring, and only a Codex seat sees whether the hook fires.@neo-gpt-emmy's review of PR #17463 established the correct disposition: a named agent is not a residual owner — a person-shaped assignment carries no lifecycle — and a post-merge L3 arm belongs on an existing open ticket distinct from the close target, becauseResolves #17442closes that one on merge. This ticket is that owner. The author's first attempt to keep #17442 open withRefsinstead was rejected byagent-pr-body-lint, which enforces the operator rule (#12367) that every agent PR body carriesResolves #N.The Problem
The verification is not "does Codex honour its own documented
SessionStartevent" — that is the vendor's contract, not ours. The falsifiable claim is thematcherexpression, and it is the only one of the three registrations that has one:"SessionStart": [{ "matcher": "startup|resume|compact", "hooks": [...] }], "UserPromptSubmit": [{ "hooks": [...] }], "Stop": [{ "hooks": [...] }]Three outcomes, and two of them are silent:
SessionStartschema and is ignored — the hook still fires, on every source, which is what we want anyway;Outcome 3 is indistinguishable from "no seat has resumed yet." A hook that silently never fires looks exactly like a hook that has not been reached, which is the same class of defect #17442 exists to fix: absence of a signal read as absence of the condition.
The Architectural Reality
.codex/hooks.json— three registrations; onlySessionStartcarries amatcher..codex/hooks/codex-context.mjs(158 LOC) — one module, two paths, selected by an explicit--session-startflag rather than by payload shape, so the branch is assertable from a non-Codex seat.test/playwright/unit/hooks/codexContextHook.spec.mjs— proves the module emits correctly when invoked; it cannot prove invocation..codex/hooks/codex-lane-state-stop.mjs(434 LOC) on the un-matchedStopregistration, which is the working precedent for a matcher-free entry.The Fix
A Codex seat, on the first
startup/resume/compactafter PR #17463 merges, reports what it saw. If the line is present, this ticket closes on that comment. If it is absent, the fix is one of:matcherkey soSessionStartmatches its two siblings' shape, orEither is a one-line change to
.codex/hooks.json, which is what makes this a leaf rather than an epic.The exact string to look for:
Contract Ledger Matrix
.codex/hooks.json→SessionStart[0].matcherstartup,resumeandcompactStop/UserPromptSubmitentries that already work without one.codex/hooks/codex-context.mjsmain()docblockDecision Record impact
none.Acceptance Criteria
startup/resume/compactafter PR #17463 merged — naming which of the three sources it observed. Post-merge only.matcherkey is dropped or corrected in.codex/hooks.json, and the same seat re-confirms.main()docblock in.codex/hooks/codex-context.mjs, so the next seat reads it as a settled fact and not as an open question.Out of Scope
AGENTS.mdtrigger — all delivered and unit-proven under #17442.SessionStartequivalents..claude/hooks/is a separate registration surface with its own schema.Avoided Traps
matcherexpression, which is why the fallback in the ledger is removal rather than a fix.Related
@neo-gpt-emmy's review is the authority for this ticket's shape.Live latest-open sweep: checked latest 20 open issues at 2026-08-21T14:34:01Z, plus a
resources/content/issues/keyword sweep forSessionStart/ hook-fire / hook-registration (5 hits, all CLOSED and all Kimi/OpenCode wake-route tickets), plus a 30-message A2A in-flight claim scan; no equivalent found.Origin Session ID: ab15d2b8-eb14-4237-ad18-ce48584b2d07
Retrieval Hint:
query_raw_memories("codex SessionStart matcher hook fires live seat post-merge L3 residual owner")