Plugin activation/readback drift can fail closed before provider calls and make recovery non-atomic
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 125k
- Forks
- 19.4k
- PR merge metrics
- PR metrics pending
Description
What issue are you seeing?
A third-party Codex plugin can be reported as installed/enabled and still become unusable at the point where a PreToolUse safety hook must authorize a provider call.
In the observed case, a normal, user-requested latest-inbound reply was about to be sent through a Telegram provider. The opt-out hook ran before the provider call, but the runtime-selected plugin artifact did not match the plugin's single-project exact activation binding. The hook emitted the redacted error activation-invalid; its wrapper surfaced telegram_optout_check_unavailable; and Codex blocked the tool call before the Telegram API was invoked. A generic fallback was also unavailable because its key was intentionally host-local and absent on that host. The result was a delayed legitimate reply, not a rejection by Telegram.
Recovery exposed a related plugin portability/activation gap:
- static seal/readback validation observed a stale content hash;
- path resolution used by post-install readback was off by one directory level;
- an isolated Python process could not import a dependency packaged for the plugin (
Telethon); - the one-shot staged installer correctly refused activation and verified rollback, but the plugin remained blocked.
Failing closed is correct for this safety hook. The bug is that the Codex plugin/runtime contract does not provide a canonical, immutable activation identity plus a durable dependency-complete artifact that both the hook and installer can verify. Plugin state can therefore say "installed/enabled" while the active hook cannot prove that it is the intended artifact.
What steps can reproduce the bug?
This is the smallest provider-neutral form of the reproduction; no private project data is required.
- Create a plugin with a
PreToolUsehook in.codex-plugin/plugin.json. - Have the hook fail closed unless a single-project activation record exactly matches the active plugin ID, version, resolved root, and content hash.
- Include a Python runtime dependency in the hook implementation (the observed dependency was
Telethon) and execute the validation in isolated Python. - Install the plugin from a marketplace/cache, generate a static seal, and record the activation binding.
- Refresh or recover the plugin so that Codex selects/materializes a cache root whose runtime identity or post-install readback path differs from the activation record, while the plugin still appears installed/enabled.
- Start a fresh Codex CLI session and invoke a tool that would send a normal reply through a provider.
- Observe that the hook runs, reports
activation-invalid, and the wrapper reports the provider-specific checker as unavailable. Confirm that the provider API is never called. - Attempt a staged one-shot repair that seals, reads back, imports dependencies under isolated Python, and activates only after all checks pass.
- Observe one or more of: stale seal hash, one-level path-index mismatch, missing packaged dependency in isolated Python, followed by rollback-verified/blocked state.
The core invariant can be represented as:
active = runtime_readback()
assert active.plugin_id == expected.plugin_id
assert active.version == expected.version
assert active.resolved_root == expected.resolved_root
assert active.content_hash == expected.content_hash
import telethon # must resolve from the installed plugin artifact, not global state
Today, plugins have to invent each side of this contract, and the values can be derived from different roots or runtime environments.
What is the expected behavior?
- Codex should expose one canonical, host-verifiable identity for the exact active plugin artifact: plugin ID, version, immutable resolved root, and content hash.
- Install, seal, dependency validation, activation, and readback should use that same identity and complete as one staged transaction. A failed candidate should leave the last-known-good artifact active.
- A plugin should not be reported as installed/enabled/current until its declared hook runtime and dependency closure are runnable in the interpreter environment Codex will actually use.
- Plugin-local dependencies should be declared and materialized durably, including for isolated Python, rather than depending on unrelated global packages.
codex doctoror equivalent diagnostics should distinguish activation-identity mismatch, stale readback, dependency-closure failure, and provider failure. Provider calls must remain blocked when a fail-closed safety hook is genuinely unavailable; the fix should make the approved checker reliably available, not bypass it.
After a successful atomic install/readback, the safety hook should validate and the normal provider call should proceed exactly once.
Additional information
Environment where the chain was observed/rechecked:
- Codex CLI:
0.152.0 - OS: Linux x86_64 (
7.0.0-28-generic) - Python:
3.14.4 - Fresh CLI session; provider call blocked before network invocation
Related, but not duplicate:
- #41177 covers the absence of a supported plugin runtime dependency installation contract. That explains the isolated-Python dependency part, but not activation-binding drift, fail-closed pre-provider blocking, seal/readback disagreement, or transactional recovery.
- #39895 covers hooks being silently disabled by manifest precedence. Here the hook does execute and blocks because its active artifact cannot be verified.
- #30993 covers stale cached skill resolution. This report concerns hook activation identity and post-install validation rather than
$skillselection.
Existing issues were searched for the exact errors and for Telegram opt-out hooks, activation bindings, static seals, stale hashes, isolated Python dependencies, path-index errors, and rollback-verified installs. No matching report was found.
All customer names, project names, prompts/conversation text, message IDs, phone numbers, account identifiers, tokens, credentials, private URLs, hostnames, and host paths are intentionally omitted.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Read .codex-plugin/plugin.json and trace the PreToolUse hook, staged installer, static seal/readback, and isolated-Python validation; use the reproduction to observe activation-invalid before any provider call. Done means the same immutable identity and dependency-complete artifact pass install, activation, and readback atomically, with failed candidates rolled back and diagnostics distinguishing the reported failure modes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python, rust
- Domain
- cli, security, tooling
- Issue type
- Bug
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100