Skill discovery recursively registers nested `SKILL.md` files inside symlinked skill directories
What version of Codex CLI is running?
codex-cli 0.130.0
What subscription do you have?
Not relevant.
Which model were you using?
Not relevant.
What platform is your computer?
linux
What terminal emulator and version are you using (if applicable)?
Not relevant.
What issue are you seeing?
Codex CLI appears to scan ~/.agents/skills recursively for every SKILL.md, while also following symlinked skill directories. This causes nested implementation files inside a symlinked skill package to be registered as independent user skills.
Example setup:
~/.agents/skills/workflow-toolkit
-> /tmp/codex-skill-repro/workflow-toolkit
The symlink target has its own root SKILL.md, but it also contains internal source trees:
workflow-toolkit/
SKILL.md
runtime-sources/
app-workflow/
skills/
apply-log/
SKILL.md
init-log/
SKILL.md
writing-workflow/
skills/
apply-log/
SKILL.md
init-log/
SKILL.md
Those nested runtime-sources/**/SKILL.md files are source material for the top-level skill package. They are not intended to be installed user skills.
Codex nevertheless registers them as available skills. This produces duplicate entries in the model-visible skill list, for example:
apply-log app workflow helper
apply-log writing workflow helper
init-log app workflow helper
init-log writing workflow helper
workflow-toolkit intended top-level skill
This makes skill routing ambiguous and noisy. It also makes symlinked skill packages behave differently from the user expectation that one direct child of ~/.agents/skills represents one installed skill.
What steps can reproduce the bug?
Create a minimal skill package with one root skill and nested internal skill-like source files:
mkdir -p /tmp/codex-skill-repro/workflow-toolkit/runtime-sources/app-workflow/skills/apply-log
mkdir -p /tmp/codex-skill-repro/workflow-toolkit/runtime-sources/writing-workflow/skills/apply-log
cat > /tmp/codex-skill-repro/workflow-toolkit/SKILL.md <<'EOF'
---
name: workflow-toolkit
description: Top-level workflow toolkit skill.
---
# Workflow Toolkit
EOF
cat > /tmp/codex-skill-repro/workflow-toolkit/runtime-sources/app-workflow/skills/apply-log/SKILL.md <<'EOF'
---
name: apply-log
description: Internal app workflow source file, not an installed user skill.
---
# Internal App Workflow Source
EOF
cat > /tmp/codex-skill-repro/workflow-toolkit/runtime-sources/writing-workflow/skills/apply-log/SKILL.md <<'EOF'
---
name: apply-log
description: Internal writing workflow source file, not an installed user skill.
---
# Internal Writing Workflow Source
EOF
Symlink that package into ~/.agents/skills:
ln -s /tmp/codex-skill-repro/workflow-toolkit ~/.agents/skills/workflow-toolkit
Start Codex and inspect available skills, or call the app-server skills/list method with forceReload: true.
The filesystem difference is easy to see from the shell. A shallow scan sees no nested files:
find ~/.agents/skills -maxdepth 2 -name SKILL.md -print
But following symlinks recursively reveals files that Codex also registers:
find -L ~/.agents/skills -name SKILL.md -print | rg 'workflow-toolkit|apply-log'
Observed output:
~/.agents/skills/workflow-toolkit/SKILL.md
~/.agents/skills/workflow-toolkit/runtime-sources/app-workflow/skills/apply-log/SKILL.md
~/.agents/skills/workflow-toolkit/runtime-sources/writing-workflow/skills/apply-log/SKILL.md
The app-server skills/list method reports the same nested files as available user skills:
{
"method": "skills/list",
"params": {
"cwds": ["/tmp/codex-skill-repro"],
"forceReload": true
}
}
Representative result:
name: workflow-toolkit
path: /tmp/codex-skill-repro/workflow-toolkit/SKILL.md
enabled: true
name: apply-log
path: /tmp/codex-skill-repro/workflow-toolkit/runtime-sources/app-workflow/skills/apply-log/SKILL.md
enabled: true
name: apply-log
path: /tmp/codex-skill-repro/workflow-toolkit/runtime-sources/writing-workflow/skills/apply-log/SKILL.md
enabled: true
What is the expected behavior?
Codex should treat each direct child of ~/.agents/skills as one installed skill entry:
~/.agents/skills/<skill-name>/SKILL.md
If <skill-name> is a symlink to a directory, Codex should resolve the symlink to read the target's root SKILL.md, but it should not continue recursively discovering additional nested SKILL.md files inside that target.
In other words, the symlink target should be used as the skill directory, not as a new recursive discovery root.
The preferred fix is to prohibit deeper recursive discovery after resolving a direct installed skill directory. Installing one skill folder should install one skill.
Alternative acceptable fixes:
- Add a scan-depth option, defaulting to one skill directory level.
- Add a skill discovery ignore option, such as
skills.discovery.exclude = ["**/runtime-sources/**"]. - If recursive scanning is intentional, document it explicitly in the skills docs, including the fact that any nested
SKILL.mdunder an installed skill directory may be registered as a separate skill.
My recommendation is the first option: do not scan deeper. That keeps symlinked skill directories consistent with normal installed skill directories and avoids accidental registration of internal source material.
Additional information
Current workarounds:
- Disable specific false-positive skills by path in
config.toml:
[[skills.config]]
path = "/tmp/codex-skill-repro/workflow-toolkit/runtime-sources/app-workflow/skills/apply-log/SKILL.md"
enabled = false
This works after restarting Codex, but it does not prevent the scan and does not scale well when many nested SKILL.md files exist.
- Disable by one-off CLI config override:
codex -c 'skills.config=[{path="/tmp/codex-skill-repro/workflow-toolkit/runtime-sources/app-workflow/skills/apply-log/SKILL.md", enabled=false}]'
This causes skills/list to report that entry with:
enabled: false
- Disable individual skills through
/skillsin the TUI. This is only practical for a small number of false positives.
- Avoid symlinking rich development packages into
~/.agents/skills. Instead, use a shallow wrapper directory containing only the intended rootSKILL.md, or install a compiled flat skill snapshot that has only direct skill directories.
The last workaround avoids the issue, but it requires users to restructure otherwise valid skill packages. The better default behavior would be for Codex to stop discovery at the installed skill directory boundary.
5 Comments
I can reproduce the same class of issue with a dependency-installed
SKILL.md, not only nested source/workflow skill files.Example shape:
Codex may expose the nested dependency skill as an available independent skill, for example:
That file belongs to a third-party dependency installed under another skill. The user intended to install only the top-level skill, not to expose internal files from dependencies as user-visible Codex skills.
This makes the issue more concerning than duplicate/noisy skill discovery: third-party package contents under directories such as
node_modulescan accidentally become model-visible instructions/capabilities.At minimum, skill discovery should ignore dependency/build/VCS directories like
node_modules,vendor,.git,dist, andbuild. Preferably, discovery should stop at the installed skill directory boundary and only register the rootSKILL.mdfor each installed skill.Additional reproduction from Codex Desktop on Windows.
This is not limited to a package's internal source tree or to cc-switch being actively running. I use the same router-style skill bundle from both Claude and Codex. The shared bundle has one top-level router SKILL.md and multiple child SKILL.md files. Codex exposes both the router and the child skills in the initial model-visible Available Skills catalog.
In my setup, the Codex entry under
C:\Users\Administrator\.codex\skillsis a symlink into the shared skill tree under.cc-switch. However, the core issue remains even when the external tool is not actively running: Codex recursively discovers nested SKILL.md files below an installed skill directory. The symlink only makes the traversal visible; it is not the root cause.Expected behavior:
Actual behavior:
The fix should apply to the general installed-skill boundary, not only symlinked packages:
<root>/<skill>/SKILL.mdas the sole startup entry.Requiring users to rename child SKILL.md files, flatten packages, or maintain separate copies is not a satisfactory solution for users sharing one skill library across Claude and Codex, because it breaks interoperability and creates maintenance drift.
Windows variant of this, with a more severe failure mode.
Environment:
A junction existed at:
C:\Users\<user>\.codex\skills\develop-web-game\node_modules
-> C:\Users\<user>\Documents\<project>\node_modules
Because skill discovery follows directory junctions recursively,
skills/listwalked into the project dependency tree and eventually logged:skills scan reached its traversal limit
(root: file:///C:/Users/<user>/.codex/skills)
The Desktop skill picker then appeared incomplete or empty. This was not just duplicate nested skills.
After moving only that junction outside the skill scan root and calling
skills/listwithforceReload: true, the response returnederrors: []and all user skills reappeared, including Better Writing and the design skills.This looks like the same underlying discovery-boundary problem described here. Following arbitrary reparse points beneath an installed skill can make one accidental
node_moduleslink poison the entire catalog. The scanner should stop at the installed skill boundary, skip dependency/cache directories, or isolate traversal-limit errors per skill instead of failing the broader catalog.+1, this recursive skill discovery issue makes authoring routing-based nested skills hard, we cannot just split our skills into priviate/public and hide internal skills in nested dirs, needs fix or configurable.
Hit me as well - having skills inside of a /references folder activates recursive skill loading and I have to work around it.