Skip to content

fix(codex): run the SessionStart hook when Codex forks a session - #2290

Open
Fengzdadi wants to merge 1 commit into
oceanbase:masterfrom
Fengzdadi:fix/codex-session-start-fork
Open

Fengzdadi wants to merge 1 commit into
oceanbase:masterfrom
Fengzdadi:fix/codex-session-start-fork

Conversation

@Fengzdadi

Copy link
Copy Markdown
Contributor

Which issue or RFC does this PR close?

Fixes #2287.

Found while running real hosts for #2250. This is a behavior fix, so it is separate from the #2250 PRs, which only move code.

Rationale for this change

The Codex plugin registers its SessionStart hook with the matcher startup|resume|clear|compact. Codex sends source: "fork" when a session is forked, so it never runs the plugin's hook for a fork. A forked session then gets no bootstrap package, and the plugin does not write a Session binding for the new session ID. The first prompt still prepares context, because Scope resolution falls back to the workspace binding.

The hook script already accepts fork (_LIFECYCLES in hooks/session_binding.py), and so do the Server's bootstrap lifecycle enum and RFC 1429. The Claude Code plugin's matcher and docs include fork. Only the Codex matcher and the Codex docs leave it out, and they have done so since ba73996 (#1720).

What changes are included in this PR?

  • integrations/codex/plugins/powercontext/hooks/hooks.json: the SessionStart matcher is now startup|resume|clear|compact|fork.
  • docs/en/docs/integrations/codex.md and docs/zh/docs/integrations/codex.md: the bootstrap section lists fork. The plugin README does not list lifecycles, so it is unchanged.
  • Tests:
    • tests/codex_plugin/test_contract.py: the registered SessionStart matcher must select each lifecycle Codex sends (startup, resume, clear, compact, fork), checked with re.fullmatch. The exact-string assertion on the matcher is gone; the test now asserts the matcher's behavior. With the old matcher, the fork case fails.
    • tests/e2e/test_bootstrap_host_service_chain.py: a codex-recorded-fork case runs the packaged hook in a subprocess against a live Server with the payload Codex 0.165.0-alpha.1.2 sent for a fork. The session ID, working directory, and transcript path take the test's values; hook_event_name, model, permission_mode, and source stay as recorded. The case checks the same chain as the startup cases: the package is injected, the first prompt reports the receipt, and ordinary recall works. The hook script already accepted fork, so this case also passes without the fix. It guards the script's handling of the real payload; the contract test above is the one that fails on the old matcher. Like the existing cases, it sets an explicit Scope, so it does not cover the Session binding. The binding was checked only on the real host (below).

Open question: restore. The script and the Server accept restore, but I found no sign that Codex sends it. In the Codex 0.165.0-alpha.1.2 binary, the strings next to the SessionStart output errors list exactly startup, resume, clear, compact, and fork. The matcher therefore leaves out restore. Should the script keep accepting it, or should it go?

Are there any user-facing changes?

Yes. With the updated plugin, forking a Codex session (codex fork, codex exec fork) runs the SessionStart hook. The new session gets a Session binding and, when POWERCONTEXT_CODEX_BOOTSTRAP_CONTEXT=true, the bootstrap package. Nothing changes for the other lifecycles.

A fork keeps its parent's history, which may already contain the parent's package. The forked session then sees the package twice, bounded by POWERCONTEXT_CODEX_BOOTSTRAP_MAX_BYTES. The Claude Code plugin already behaves this way for fork, and RFC 1429 lists fork as a bootstrap lifecycle.

Codex records a trust hash for each hook. I compared two isolated Codex homes whose plugin hooks.json files differ only in this matcher. The SessionStart hook's hash differed, and the hashes of the other three hooks were the same. Users who already trusted the plugin's hooks will probably be asked to review the SessionStart hook again after updating. I did not test the update path.

There are no API or persisted-format changes.

How was this change tested?

  • uv run pytest tests/codex_plugin/test_contract.py tests/e2e/test_bootstrap_host_service_chain.py: 35 passed.
  • Mutations:
    • Restoring the old matcher fails only test_codex_session_start_hook_runs_for_every_lifecycle_codex_sends[fork].
    • Removing fork from the script's _LIFECYCLES fails only test_packaged_host_hooks_complete_the_live_bootstrap_chain[codex-recorded-fork].
  • make check: passed.
  • make unit-test: 6386 passed, 303 skipped.
  • make e2e-test: 1144 passed, 491 skipped.
  • make docs-test: passed (979 pages verified).
  • Real host. I ran Codex CLI 0.165.0-alpha.1.2 (bundled with Codex.app) on macOS in an isolated CODEX_HOME, with the plugin installed from this branch through codex plugin marketplace add and codex plugin add. It ran against a local Server with enforced auth. One Memory tagged bootstrap-context held a random marker. The plugin hooks were trusted once through the interactive "Hooks need review" dialog; I did not use --dangerously-bypass-hook-trust.
    • codex exec (startup) and then codex exec fork <thread> both answered with the marker.
    • For the fork, the hook received source: "fork". The Server logged set_scope_binding, prepare_bootstrap_context, and record_bootstrap_delivery, followed by the prompt's prepare_context. The forked rollout holds the # PowerContext bootstrap context developer message, written after the fork started.
    • The marker answer alone does not prove anything for a fork, because the parent's history already contains the marker. The Server operations and the rollout entry are the evidence.
    • Old matcher, from an earlier run the same day with the same Codex version: for a fork, the Server logged only the requests of the UserPromptSubmit and Stop hooks (resolve_scope_binding, prepare_context, capture_content_source, get_stats). It logged no binding write and no bootstrap request.
    • clear and compact were not run on the real host.

AI usage statement

This PR was prepared with Claude Code (Claude Opus 5.5). It found the gap during the #2250 host runs, made the change and the tests, and ran the real Codex host and the checks. I reviewed the change and the results.

Codex sends source "fork" for a forked session, but the plugin's
SessionStart matcher stopped at compact, so the hook never ran: the
forked session got no bootstrap package and no Session binding. The
hook script and the Server already accept fork.

Fixes oceanbase#2287.
Copilot AI balanced review requested due to automatic review settings October 28, 2026 08:00

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

An organization owner can choose the repository organization as the billing source for reviews requested by members with a paid Copilot license. Learn about Copilot code review billing.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Codex plugin's SessionStart hook does not run for forked sessions

2 participants