Skip to content

Marketplace local plugin path "./" cannot reference the repository root #17066

@mmizutani

Description

@mmizutani

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:

  1. Check 1 (L344): `path.strip_prefix("./")` — input must start with `"./"`. Rejects `"."`.
  2. Check 2 (L350): The remainder after stripping must not be empty. Rejects `"./"` because `"./".strip_prefix("./")` yields `""`.
  3. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingskillsIssues related to skills

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions