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:
/plugins and codex plugin marketplace list show it. OK.
codex plugin add installs from it. OK.
- 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.
What version of Codex CLI is running?
codex-cli 0.132.0What 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 inmarketplace_edit.rs), whichcodex plugin marketplace listreads back. So a local marketplace at a path outside$HOMEis a documented, supported setup.The listing path goes through
list_marketplaces_for_configintodiscover_marketplaces_for_configandmarketplace_roots, which expands[marketplaces.*]blocks from~/.codex/config.tomlviainstalled_marketplace_roots_from_layer_stack. The cache refresh path,refresh_non_curated_plugin_cache_with_mode, instead calls a barelist_marketplaceswith only the passedadditional_roots, so it inspectshome_dir()plus those roots and never consults the config layer stack.Consequences for a local marketplace registered solely via
[marketplaces.<name>]inconfig.toml(source_type = "local") pointing at a directory outside$HOME:/pluginsandcodex plugin marketplace listshow it. OK.codex plugin addinstalls from it. OK.plugin.jsonversion, 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'sadditional_roots. A local marketplace placed under, say,%LOCALAPPDATA%\<vendor>\marketplaces\currentwith 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 viahome_dir(), one via the client'sadditional_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 undercodex_home.We hit this in a managed-marketplace rollout and worked around it by junctioning the marketplace into
$HOMEon every convergence so the refresh path'shome_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.Same source plugin, same
[marketplaces.*]block, same trigger. The only difference between step 7 and step 10 is whether the marketplace is reachable viahome_dir(). That isolates the bug to the missing config-layer-stack consultation inrefresh_non_curated_plugin_cache_with_mode.What is the expected behavior?
refresh_non_curated_plugin_cache_with_modeshould derive marketplace roots the same waydiscover_marketplaces_for_configdoes: walkinstalled_marketplace_roots_from_layer_stackover the config layer stack, then combine withadditional_roots. Concretely, replace the barelist_marketplaces(additional_roots)call with the same root assembly used bymarketplace_roots. Any marketplace visible to/pluginsshould also be visible to refresh.Additional information
None.