Skip to content

fix(install-channel): a Codex plugin install was told it was "unknown" - #207

Merged
kevintseng merged 4 commits into
mainfrom
fix/codex-plugin-install-channel
Aug 24, 2026
Merged

fix(install-channel): a Codex plugin install was told it was "unknown"#207
kevintseng merged 4 commits into
mainfrom
fix/codex-plugin-install-channel

Conversation

@kevintseng

Copy link
Copy Markdown
Contributor

The report

From a real Codex CLI session:

❌ memesh update does not support this install method (unknown).
   Update this installation from the tool or workflow that installed MeMesh.

…on an install memesh fully supports. memesh doctor carried the same non-answer into its summary (PASS_WITH_CONCERNS, "install method unknown").

Cause

Codex CLI adopted Claude Code's plugin manifest format and its cache layout, one directory over:

Runtime Plugin cache
Claude Code ~/.claude/plugins/cache/<marketplace>/<plugin>/<version>
Codex CLI ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>

detectInstallChannel matched only .claude, so a Codex-hosted copy fell past every branch to unknown.

Confirmed on disk rather than assumed: ~/.codex/plugins/cache/pcircle-memesh/memesh/4.7.1 exists and mirrors the Claude Code layout exactly, and ~/.codex/config.toml carries [hooks.state."memesh@pcircle-memesh:hooks/hooks.json:…"] entries for every event — Codex runs memesh's own hook scripts, not just its MCP server.

The fix

Both runtimes classify as plugin-marketplace. That is deliberate: all three runtime consumers of the value ask the same host-agnostic question — "is this wired by a plugin runtime" — and the answer is yes for both. Splitting the enum would have meant editing all three conditions and still leaving the host-specific wording to fix.

What genuinely differs is the remediation, so that is derived, not assumed:

Recommended command
Codex CLI codex plugin marketplace upgrade pcircle-memesh, then codex plugin add memesh@pcircle-memesh
Claude Code memesh upgrade-plugin (unchanged)

A Codex user is deliberately not sent to memesh upgrade-plugin. That script reads $HOME/.claude/plugins/marketplaces/pcircle-memesh and patches $HOME/.claude/plugins/installed_plugins.json; neither exists under ~/.codex (checked — Codex keeps plugin state in config.toml and stages marketplaces under ~/.codex/.tmp), so it aborts with ERROR: marketplace cache not found. A confidently wrong instruction is worse than the generic one it replaces.

Codex's syntax was read off codex plugin marketplace upgrade --help and codex plugin add --help on this machine, not recalled.

Three places that said "Claude Code" to everyone now name the runtime they found: the doctor's hook-wiring row, its shell-CLI row, and the session-start update banner.

getInstallChannelSupport's second parameter is required, not optional. Every caller already holds that path — it is what produced the channel — so an optional one only bought the ability to be answered with a guess, and the "host unknown" branch that served it was code for a case that cannot occur.

Docs

AGENTS.md claimed Codex CLI "has no hooks". True of an MCP-only wiring, false of a plugin install — corrected to distinguish the two.

Evidence

node scripts/run-tests-isolated.mjs   exit=0   Test Files 185 passed (185)
                                               Tests 2534 passed (2534)
npm run typecheck                     exit=0
node scripts/check-doc-claims.mjs     exit=0   (all 18 gates)
npm run build                         exit=0

End-to-end against the real paths on this machine:

~/.codex/plugins/cache/…   -> plugin-marketplace / codex
                              "Codex CLI plugin marketplace"
                              codex plugin marketplace upgrade pcircle-memesh
~/.claude/plugins/cache/…  -> plugin-marketplace / claude-code
                              "Claude Code plugin marketplace"
                              memesh upgrade-plugin

Break-test — removed '.codex' from PLUGIN_HOST_DIRS:

exit=1, 3 failed | 12 passed
  detects Codex CLI plugin-marketplace cache paths
  does NOT prescribe upgrade-plugin to a Codex-hosted install
  tells the two runtimes apart

Restored and re-verified byte-identical.

Reported from a real Codex session:

  ❌ memesh update does not support this install method (unknown).
     Update this installation from the tool or workflow that installed MeMesh.

on an install memesh fully supports. `memesh doctor` carried the same
non-answer into its summary.

Codex CLI adopted Claude Code's plugin manifest format and its cache layout,
one directory over — ~/.codex/plugins/cache/<marketplace>/<plugin>/<version>
against ~/.claude/plugins/cache/… . detectInstallChannel matched only
`.claude`, so a Codex-hosted copy fell past every branch to `unknown`.
Confirmed on disk: ~/.codex/plugins/cache/pcircle-memesh/memesh/4.7.1 exists
and mirrors the Claude Code layout exactly, and ~/.codex/config.toml carries
[hooks.state."memesh@pcircle-memesh:hooks/hooks.json:…"] entries for every
event — Codex is running memesh's own hook scripts, not just its MCP server.

Both runtimes now classify as `plugin-marketplace`. That is deliberate rather
than lazy: all three runtime consumers of the value ask the same host-agnostic
question — "is this wired by a plugin runtime" — and the answer is yes for
both. Splitting the enum would have required editing all three conditions and
still left the host-specific wording to fix.

What genuinely differs is the remediation, so that is now derived rather than
assumed. Verified against the real Codex CLI on this machine rather than
guessed — `codex plugin marketplace upgrade --help` takes an optional
MARKETPLACE_NAME, `codex plugin add` takes PLUGIN@MARKETPLACE:

  Codex        codex plugin marketplace upgrade pcircle-memesh
               codex plugin add memesh@pcircle-memesh
  Claude Code  memesh upgrade-plugin   (unchanged)

A Codex user is deliberately NOT sent to `memesh upgrade-plugin`. That script
reads $HOME/.claude/plugins/marketplaces/pcircle-memesh and patches
$HOME/.claude/plugins/installed_plugins.json; neither exists under ~/.codex
(checked — Codex keeps plugin state in config.toml and stages marketplaces
under ~/.codex/.tmp), so it aborts with "marketplace cache not found". A
confidently wrong instruction is worse than the generic one it replaces.

Three places that said "Claude Code" to everyone now name the runtime they
found: the doctor's hook-wiring row, its shell-CLI row, and the session-start
update banner. AGENTS.md's claim that Codex "has no hooks" is corrected — true
of an MCP-only wiring, false of a plugin install.

getInstallChannelSupport's second parameter is required, not optional. Every
caller already holds that path — it is what produced the channel — so an
optional one only bought the ability to be answered with a guess, and the
"host unknown" branch that served it was code for a case that cannot occur.

Evidence:
  node scripts/run-tests-isolated.mjs        exit=0
    Test Files  185 passed (185)
         Tests  2534 passed (2534)
  npm run typecheck                          exit=0
  node scripts/check-doc-claims.mjs          exit=0  (all 18 gates)
  npm run build                              exit=0
  end-to-end against the real paths on this machine:
    ~/.codex/plugins/cache/…  -> plugin-marketplace / codex /
       "Codex CLI plugin marketplace" / codex plugin marketplace upgrade …
    ~/.claude/plugins/cache/… -> plugin-marketplace / claude-code /
       "Claude Code plugin marketplace" / memesh upgrade-plugin
  break-test — removed '.codex' from PLUGIN_HOST_DIRS:
    exit=1, 3 failed | 12 passed
      detects Codex CLI plugin-marketplace cache paths
      does NOT prescribe upgrade-plugin to a Codex-hosted install
      tells the two runtimes apart
    restored and re-verified byte-identical
…annel had resolved

Caught in review before merge. `detectInstallChannel` matches against
`path.resolve(packageRoot)`; the three new consumers of `detectPluginHost`
pass the raw value. The old `isPluginMarketplacePath` was only ever called
with the already-normalised path, so the asymmetry arrived with this branch.

That made the comment I wrote a promise the code did not keep:

    // channel === 'plugin-marketplace' is derived from this same path, so
    // detectPluginHost cannot miss here.

It could. `runDoctor` takes `packageRoot` from its caller, and `path.resolve`
collapses `..`, makes relative absolute, and on Windows rewrites `/` to `\`.
`C:/Users/a/.codex/plugins/cache/…` would classify as `plugin-marketplace`
while an unresolved search for `\.codex\plugins\cache\` found nothing — and
that silent null hands a Codex user `memesh upgrade-plugin`, the one command
this branch exists to keep away from them.

The first test I wrote for this was a false guard: it built the input with
`path.join`, which collapses `..` itself, so the argument arrived normalised
and the assertion passed with or without the fix. The mutation run is what
exposed it — the mutant survived. The input is now a raw unnormalised literal.

  break-test, `path.resolve` removed:
    first attempt (path.join input)  exit=0, 16 passed  ← survived, bad test
    after fixing the input           exit=1, 1 failed | 15 passed
      plugin host detection > normalises the path the same way
      detectInstallChannel does
    restored and re-verified byte-identical

  node scripts/run-tests-isolated.mjs tests/install-channel.test.ts  exit=0
    Test Files 1 passed (1), Tests 16 passed (16)
  npm run typecheck  exit=0
…-host branch

The audit baseline is keyed by line number, so adding 7 lines above
`verifySkillsManifest` in this branch moved a triaged entry from
doctor.ts:1792 to :1799 and the gate reported it as a new untriaged hit
plus a stale entry to prune. That is a re-key, not a re-triage.

Verified before moving it, rather than assuming:

  working tree  1799: `  const entries = manifest.entries ?? [];`
  origin/main   1792: `  const entries = manifest.entries ?? [];`
  byte-identical — classification and reason unchanged.

The first attempt rewrote the file with json.dumps and silently unescaped
every — in the file: 69 lines changed for a 1-line move. Reverted and
done as a text substitution — the diff is 2 lines.

  npm run verify:release  exit=0
    ✓ C5: denominator=109 hits=77 new=0
    ✓ Every hit is triaged; every detector saw a non-empty candidate set.
…all-channel

# Conflicts:
#	dist/core/doctor.d.ts.map
#	dist/core/doctor.js.map
#	dist/skills-manifest.json
#	dist/transports/cli/cli.js.map
#	dist/transports/http/server.js.map
#	scripts/audit/baseline.json
@kevintseng
kevintseng merged commit 907dda8 into main Aug 24, 2026
13 checks passed
@kevintseng
kevintseng deleted the fix/codex-plugin-install-channel branch August 24, 2026 09:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant