codex plugin list reports installed, enabled for a plugin whose plugins/cache payload is missing, and the model sees none of its skills
Summary
codex plugin list reports a plugin as installed, enabled (and --json
returns "installed": true, "enabled": true) when the plugin's payload under$CODEX_HOME/plugins/cache/<marketplace>/<plugin>/<version>/ has lost itsskills/ subtree. In that state the plugin contributes zero skills to<skills_instructions>, so a model is told nothing about skills the operator
believes are installed, and any automation gated on plugin list skips the one
command that would fix it.
Enablement (config.toml) and exposure (the materialized payload) are tracked
independently and never reconciled, and there is no codex plugin doctor /verify / --repair to ask about the second one.
Observed with codex 0.144.6 on Linux.
Reproduction
export CODEX_HOME=$(mktemp -d)
# any marketplace works; this uses a renamed copy of the curated catalogue
git clone -q --depth 1 https://github.com/openai/plugins.git /tmp/plugins-src
cp -r /tmp/plugins-src /tmp/mkt && rm -rf /tmp/mkt/.git
python3 - <<'PY'
import json
p = "/tmp/mkt/.agents/plugins/marketplace.json"
m = json.load(open(p)); m["name"] = "repro"
m["plugins"] = [x for x in m["plugins"] if x["name"] == "superpowers"]
json.dump(m, open(p, "w"), indent=2)
PY
codex plugin marketplace add /tmp/mkt --json > /dev/null
codex plugin add superpowers@repro --json > /dev/null
codex debug prompt-input probe | grep -c 'superpowers:' # => 14
# Remove ONLY the skills/ subtree; the version directory and config.toml survive.
find "$CODEX_HOME/plugins/cache/repro/superpowers" -maxdepth 2 -name skills -type d -exec rm -rf {} +
codex plugin list | grep superpowers
# superpowers@repro installed, enabled <version> ...
codex plugin list --json | python3 -c 'import json,sys; print([ (p["pluginId"], p["installed"], p["enabled"]) for p in json.load(sys.stdin)["installed"] ])'
# [('superpowers@repro', True, True)]
codex debug prompt-input probe | grep -c 'superpowers:' # => 0
Installed, enabled, and invisible — simultaneously.
The same thing happens when the whole plugins/cache/<marketplace>/<plugin>
directory is removed while config.toml keeps its [plugins."…"] block, which
is how we hit it in practice (a persistent per-repository CODEX_HOME whose
cache did not survive a container rebuild).
Impact
We provision a repository-scoped CODEX_HOME before starting codex exec. The
provisioning step used plugin list to decide whether an install was needed, so
in this state it skipped plugin add, launched the session, and the model
correctly refused the task because the mandated skills were not in its prompt.
The failure is silent from the CLI's point of view: every command exits 0 and
every status field says healthy.
It is also not self-correcting from inside a session — a codex plugin add run
by the model after the session started does not retroactively add the skills to
the already-rendered prompt.
Workaround
Do not trust plugin list for this question. Stat the payload directly, and
verify against the rendered prompt:
ls "$CODEX_HOME/plugins/cache/<marketplace>/<plugin>"/*/skills/*/SKILL.md
codex debug prompt-input probe | grep -o '<plugin>:[a-z-]*' | sort -u
codex plugin add <id> repairs both a missing and a gutted payload and is
idempotent, so it is safe to run unconditionally.
Suggested fixes (any one would be sufficient for us)
- Make
plugin liststat the payload: reportinstalledonly when the
version directory referenced by the config entry actually exists (and,
ideally, report enabled, payload missing rather than installed, enabled).
- Reconcile on load: when a plugin is enabled in
config.tomlbut its cached
payload is absent, re-materialize it from the configured marketplace instead
of rendering a session without it.
- Add a
codex plugin doctor(orplugin list --verify) that reports
payload-vs-config divergence and can repair it, so automation has a
supported command to call.
Related but distinct: #31365 (payload present, config entry lost — the mirror
image), #21138 (payload stale because refresh compares only the version string),
#29103 (marketplace source not persisted).
2 Comments
Confirmed this is still reproducible on the current stable
codex-cli 0.147.0on macOS 26.5 arm64.I reran the original scenario with isolated temporary
HOME,CODEX_HOME, XDG directories, and a local shallow clone ofopenai/plugins; no model, API call, or personal Codex state was involved.Observed sequence:
So this now has cross-version evidence from 0.144.6 to 0.147.0 and reproduces outside Linux. I am inspecting the current loader/list reconciliation path for a focused regression test and minimal fix; I will link a PR here if that remains unaddressed on
main.I traced the mismatch to
PluginStore::is_installed: it currently answers whether an active version directory exists, while skill discovery separately requires the declared legacy skill paths to still exist. That lets list state remaininstalled: trueafter only the cachedskills/subtree is removed.A focused fix is ready on
builtbyhuy:fix/plugin-list-payload-validationat535a454. The approach preserves the existing directory-presence meaning ofis_installedfor remote metadata/script attribution, adds a payload predicate only for plugin-list reconciliation, checks explicitly declared legacy skill paths, and keeps Agent Plugin conventional optional roots valid.Regression coverage includes:
skills/, observeinstalled: falsewhileenabled: true, then reinstall and confirm repairskills/directoryObserved locally:
core-plugins390/390, CLI 351/351, plusjust fixfor both crates andjust fmt.Per the invitation-only contribution policy, I have not opened an unsolicited PR. If this approach matches the intended fix and a maintainer would like a PR, I can submit the prepared branch.