Description
The resolve_plugin_source_path() function in codex-rs/core/src/plugins/marketplace.rs rejects every form of local plugin source path that resolves to the repository root directory (the marketplace root itself). This makes it impossible to register a plugin whose .codex-plugin/plugin.json and skills live at the top level of the repository.
This may be by design, but supporting "./" as a reference to the repo root is a natural use case — particularly for monorepos that keep a single top-level .codex-plugin/plugin.json alongside an existing skills/ directory.
Steps to reproduce
Directory layout:
<repo>/
.agents/plugins/marketplace.json
.codex-plugin/plugin.json
skills/
marketplace.json:
{
"name": "my-plugins",
"interface": { "displayName": "My plugins" },
"plugins": [
{
"name": "repo-root-plugin",
"source": { "source": "local", "path": "./" },
"policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" },
"category": "productivity"
}
]
}
Result: The marketplace fails to load.
Every path variation that should resolve to the repo root is rejected:
| `path` value |
Error |
| `"./"` |
`local plugin source path must not be empty` |
| `"."` |
`local plugin source path must start with './'` |
| `"././"` |
`local plugin source path must stay within the marketplace root` |
| `"./plugins/../"` |
`local plugin source path must stay within the marketplace root` |
Log output in `~/.codex/log/codex-tui.log`:
WARN codex_core::plugins::marketplace: skipping marketplace that failed to load
path=<repo>/.agents/plugins/marketplace.json
error=invalid marketplace file \`<repo>/.agents/plugins/marketplace.json\`: local plugin source path must not be empty
Root cause
The validation in `resolve_plugin_source_path()` (marketplace.rs L338-374) applies three sequential checks after stripping the `"./"` prefix:
- Check 1 (L344): `path.strip_prefix("./")` — input must start with `"./"`. Rejects `"."`.
- Check 2 (L350): The remainder after stripping must not be empty. Rejects `"./"` because `"./".strip_prefix("./")` yields `""`.
- Check 3 (L358-360): All `Path::components()` must be `Component::Normal`. Rejects `"././"` (contains `CurDir`) and `"./plugins/../"` (contains `ParentDir`).
The function always requires at least one `Normal` path component, so the resolved path is always a strict subdirectory of the marketplace root. No valid `path` value can reference the root itself.
Proposed change
Treat an empty remainder (after stripping `"./"`) as a valid path that resolves to the marketplace root:
RawMarketplaceManifestPluginSource::Local { path } => {
let Some(path) = path.strip_prefix("./") else {
return Err(/* "must start with \`./\`" */);
};
// "./" means the marketplace root itself.
if path.is_empty() {
return Ok(marketplace_root_dir(marketplace_path)?);
}
let relative_source_path = Path::new(path);
if relative_source_path
.components()
.any(|component| !matches!(component, Component::Normal(_)))
{
return Err(/* "must stay within the marketplace root" */);
}
Ok(marketplace_root_dir(marketplace_path)?.join(relative_source_path))
}
Current workarounds
A. Place the plugin in a subdirectory and symlink to the repo-root skills:
.codex/plugins/my-plugin/
.codex-plugin/plugin.json
skills -> ../../../skills # symlink
{ "source": { "source": "local", "path": "./.codex/plugins/my-plugin" } }
This works but adds indirection and fragile symlinks that may break on some platforms.
B. Set "path": "../.." in .agents/plugins/marketplace.json
{
"name": "ecc",
"interface": {
"displayName": "Everything Claude Code"
},
"plugins": [
{
"name": "ecc",
"source": {
"source": "local",
"path": "../.."
},
...
}
]
}
https://github.com/affaan-m/everything-claude-code/blob/098b773c115364259abeb538cbc7a68fffabbf2c/.agents/plugins/marketplace.json#L1-L11
This works but introduces undesirable reference to the ancestor directories of the repository root.
Environment
- Codex CLI: main branch at `80ebc80be`
- OS: macOS (Darwin 25.4.0)
Comparison with Claude Code Marketplace/Plugins
Claude Code marketplace configs do permit a local plugin source path ./, allowing bundling the repo root /skills/ in a repo root Claude Code plugin:
<repo>/
.claude-plugin/
marketplace.json
plugin.json
skills/
Example:
{
"name": "ecc",
...
"plugins": [
{
"name": "ecc",
"source": "./",
...
}
]
}
https://github.com/affaan-m/everything-claude-code/blob/098b773c115364259abeb538cbc7a68fffabbf2c/.claude-plugin/marketplace.json#L15
{
"name": "ecc",
...
"skills": ["./skills/"],
...
}
https://github.com/affaan-m/everything-claude-code/blob/main/.claude-plugin/plugin.json
Proposed Changes
A possible change to relax the restrictions with respect to the local plugin path in the marketplace config:
https://github.com/openai/codex/compare/main...mmizutani:codex:fix/marketplace-root-plugin-path?expand=1
Description
The
resolve_plugin_source_path()function incodex-rs/core/src/plugins/marketplace.rsrejects every form of local plugin source path that resolves to the repository root directory (the marketplace root itself). This makes it impossible to register a plugin whose.codex-plugin/plugin.jsonand skills live at the top level of the repository.This may be by design, but supporting
"./"as a reference to the repo root is a natural use case — particularly for monorepos that keep a single top-level.codex-plugin/plugin.jsonalongside an existingskills/directory.Steps to reproduce
Directory layout:
marketplace.json:
{ "name": "my-plugins", "interface": { "displayName": "My plugins" }, "plugins": [ { "name": "repo-root-plugin", "source": { "source": "local", "path": "./" }, "policy": { "installation": "AVAILABLE", "authentication": "ON_INSTALL" }, "category": "productivity" } ] }Result: The marketplace fails to load.
Every path variation that should resolve to the repo root is rejected:
Log output in `~/.codex/log/codex-tui.log`:
Root cause
The validation in `resolve_plugin_source_path()` (marketplace.rs L338-374) applies three sequential checks after stripping the `"./"` prefix:
The function always requires at least one `Normal` path component, so the resolved path is always a strict subdirectory of the marketplace root. No valid `path` value can reference the root itself.
Proposed change
Treat an empty remainder (after stripping `"./"`) as a valid path that resolves to the marketplace root:
Current workarounds
A. Place the plugin in a subdirectory and symlink to the repo-root skills:
{ "source": { "source": "local", "path": "./.codex/plugins/my-plugin" } }This works but adds indirection and fragile symlinks that may break on some platforms.
B. Set
"path": "../.."in.agents/plugins/marketplace.json{ "name": "ecc", "interface": { "displayName": "Everything Claude Code" }, "plugins": [ { "name": "ecc", "source": { "source": "local", "path": "../.." }, ... } ] }https://github.com/affaan-m/everything-claude-code/blob/098b773c115364259abeb538cbc7a68fffabbf2c/.agents/plugins/marketplace.json#L1-L11
This works but introduces undesirable reference to the ancestor directories of the repository root.
Environment
Comparison with Claude Code Marketplace/Plugins
Claude Code marketplace configs do permit a local plugin source path
./, allowing bundling the repo root/skills/in a repo root Claude Code plugin:Example:
{ "name": "ecc", ... "plugins": [ { "name": "ecc", "source": "./", ... } ] }https://github.com/affaan-m/everything-claude-code/blob/098b773c115364259abeb538cbc7a68fffabbf2c/.claude-plugin/marketplace.json#L15
{ "name": "ecc", ... "skills": ["./skills/"], ... }https://github.com/affaan-m/everything-claude-code/blob/main/.claude-plugin/plugin.json
Proposed Changes
A possible change to relax the restrictions with respect to the local plugin path in the marketplace config:
https://github.com/openai/codex/compare/main...mmizutani:codex:fix/marketplace-root-plugin-path?expand=1