Plugin hooks fail with exit 1 for the remainder of a session after the startup marketplace auto-upgrade replaces the cached plugin version
What version of Codex CLI is running?
codex-cli 0.142.5
What subscription do you have?
None
Which model were you using?
gpt-5.5
What platform is your computer?
Windows 11 Home (10.0.22631)
What terminal emulator and version are you using (if applicable)?
Windows Terminal + PowerShell 7.6.3
Codex doctor report
Omitted — the report contains no plugin/marketplace lifecycle diagnostics beyond what is described below; specific sections available on request.
What issue are you seeing?
I maintain a plugin installed from a git marketplace that declares SessionStart / UserPromptSubmit / Stop hooks. After new commits land in the marketplace repo, the first Codex session started afterwards reports hook exit 1 on every hook event, for the entire session — without any manual upgrade having been run. Starting another session makes the problem disappear.
Reading the source (main branch; behavior observed on 0.142.5), this looks like a lifecycle race between the startup marketplace auto-upgrade and hook declaration loading:
- On startup,
maybe_start_plugin_startup_tasks_for_config(codex-rs/core-plugins/src/manager.rs) unconditionally spawns theplugins-marketplace-auto-upgradethread. When the git marketplace has new commits, it activates the new marketplace root and callsrefresh_non_curated_plugin_cache_force_reinstall_detailed, which reinstalls the plugin cache even when the plugin version is unchanged. This part is directly visible in the source, and the auto-upgrade itself is confirmed on my machine:.codex-marketplace-install.jsonpicked up new upstream revisions with no manual action. - The reinstall path in
codex-rs/core-plugins/src/store.rsinvalidates the previously active cache directory either way: a version bump ends withremove_old_plugin_versionsdeleting the old version directory (remove_dir_all); an unchanged version replaces the whole plugin cache entry via a backup/rename swap. - Hook commands reference
${PLUGIN_ROOT}, which resolves to a versioned cache path (plugins/cache/<marketplace>/<plugin>/<version>/…). I could not pin down the exact expansion site in the source, but the observed behavior — the already-running session fails on every subsequent hook event while a freshly started session works — suggests the resolved path is captured when the plugin is loaded and not re-resolved for the lifetime of the session. - If that reading is right, the failure sequence is: hooks load early against the old cache path, the background upgrade then deletes or replaces that path, and every later hook invocation runs
node <now-gone-path>/scripts/hook.mjs, which exits 1 (ERR_MODULE_NOT_FOUND) until the session ends. The next session resolves the already-upgraded cache and works — matching what I see.
Since the cache is shared, a newly started Codex process should likewise invalidate hooks of other sessions still running. I have not specifically reproduced this cross-process case; it follows from the same mechanism.
Windows note: the rename swap can presumably fail while a hook node process briefly holds file handles. Possibly related, one upgrade attempt left a stale marketplace-upgrade-* staging directory behind under .tmp/marketplaces/.staging/.
What steps can reproduce the bug?
Observed with a real plugin; a minimal setup should look like:
- Create a git marketplace with one plugin whose hook command references
${PLUGIN_ROOT}, e.g.node "${PLUGIN_ROOT}/scripts/hook.mjs"(script body irrelevant; exiting 0 is enough). - Add the marketplace and install the plugin.
- Push a commit to the marketplace repo. Per the analysis above, bumping the plugin version should make the failure deterministic; content-only changes only expose the shorter swap window.
- Start a new Codex TUI session and trigger hook events.
- Hook invocations fail with exit 1 for the rest of the session; the next session works again.
What is the expected behavior?
A marketplace auto-upgrade should not invalidate hook commands of running sessions. Possible directions:
- re-resolve / reload hook declarations after the startup auto-upgrade completes (at least in the process that performed it), or
- resolve
${PLUGIN_ROOT}lazily at hook-execution time against the currently active version, or - keep superseded version directories until no running session references them (grace period / in-use tracking) instead of deleting them right after activation.
Additional information
Timeline on my machine consistent with the analysis: hooks in a running session began failing a few hours after an upstream push, in the same window as an auto-upgrade attempt (the one that left the stale staging directory); the install metadata showed the new revision although no manual upgrade was run. Source references are against current main — happy to re-verify against a specific tag if useful.
7 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Not closing as a duplicate of #31115 (different surface and symptom), but a quick tracker sweep suggests one common plugin-cache lifecycle problem behind several open issues:
Common thread: cache refresh/reinstall isn't transparent to consumers — paths are resolved early, superseded version dirs are deleted immediately, and the swap isn't atomic on Windows. The fix directions in the issue body (re-resolve after upgrade / lazy
${PLUGIN_ROOT}/ grace period before deleting old versions) would likely cover several of these at once.Observed a second failure mode in the same auto-refresh subsystem today (same machine, 0.142.5): when the marketplace cache refresh cannot reach GitHub (flaky network,
git ls-remotefails with exit code 128), the loader logsand from that point the process silently stops firing that plugin's hooks entirely — no
hook/startedevents inlogs_2.sqlite, no hook errors, and the on-disk plugin cache is untouched (so this is not the rename-swap / exit-1 path described above). Sessions started after connectivity recovers fire hooks normally again.Net effect: a transient GitHub connectivity blip disables an installed plugin's hooks for the rest of the process. A failed refresh should probably keep the last known marketplace snapshot instead of treating configured plugins as orphaned.
I hit the same cache-lifecycle failure with a Python plugin hook using
${CLAUDE_PLUGIN_ROOT}: after a version bump, the running session kept the old versioned command path after that directory had been removed. For aPreToolUsehook, Python's missing-script exit code 2 plus stderr was then interpreted as an intentional hook block, which made every shell/patch operation fail.I traced the current source and confirmed the two relevant paths:
I prepared a focused patch here:
The patch:
PLUGIN_ROOT/CLAUDE_PLUGIN_ROOTimmediately before every hook execution, so an already-running session follows A → B upgrades and remains valid after A is garbage-collected;PreToolUseblock.The regression test uses one hook engine/session and covers 0.28.0 → 0.29.0 rebinding, deletion of 0.28.0, deletion of the active hook script, and deletion of the active cache root. The latter two remain fail-open.
Validation:
just test -p codex-plugin: 5 passedjust test -p codex-hooks: 121 passedjust test -p codex-core-plugins: 315 passedjust test: 12,049 passed; two unrelated host-environment failures were reproduced individually and both passed after isolating the inherited environment (Homebrew Python ahead of the macOSxcrunstub for the seatbelt/openpty test, and unsetting inheritedCODEX_SANDBOX_NETWORK_DISABLEDfor the user-shell test)just fix,just fmt, and Bazel lock update completedPer the external contribution policy, I have not opened a PR without an explicit invitation. If this approach matches the intended fix, would a maintainer be willing to invite a PR from this branch?
Additional confirmation from macOS, including the cross-process case inferred in the issue body.
Environment:
Observed sequence:
The missing root affected all hooks from that plugin, including pre-tool write checks and stop/turn gates. Manually invoking the hook from the current 1.0.1 cache succeeded, ruling out a hook dependency or PATH problem.
This confirms cross-process cache invalidation on macOS, rather than only the same-process startup race or a Windows-specific file-handling issue. Retaining superseded versions for a grace period, or tracking active version leases before garbage collection, would preserve running-session consistency. As a useful comparison, another plugin host on the same machine keeps prior versions and records in-use/orphaned lifecycle markers, which avoids invalidating active sessions during upgrades.
Independent reproduction on macOS confirms the version-bump path described here:
StopandUserPromptSubmithook commands under version A;I prepared a focused alternative branch that addresses the shared-cache lifetime directly:
https://github.com/sangalli/codex/tree/fix/plugin-version-retention
The change:
localor higher versions immediately when retaining them would prevent the newly installed version from becoming active.The regression coverage exercises recent-version retention, expiry cleanup, and same-version reinstall. I verified the red state against the current immediate-deletion behavior before implementing the change. The new tests pass; the full
codex-core-pluginsrun passed 352/354 tests, with two unrelated host-environment fixture failures reproduced independently (a local bare-Git push fixture and a skill snapshot polluted by user-level skills).just fix -p codex-core-pluginsandjust fmtcomplete cleanly.Per the external contribution policy, I have not opened a PR. If this grace-period approach matches the intended lifecycle fix, would a maintainer be willing to explicitly invite a PR from this branch?
Still reproduces on codex-cli 0.146.0 (macOS, Node 22.22.1) — posting only as a version-recency data point, since the cross-process/shared-cache case itself is already confirmed above on 0.144.x and again in the independent macOS reproduction, and I have nothing to add to those.
Same shape: a long-running session held a marketplace plugin at 1.4.1, a separate Codex process refreshed the shared cache to 1.4.2 and removed the 1.4.1 directory, and the original session kept executing hook paths under the deleted root until it was restarted (direct invocation of the retained paths reproduced
MODULE_NOT_FOUND). Two details worth adding to the record:SessionStartfails in addition to the already-reportedStopandUserPromptSubmit, and the session's model-visible world state still named the deleted 1.4.1 skill root after the cache update — including immediately after compaction, which appears to be an exposure point (it triggers lifecycle hooks) rather than a cause. Observed across two unrelated projects on the same machine.Given three macOS reports now, the
windows-oslabel may be understating the scope.