codex - 💡(How to fix) Fix Plugin cache refresh ignores marketplaces defined in config.toml

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…

Root Cause

B. The refresh path effectively only honors local marketplaces reachable via home_dir() or the embedding client's additional_roots. A local marketplace placed under, say, %LOCALAPPDATA%\<vendor>\marketplaces\current with a fully valid [marketplaces.<name>] block is invisible to refresh even though the listing path makes it look configured. So you can effectively have at most two local marketplaces (one resolvable via home_dir(), one via the client's additional_roots); any config-only local marketplace at a third location is dead weight. Git-sourced marketplaces are unaffected because they have a separate refresh path (marketplace_upgrade) that operates against their on-disk clone under codex_home.

Fix Action

Fix / Workaround

A. Version bumps shipped via a config-defined local marketplace are silently a no-op. There is no in-band way for an operator to deliver an update; the only workaround is to wipe ~/.codex/plugins/cache/<mp>/ externally on each release.

We hit this in a managed-marketplace rollout and worked around it by junctioning the marketplace into $HOME on every convergence so the refresh path's home_dir() lookup catches it. The workaround should not be necessary, and the divergence between the two paths is hard to notice without reading both.

6. Trigger refresh by launching the TUI briefly. The TUI's startup

fetch_plugins_list (tui/src/app/event_dispatch.rs) sends a PluginList

JSON-RPC request to the app-server, whose handler

(app-server/src/request_processors/plugins.rs:276) calls

maybe_start_plugin_list_background_tasks_for_config, which is the only

code path that invokes refresh_non_curated_plugin_cache. So

codex --version and codex plugin add do NOT exercise refresh; the

TUI does. Launch and immediately exit:

codex # let it come up, then Ctrl+C / :quit

Code Example

{
  "codexVersion": "0.132.0",
  "checks": {
    "config.load": { "status": "ok", "details": { "CODEX_HOME": "C:\\Users\\<user>\\.codex" } },
    "runtime.provenance": { "details": { "install method": "npm", "platform": "windows-x86_64", "version": "0.132.0" } }
  }
}

---

# 0. Snapshot config so we can restore it.
$repro = "$env:TEMP\codex-repro"
New-Item -ItemType Directory -Path $repro -Force | Out-Null
Copy-Item "$env:USERPROFILE\.codex\config.toml" "$repro\config.toml.snapshot"

# 1. Minimal local marketplace OUTSIDE $HOME.
$mp     = "$repro\mp"
$plugin = "$mp\repro-test-plugin"
New-Item -ItemType Directory -Path "$mp\.claude-plugin","$plugin\.claude-plugin" -Force | Out-Null

@'
{
  "name": "repro-mp",
  "owner": { "name": "repro" },
  "plugins": [ { "name": "repro-test-plugin", "source": "./repro-test-plugin" } ]
}
'@ | Set-Content -Encoding UTF8 "$mp\.claude-plugin\marketplace.json"

@'
{ "name": "repro-test-plugin", "version": "1.0.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

# 2. Register via config.toml only.
Add-Content "$env:USERPROFILE\.codex\config.toml" @"

[marketplaces.repro-mp]
source_type = "local"
source = "$($mp -replace '\\','\\')"
"@

# 3. Listing path sees it.
codex plugin marketplace list
#   repro-mp   C:\Users\...\codex-repro\mp

# 4. Install (also uses the listing path).
codex plugin add 'repro-test-plugin@repro-mp'
#   Installed plugin root: ...\repro-mp\repro-test-plugin\1.0.0

# 5. Bump source plugin to 1.1.0.
@'
{ "name": "repro-test-plugin", "version": "1.1.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

# 6. Trigger refresh by launching the TUI briefly. The TUI's startup
#    `fetch_plugins_list` (tui/src/app/event_dispatch.rs) sends a `PluginList`
#    JSON-RPC request to the app-server, whose handler
#    (app-server/src/request_processors/plugins.rs:276) calls
#    `maybe_start_plugin_list_background_tasks_for_config`, which is the only
#    code path that invokes `refresh_non_curated_plugin_cache`. So
#    `codex --version` and `codex plugin add` do NOT exercise refresh; the
#    TUI does. Launch and immediately exit:
codex      # let it come up, then Ctrl+C / :quit

# 7. Cache still at 1.0.0. Expected 1.1.0.
Get-ChildItem "$env:USERPROFILE\.codex\plugins\cache\repro-mp\repro-test-plugin" -Directory | ForEach-Object Name
#   1.0.0

# 8. Counter-check: junction the marketplace into $HOME so it's reachable
#    via home_dir() as well. Source, config, and trigger are otherwise
#    identical to steps 1-7.
cmd /c "mklink /J `"$env:USERPROFILE\.claude-plugin`" `"$mp\.claude-plugin`""
cmd /c "mklink /J `"$env:USERPROFILE\repro-test-plugin`" `"$mp\repro-test-plugin`""

# 9. Launch and exit the TUI again. Refresh runs, this time with the
#    marketplace reachable via home_dir().
codex      # let it come up, then Ctrl+C / :quit

# 10. Cache now at 1.1.0.
Get-ChildItem "$env:USERPROFILE\.codex\plugins\cache\repro-mp\repro-test-plugin" -Directory | ForEach-Object Name
#   1.1.0

# Cleanup.
[System.IO.Directory]::Delete("$env:USERPROFILE\.claude-plugin", $false)
[System.IO.Directory]::Delete("$env:USERPROFILE\repro-test-plugin", $false)
codex plugin remove 'repro-test-plugin@repro-mp'
Copy-Item "$repro\config.toml.snapshot" "$env:USERPROFILE\.codex\config.toml" -Force
Remove-Item "$env:USERPROFILE\.codex\plugins\cache\repro-mp" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $repro -Recurse -Force
RAW_BUFFERClick to expand / collapse

What version of Codex CLI is running?

codex-cli 0.132.0

What subscription do you have?

ChatGPT Business

Which model were you using?

gpt-5.4 (not model-dependent)

What platform is your computer?

Microsoft Windows NT 10.0.26100.0 x64 (Windows 11 Enterprise 24H2). The bug is not platform-specific; the reproduction below uses Windows paths.

What terminal emulator and version are you using (if applicable)?

Windows Terminal (PowerShell 7).

Codex doctor report

{
  "codexVersion": "0.132.0",
  "checks": {
    "config.load": { "status": "ok", "details": { "CODEX_HOME": "C:\\Users\\<user>\\.codex" } },
    "runtime.provenance": { "details": { "install method": "npm", "platform": "windows-x86_64", "version": "0.132.0" } }
  }
}

What issue are you seeing?

The listing and refresh code paths disagree about which marketplaces exist.

Registering a local marketplace at an arbitrary on-disk path is documented at plugins/build via codex plugin marketplace add <local-path>. That command persists the registration as a [marketplaces.<name>] block in ~/.codex/config.toml (implemented in marketplace_edit.rs), which codex plugin marketplace list reads back. So a local marketplace at a path outside $HOME is a documented, supported setup.

The listing path goes through list_marketplaces_for_config into discover_marketplaces_for_config and marketplace_roots, which expands [marketplaces.*] blocks from ~/.codex/config.toml via installed_marketplace_roots_from_layer_stack. The cache refresh path, refresh_non_curated_plugin_cache_with_mode, instead calls a bare list_marketplaces with only the passed additional_roots, so it inspects home_dir() plus those roots and never consults the config layer stack.

Consequences for a local marketplace registered solely via [marketplaces.<name>] in config.toml (source_type = "local") pointing at a directory outside $HOME:

  1. /plugins and codex plugin marketplace list show it. OK.
  2. codex plugin add installs from it. OK.
  3. When the plugin author bumps plugin.json version, subsequent codex launches do not pick it up. The cache stays pinned to the originally installed version.

Two consequent bad outcomes:

A. Version bumps shipped via a config-defined local marketplace are silently a no-op. There is no in-band way for an operator to deliver an update; the only workaround is to wipe ~/.codex/plugins/cache/<mp>/ externally on each release.

B. The refresh path effectively only honors local marketplaces reachable via home_dir() or the embedding client's additional_roots. A local marketplace placed under, say, %LOCALAPPDATA%\<vendor>\marketplaces\current with a fully valid [marketplaces.<name>] block is invisible to refresh even though the listing path makes it look configured. So you can effectively have at most two local marketplaces (one resolvable via home_dir(), one via the client's additional_roots); any config-only local marketplace at a third location is dead weight. Git-sourced marketplaces are unaffected because they have a separate refresh path (marketplace_upgrade) that operates against their on-disk clone under codex_home.

We hit this in a managed-marketplace rollout and worked around it by junctioning the marketplace into $HOME on every convergence so the refresh path's home_dir() lookup catches it. The workaround should not be necessary, and the divergence between the two paths is hard to notice without reading both.

What steps can reproduce the bug?

Reproduced on codex-cli 0.132.0, Windows 11 24H2.

# 0. Snapshot config so we can restore it.
$repro = "$env:TEMP\codex-repro"
New-Item -ItemType Directory -Path $repro -Force | Out-Null
Copy-Item "$env:USERPROFILE\.codex\config.toml" "$repro\config.toml.snapshot"

# 1. Minimal local marketplace OUTSIDE $HOME.
$mp     = "$repro\mp"
$plugin = "$mp\repro-test-plugin"
New-Item -ItemType Directory -Path "$mp\.claude-plugin","$plugin\.claude-plugin" -Force | Out-Null

@'
{
  "name": "repro-mp",
  "owner": { "name": "repro" },
  "plugins": [ { "name": "repro-test-plugin", "source": "./repro-test-plugin" } ]
}
'@ | Set-Content -Encoding UTF8 "$mp\.claude-plugin\marketplace.json"

@'
{ "name": "repro-test-plugin", "version": "1.0.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

# 2. Register via config.toml only.
Add-Content "$env:USERPROFILE\.codex\config.toml" @"

[marketplaces.repro-mp]
source_type = "local"
source = "$($mp -replace '\\','\\')"
"@

# 3. Listing path sees it.
codex plugin marketplace list
#   repro-mp   C:\Users\...\codex-repro\mp

# 4. Install (also uses the listing path).
codex plugin add 'repro-test-plugin@repro-mp'
#   Installed plugin root: ...\repro-mp\repro-test-plugin\1.0.0

# 5. Bump source plugin to 1.1.0.
@'
{ "name": "repro-test-plugin", "version": "1.1.0" }
'@ | Set-Content -Encoding UTF8 "$plugin\.claude-plugin\plugin.json"

# 6. Trigger refresh by launching the TUI briefly. The TUI's startup
#    `fetch_plugins_list` (tui/src/app/event_dispatch.rs) sends a `PluginList`
#    JSON-RPC request to the app-server, whose handler
#    (app-server/src/request_processors/plugins.rs:276) calls
#    `maybe_start_plugin_list_background_tasks_for_config`, which is the only
#    code path that invokes `refresh_non_curated_plugin_cache`. So
#    `codex --version` and `codex plugin add` do NOT exercise refresh; the
#    TUI does. Launch and immediately exit:
codex      # let it come up, then Ctrl+C / :quit

# 7. Cache still at 1.0.0. Expected 1.1.0.
Get-ChildItem "$env:USERPROFILE\.codex\plugins\cache\repro-mp\repro-test-plugin" -Directory | ForEach-Object Name
#   1.0.0

# 8. Counter-check: junction the marketplace into $HOME so it's reachable
#    via home_dir() as well. Source, config, and trigger are otherwise
#    identical to steps 1-7.
cmd /c "mklink /J `"$env:USERPROFILE\.claude-plugin`" `"$mp\.claude-plugin`""
cmd /c "mklink /J `"$env:USERPROFILE\repro-test-plugin`" `"$mp\repro-test-plugin`""

# 9. Launch and exit the TUI again. Refresh runs, this time with the
#    marketplace reachable via home_dir().
codex      # let it come up, then Ctrl+C / :quit

# 10. Cache now at 1.1.0.
Get-ChildItem "$env:USERPROFILE\.codex\plugins\cache\repro-mp\repro-test-plugin" -Directory | ForEach-Object Name
#   1.1.0

# Cleanup.
[System.IO.Directory]::Delete("$env:USERPROFILE\.claude-plugin", $false)
[System.IO.Directory]::Delete("$env:USERPROFILE\repro-test-plugin", $false)
codex plugin remove 'repro-test-plugin@repro-mp'
Copy-Item "$repro\config.toml.snapshot" "$env:USERPROFILE\.codex\config.toml" -Force
Remove-Item "$env:USERPROFILE\.codex\plugins\cache\repro-mp" -Recurse -Force -ErrorAction SilentlyContinue
Remove-Item $repro -Recurse -Force

Same source plugin, same [marketplaces.*] block, same trigger. The only difference between step 7 and step 10 is whether the marketplace is reachable via home_dir(). That isolates the bug to the missing config-layer-stack consultation in refresh_non_curated_plugin_cache_with_mode.

What is the expected behavior?

refresh_non_curated_plugin_cache_with_mode should derive marketplace roots the same way discover_marketplaces_for_config does: walk installed_marketplace_roots_from_layer_stack over the config layer stack, then combine with additional_roots. Concretely, replace the bare list_marketplaces(additional_roots) call with the same root assembly used by marketplace_roots. Any marketplace visible to /plugins should also be visible to refresh.

Additional information

None.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING