codex plugin list reports installed, enabled for a plugin whose plugins/cache payload is missing, and the model sees none of its skills

Open 💬 0 comments Opened Jul 20, 2026 by konard

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 its
skills/ 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)

  1. Make plugin list stat the payload: report installed only when the

version directory referenced by the config entry actually exists (and,
ideally, report enabled, payload missing rather than installed, enabled).

  1. Reconcile on load: when a plugin is enabled in config.toml but its cached

payload is absent, re-materialize it from the configured marketplace instead
of rendering a session without it.

  1. Add a codex plugin doctor (or plugin 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).

View original on GitHub ↗