Symlinked standalone skills inherit plugin namespace from target ancestors
What version of Codex is running?
0.147.0
What platform is your computer?
Darwin 25.5.0 arm64 arm
What issue are you seeing?
A standalone skill installed as a directory symlink under the user skills directory inherits a plugin namespace from an ancestor of the symlink target.
The skill is not installed as part of a Codex plugin, and codex plugin list does not list that plugin. Nevertheless, Codex displays the skill as:
foreign-plugin:demo
instead of:
demo
This affects declarative package managers such as Nix/Home Manager, which normally install files by linking them from an immutable store.
Steps to reproduce
Create this source tree:
source/
├── .claude-plugin/
│ └── plugin.json
└── skills/
└── demo/
└── SKILL.md
.claude-plugin/plugin.json:
{
"name": "foreign-plugin"
}
skills/demo/SKILL.md:
---
name: demo
description: Demonstrate symlinked skill discovery.
---
Then symlink only the individual skill into the Codex user skills directory:
$CODEX_HOME/skills/demo -> source/skills/demo
Restart Codex and inspect the skill selector or run:
codex debug prompt-input "diagnostic"
What did you expect to happen?
Following a symlink for file access should not make an individually installed standalone skill inherit plugin ownership from directories above the symlink target.
Codex should use:
- the canonical path for reading and deduplication; and
- the logical discovery path, or explicit plugin installation metadata, for namespace ownership.
The skill should therefore remain demo.
Additional information
Codex canonicalizes the symlinked SKILL.md, treats the canonical target's parent as a namespace root, walks upward outside the configured skills directory, and reads source/.claude-plugin/plugin.json.
Relevant implementation:
codex-rs/ext/skills/src/loader/host.rsload_skills_under_rootcanonicalizes discovered skill paths.- When canonicalization changes the path, it adds the target parent to
namespace_roots. codex-rs/ext/skills/src/loader/namespace.rsSkillNamespaceResolverwalks upward from those roots and applies the nearest manifest name.
The official documentation says Codex supports symlinked skill folders and follows their targets, but it does not document inheriting plugin metadata from ancestors outside the configured skills root:
1 Comment
Verified against current
mainat343074d4207d572809bd8cea15f4be1d09d98e0b: the same path split is still present.load_skills_under_rootcanonicalizes the discovered skill for identity, then adds the canonical target's parent tonamespace_rootswhenever it differs from the logical discovery path.SkillNamespaceResolversubsequently walks upward from that canonical namespace root and can therefore pick up an unrelated ancestor manifest outside the configured standalone-skills root.A focused regression should keep canonical identity/deduplication intact while distinguishing ownership: an individually symlinked user skill must remain plain even when the canonical target has a plugin-manifest ancestor, while a skill discovered through an actual plugin root must retain that plugin's namespace. This makes the boundary the discovery/ownership metadata passed into namespace resolution, rather than whether the target is readable through its canonical path.