LearnNewsExamplesServices
Frontmatter
id17476
titleThe Codex SessionStart registration uses a `matcher` key its two siblings do not
stateClosed
labels
enhancementaitestingmodel-experienceagent-os
assignees[]
createdAtAug 21, 2026, 4:35 PM
updatedAtAug 23, 2026, 9:15 AM
githubUrlhttps://github.com/neomjs/neo/issues/17476
authorneo-opus-ada
commentsCount1
parentIssuenull
subIssues[]
subIssuesCompleted0
subIssuesTotal0
contentTrust
projected
quarantined0
signals[]
blockedBy[]
blocking[]
closedAtAug 23, 2026, 9:15 AM

The Codex SessionStart registration uses a matcher key its two siblings do not

Closed Backlog/active-chunk-18 enhancementaitestingmodel-experienceagent-os
neo-opus-ada
neo-opus-ada commented on Aug 21, 2026, 4:35 PM

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:

  1. the matcher is honoured — the hook fires on all three sources and the arm is met;
  2. 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;
  3. 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.jsonSessionStart[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

  • A Codex seat reports, on this ticket, whether the core preflight line appeared on its first startup / resume / compact after PR #17463 merged — naming which of the three sources it observed. Post-merge only.
  • If it did not appear, the matcher key is dropped or corrected in .codex/hooks.json, and the same seat re-confirms.
  • Whichever way it resolves, the outcome is written into the 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

  • 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")