Plugin hooks fail with exit 1 for the remainder of a session after the startup marketplace auto-upgrade replaces the cached plugin version

Open 💬 7 comments Opened Jul 7, 2026 by Victor-Quqi
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

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:

  1. On startup, maybe_start_plugin_startup_tasks_for_config (codex-rs/core-plugins/src/manager.rs) unconditionally spawns the plugins-marketplace-auto-upgrade thread. When the git marketplace has new commits, it activates the new marketplace root and calls refresh_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.json picked up new upstream revisions with no manual action.
  2. The reinstall path in codex-rs/core-plugins/src/store.rs invalidates the previously active cache directory either way: a version bump ends with remove_old_plugin_versions deleting the old version directory (remove_dir_all); an unchanged version replaces the whole plugin cache entry via a backup/rename swap.
  3. 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.
  4. 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:

  1. 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).
  2. Add the marketplace and install the plugin.
  3. 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.
  4. Start a new Codex TUI session and trigger hook events.
  5. 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.

View original on GitHub ↗

7 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #31115

Powered by Codex Action

Victor-Quqi · 1 month ago

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:

  • Resolved cache paths outliving their directory after a refresh: #25285 (skills), #31115 (bundled browser), #30270 (stale bundled marketplace path in config)
  • Non-atomic cache replacement under Windows file locks: #28247, #24296, #25874, #28277
  • Inverse symptom — stale cache entry still resolving: #30993

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.

Victor-Quqi · 1 month ago

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-remote fails with exit code 128), the loader logs

WARN codex_core_plugins::loader: configured non-curated plugin no longer exists in discovered marketplaces during cache refresh plugin="anyrouter-status-monitor" marketplace="router-vitals"

and from that point the process silently stops firing that plugin's hooks entirely — no hook/started events in logs_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.

woogis-code · 1 month ago

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 a PreToolUse hook, 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:

  • hook discovery expands the plugin-root placeholder into the versioned cache directory and retains that resolved command in the session's configured handler;
  • plugin cache replacement activates the new version and immediately removes old version directories.

I prepared a focused patch here:

The patch:

  1. shares active plugin-version selection with the hook runtime;
  2. retains the raw plugin hook command template and plugin cache base root;
  3. re-resolves PLUGIN_ROOT / CLAUDE_PLUGIN_ROOT immediately before every hook execution, so an already-running session follows A → B upgrades and remains valid after A is garbage-collected;
  4. treats a missing active plugin root or missing static root-relative hook path as a recoverable hook failure with guidance, rather than allowing a missing Python script's exit code 2 to become a hard PreToolUse block.

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 passed
  • just test -p codex-hooks: 121 passed
  • just test -p codex-core-plugins: 315 passed
  • full just 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 macOS xcrun stub for the seatbelt/openpty test, and unsetting inherited CODEX_SANDBOX_NETWORK_DISABLED for the user-shell test)
  • just fix, just fmt, and Bazel lock update completed

Per 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?

luozhixiong01 · 1 month ago

Additional confirmation from macOS, including the cross-process case inferred in the issue body.

Environment:

  • macOS arm64
  • the active Codex TUI session was started with codex-cli 0.144.1
  • a second app-server process running codex-cli 0.144.3 performed the Git marketplace refresh

Observed sequence:

  1. Session A loaded plugin hooks from a versioned cache path such as ~/.codex/plugins/cache/example-marketplace/example-plugin/1.0.0/.
  2. While Session A remained active, another app-server process refreshed the marketplace and installed plugin version 1.0.1.
  3. The refresh removed the 1.0.0 directory. Session A retained hook commands resolved against that deleted path.
  4. Every subsequent hook invocation in Session A failed with exit code 127 and "No such file or directory". A newly started session resolved 1.0.1 and worked normally.
  5. For PreToolUse, Codex displayed the hook failure but continued executing the shell tool, so the hook enforcement effectively failed open.

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.

sangalli · 1 month ago

Independent reproduction on macOS confirms the version-bump path described here:

  • a marketplace update replaced plugin version A with version B;
  • an already-running session retained absolute Stop and UserPromptSubmit hook commands under version A;
  • the installer removed version A, after which every invocation in that session exited with code 1;
  • invoking the same hooks from version B exited successfully, and a fresh session loaded version B successfully.

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:

  1. retains superseded plugin versions for seven days from their first retirement;
  2. prunes expired versions during a later install/update;
  3. replaces only the current version directory during a same-version force reinstall, preserving still-retained siblings;
  4. continues removing local or 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-plugins run 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-plugins and just fmt complete 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?

behruznassre · 28 days ago

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: SessionStart fails in addition to the already-reported Stop and UserPromptSubmit, 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-os label may be understating the scope.