Marketplace local plugin path "./" cannot reference the repository root
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": "../.."
},
...
}
]
}
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": "./",
...
}
]
}
{
"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
9 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
Thanks for documenting this. Unfortunately, neither workaround works for me. I still have to create a dedicated plugin folder based on Codex docs. Is there a reason Codex doesn’t follow the same convention as Claude?
I also encountered this problem.
Fix PR opened: https://github.com/openai/codex/pull/28771
Updated fix PR: https://github.com/openai/codex/pull/28771
Final behavior accepts repo-root local plugin paths
.and./, keeps""invalid, and still rejects non-normal/traversal paths like././,./plugins/../, and../....Merged: https://github.com/openai/codex/pull/28771
The root-local marketplace fix is now landed.
_Sent from my Codex_
Per my Codex slop above 😅 this should be addressed in the next cut!
Did the fix work for you @mmizutani ? I still face an issue - it's adding the marketplace but it does not appear inside Codex session, and the plugin also is not being listed. 🤔
EDIT: I was testing on v0.141.0, and it does not yet include the fix! Sorry about that
@sophio-japharidze-sonarsource
The fix #28771 has been released in v0.142.0.