Skip to main content

Native session capability matrix

This page is the INF-754 checkpoint for epic PLT-1804. It records what each mandatory macOS client surface exposes to a passive session reporter. It is the contract the session service (PLT-1807), the transport (INF-159) and the three adapters (INF-750, INF-751, INF-752) build on. The design is in .ai/specs/SPEC-plt-316-persistent-threads-session-tracking.md.

This checkpoint does not certify a release. INF-753 repeats the native evidence on the integrated revision, on two enrolled machines, with sessions a person starts directly.

Nor does it yet meet INF-754's exit condition. That needs each mandatory surface's hook, identity and trust behaviour observed in a session a person starts (the human-gated steps below), or an unsupported surface returned for a scope decision. INF-754 stays open until then, and the session service and the adapters wait on it.

Evidence tiers​

Every cell below carries one of these labels. They are not interchangeable.

LabelMeaning
ObservedThe installed native binary dispatched its own hooks to the capture probe. Two kinds of start were used: an agent typed into a pseudo-terminal, or the client ran headless (claude -p, codex exec). The Claude Code hooks were injected with --settings rather than a settings file. This shows how the client dispatches its hooks and what its lifecycle events mean. It does not show what happens when a person starts the session.
DocumentedVendor documentation or the vendor's source at the installed tag says so. Nothing was dispatched on this machine.
Human-gatedObservation needs a person to act: trust a hook, start a GUI session, use a second machine, or log in. The steps are in Human-gated steps.

Tested environment (machine 1)​

ItemVersion
macOS27.0.1
Claude Code CLI2.1.153 for the first headless run, then 2.1.296. The client updated itself between two runs on 2026-10-10. Every interactive result below is 2.1.296. Neither build puts its version in the hook payload.
Codex CLIcodex-cli 0.162.0-alpha.17.2, bundled inside the ChatGPT macOS app 26.1007.21159, which also hosts the local Codex desktop experience. It is not on PATH.
Cursor IDE3.21.16
cursor-agent CLI2025.09.17-25b418f. This surface is optional (D1). It was not evaluated: no account was configured for this evaluation, and this build predates the CLI hook support Cursor shipped in January 2026.

Matrix​

Claude Code CLI (mandatory)​

CapabilityResultTier
MechanismCommand hooks from settings files (~/.claude/settings.json, .claude/settings.json, .claude/settings.local.json) or --settings. Hooks from every level are merged, and all matching hooks run.Observed / Documented
Native IDsession_id in every payload. CLAUDE_CODE_SESSION_ID in the hook environment equals it on both builds.Observed
StartSessionStart with source=startup.Observed
Resume--resume <id> fires SessionStart(resume) with the same session_id under a new client process. This is the spec's "one native session, several process episodes".Observed
Compaction/compact fires PreCompact(manual), SubagentStop and then SessionStart(compact), with the same session_id. Compaction is not a new session.Observed
Clear/clear fires SessionEnd(clear) for the old ID, then SessionStart(clear) with a new session_id in the same process. It is a new native session, not a new episode.Observed
Turn endStop fires after each answer. It is not a session end.Observed
Normal end/exit fires SessionEnd(prompt_input_exit). A headless run fires SessionEnd(other).Observed
SignalsSIGTERM, SIGHUP and closing the terminal each fire SessionEnd(other), with exit statuses 143, 129 and 129. SIGKILL fires nothing.Observed
IdleAbout 60 s after an answer with no input, Notification(idle_prompt) fires once. There is no periodic hook.Observed
LivenessNo hook fires periodically. The hook environment carries CLAUDE_PID, and the client is the hook's direct parent process, so a sidecar can watch that process.Observed
VersionNot in the payload or the environment. It must be resolved per episode from the client executable.Observed
DeadlinesProbe version 1 finished in 14–36 ms, timed from the start of its capture, so without Node's startup. The documented SessionEnd budget is 1.5 s for all of a session's end hooks together.Observed / Documented
TrustInteractive sessions hold back every hook until the folder is trusted. On 2.1.296 a new git repository nested inside a trusted checkout still showed the trust dialog. Headless -p treats the folder as trusted and recorded no trust entry.Observed
Nested directoriesA session started in a nested repository inside a checkout ran that checkout's project hooks as well as its own. This held on both builds.Observed
Other surfacesThe same hooks are documented to fire in the desktop app and the IDE extensions.Documented

Codex CLI and local Codex desktop (mandatory)​

CapabilityResultTier
Mechanismhooks.json, or [hooks] in config.toml, at user level (~/.codex/) and project level (<repo>/.codex/). Every layer's matching hooks run. The repository's .codex/hooks.json and scripts/codex-native-hooks.ts already use this mechanism.Documented
TrustEach non-managed hook must be trusted for its current definition hash. Trust is stored only in the user config.toml, so a project cannot trust its own hooks. Under codex exec, untrusted project hooks were skipped silently, with no warning on stderr, while already-trusted user-level hooks ran.Observed / Documented
Native IDsession_id in the payload; subagent hooks carry the parent's. Shell commands receive CODEX_THREAD_ID; whether hook processes receive it is unverified.Documented
Start / resumeSessionStart with source = startup, resume, clear or compact; the installed tag's schema also allows fork. Whether the ID is stable across resume is not documented.Documented
EndSessionEnd always reports reason=other. It fires when the conversation is archived or deleted, when Codex closes normally, or after 30 minutes idle while no connected client has the conversation open. The payload cannot distinguish these causes. SessionEnd never fires for subagents.Documented
Turn endStop, which carries turn_id. Under codex exec no Stop dispatch was seen, even from the trusted user-level hook, so its dispatch there is still to be shown in an interactive session.Documented
CompactionSessionStart(compact) and PreCompact/PostCompact exist. Whether the ID survives compaction is undocumented.Documented
DeadlinesSessionEnd defaults to 1 s and is clamped at 3 s; other events default to 600 s.Documented
LivenessNo periodic hook is documented, and SessionEnd may follow 30 minutes of idleness, so liveness needs the same bounded sidecar as Claude Code. Which process to watch is still to be shown.Human-gated
VersionThe payload carries model but no client version, so it must be resolved from the executable.Documented
DesktopThe Codex app has an in-app hook-trust review flow. Whether the Codex tab inside the ChatGPT app 26.1007 shares that hook runtime is undocumented, and needs a session a person starts.Human-gated

Cursor IDE (mandatory)​

CapabilityResultTier
Mechanismhooks.json (version: 1) at enterprise, team, project (.cursor/hooks.json) and user (~/.cursor/hooks.json) level. All matching hooks run. Project hooks need a trusted workspace.Documented
Native IDconversation_id in every payload; sessionStart.session_id equals it. generation_id changes on every user message. Whether conversation_id survives reopening a chat is undocumented.Documented
LifecyclesessionStart and sessionEnd exist. sessionEnd carries reason (completed, aborted, error, window_close, user_close). A vendor-acknowledged forum report says sessionEnd runs only on window close, after the shell is torn down, so its command may never run. stop fires per turn with status.Documented
Claude hooksCursor loads Claude Code hooks from .claude/settings.json, .claude/settings.local.json and ~/.claude/settings.json by default, and maps their events onto its own. This repository's Claude hooks therefore also run in Cursor.Documented
EnvironmentCURSOR_PROJECT_DIR, CURSOR_VERSION, CURSOR_USER_EMAIL and CURSOR_TRANSCRIPT_PATH; the payload carries cursor_version.Documented
ResumeWhether reopening a chat keeps conversation_id and fires sessionStart again is undocumented.Human-gated
CompactionpreCompact exists; whether compaction keeps conversation_id is undocumented.Human-gated
DeadlinesA per-hook timeout exists, but no default is documented. Step 4 records how long each hook took, not the limit, so the limit itself stays to be shown.Human-gated
LivenessNo periodic hook is documented, and sessionEnd may not run at all, so liveness needs a bounded sidecar. Which process to watch is still to be shown.Human-gated
Versioncursor_version in the payload and CURSOR_VERSION in the hook environment.Documented
DispatchNot yet observed on this machine. Step 4 below also checks that beforeSubmitPrompt lets a prompt through when the probe prints nothing.Human-gated

Design inputs for the adapters​

These follow from the matrix. Each owning task confirms them against its own native canary.

  • Liveness needs a sidecar for Claude Code. No hook fires periodically. A bounded reporter tied to CLAUDE_PID, sending a heartbeat every 30 s, is the only honest liveness source. It must stop when that process exits. The spec already allows a sidecar on these terms.
  • A missing end is normal. SIGKILL produces no SessionEnd, and Codex's idle SessionEnd can arrive 30 minutes late, so a session with no end observation becomes disconnected through missing heartbeats. Its outcome stays unknown.
  • Identity transitions differ by client. Claude Code keeps the ID across resume and compaction, and issues a new ID on /clear inside the same process. The adapter must therefore close one session and open another in a single process episode. It must not merge them into one session.
  • Record the client version per episode. The version is not in the payload, and it changed under a running test session. Resolve it from the client executable each time an episode starts.
  • Detect the host client. Cursor runs .claude/settings*.json hooks by default. A Claude Code adapter that sees cursor_version or CURSOR_* must report the session as Cursor, or the session is registered twice under the wrong client.
  • End hooks only spool. Codex allows 1–3 s and Claude Code 1.5 s in total. An end hook validates its input and appends to the local queue; the network drain runs separately.
  • Trust is the human's. Codex trust lives in the user's config.toml, and Claude Code and Cursor trust is per folder or workspace. Setup never writes trust state. It prints the step the person takes.
  • Existing hooks stay. Other tools commonly install user-level hooks in all three clients. Adapters add their own entries and never rewrite or reorder an existing one.

Human-gated steps​

Run these on each enrolled machine. In hook mode the capture probe records only the payload's key names, ID digests, enumerated values, process ancestry and git shape. An enumerated value is kept only when it is one of the values the client documents for that field (a closed list in the probe), and a number only in a documented count field within its bound. A version is kept only in dotted numeric form with at most a known pre-release word, and a model only as its family name, never the full value. Anything else, of any shape, is recorded as <unrecorded>. It writes only to ~/.constellation/agent-session-probe/ (mode 0700), or to the folder CONSTELLATION_PROBE_DIR names. It prints nothing on stdout and sends nothing; when it cannot record, or the spool is full, it prints one stderr line naming only an error code, and still exits 0. Each capture budgets its lookups against a deadline from the probe's own start: 750 ms for an end event, or for an event it cannot name, and 1.5 s for any other. That is half of Claude Code's shared 1.5 s end-hook budget, and under the 2 s the probe sets for every Codex hook. Each lookup, and the wait for stdin, gets only the time left, so an end event's record is normally written by then. Only a very slow start can make it later, and then the lookups are skipped rather than the record lost. The probe exits once its record is written, even if a client holds its stdin open. A lookup the deadline or a failure cut short is named in the record's enrichment, as timed_out, skipped or failed, and enrichment.stdin_wait_ms records how long stdin was given; report counts the gaps per session as gaps. From probe version 2, elapsed_ms counts from the probe's process start, Node's startup included (INF-770). init-workspace writes one client's hook file into the workspace you name and never writes user-level configuration. Use one workspace per client: Cursor also runs .claude/settings*.json hooks, so a workspace holding two formats would record every Cursor event twice. remove-workspace deletes exactly the file it wrote, and keeps it if its content has changed since. It leaves the workspace's .git, the emptied client folder, and any trust you granted in steps 1–4 for you to remove. The spool is local evidence: its git digests are unsalted hashes of local paths, so summarise it rather than share it raw. The probe refuses a client folder or hook file that is a symlink, but it is a disposable harness for your own scratch folder, not a tool hardened against a hostile workspace.

node scripts/agent-session-tracking/capability-probe.mjs init-workspace "$HOME/probe-claude" --client claude-code
node scripts/agent-session-tracking/capability-probe.mjs init-workspace "$HOME/probe-codex" --client codex
node scripts/agent-session-tracking/capability-probe.mjs init-workspace "$HOME/probe-cursor" --client cursor
node scripts/agent-session-tracking/capability-probe.mjs report
node scripts/agent-session-tracking/capability-probe.mjs remove-workspace "$HOME/probe-claude"
  1. Claude Code, started by a person. Open a terminal in probe-claude and run claude. Accept the folder trust dialog yourself. Send one message, then /exit. Run claude --continue, send a message, run /compact, then close the terminal window. Run report: the session appears once, with two process episodes.
  2. Codex CLI. Run codex in probe-codex. Trust the probe hooks with /hooks. Send a message, run /compact, then quit, then codex resume the same session. Check that SessionStart(startup) fired (the hook file was accepted), that PreCompact and PostCompact were recorded, and that SessionStart(resume) keeps the session digest.
  3. Codex desktop. Open probe-codex as a project in the ChatGPT app's Codex tab. Review and trust the hooks in-app, start a thread, close the app and reopen the same thread.
  4. Cursor IDE. Open probe-cursor in Cursor and trust it. Start an agent conversation, send two messages, then compact the conversation and send one more. Close the window, reopen the same chat and send another. Check that sessionStart fired (the hook file was accepted), that preCompact and sessionEnd were recorded, that the conversation_id digest held across compaction and reopening, and that the probe's empty output let every prompt through. Run report to see how long each hook took, and any gaps.
  5. Second machine. Repeat steps 1–4 on the second enrolled Mac.
  6. Reporter credential. Confirm which existing credential the reporter enrolls with: the pt login credential store, or a SERVICE-principal key for headless use. No vendor credential is used.

Bounded test plan for the adapter tasks​

Each adapter task runs these scenarios on both machines, with the integrated reporter in place of the probe. A failed scenario is retried at most twice with a changed hypothesis. After that it becomes a recorded blocker, never a waived row.

ScenarioClaude CodeCodex CLICodex desktopCursor IDE
Direct start, explicit task bindrequiredrequiredrequiredrequired
Resume keeps one native sessionrequiredrequiredrequiredrequired
Compaction is not a new sessionrequiredrequired—required
New conversation in one process (/clear)requiredrequired——
Normal endrequiredrequiredrequiredrequired
SIGKILL or crash → disconnected ≤ 120 srequiredrequiredrequiredrequired
Offline replay does not mark liverequiredrequiredrequiredrequired
Trust disabled → degraded, session intactrequiredrequiredrequiredrequired
Hosted inside Cursor reports as Cursorrequired——required
Uninstall leaves no process or hookrequiredrequiredrequiredrequired