Skill discovery recursively registers nested `SKILL.md` files inside symlinked skill directories

Open 💬 5 comments Opened May 12, 2026 by Fmajor

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.md under 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:

  1. 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.

  1. 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
  1. Disable individual skills through /skills in the TUI. This is only practical for a small number of false positives.
  1. Avoid symlinking rich development packages into ~/.agents/skills. Instead, use a shallow wrapper directory containing only the intended root SKILL.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.

View original on GitHub ↗

5 Comments

andreamk · 3 months ago

I can reproduce the same class of issue with a dependency-installed SKILL.md, not only nested source/workflow skill files.

Example shape:

<installed-skill>/
  SKILL.md
  node_modules/
    playwright-core/
      lib/
        tools/
          trace/
            SKILL.md

Codex may expose the nested dependency skill as an available independent skill, for example:

<installed-skill>/node_modules/playwright-core/lib/tools/trace/SKILL.md

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_modules can accidentally become model-visible instructions/capabilities.

At minimum, skill discovery should ignore dependency/build/VCS directories like node_modules, vendor, .git, dist, and build. Preferably, discovery should stop at the installed skill directory boundary and only register the root SKILL.md for each installed skill.

assam105 · 1 month ago

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\skills is 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:

  • Register one skill for each direct child of the configured skill root.
  • Treat a router package as one installed skill.
  • Read child SKILL.md files only after the router is selected and explicitly routes to them.
  • Do not put unselected child skills into the initial model-visible catalog.

Actual behavior:

  • The router and all nested child skills are registered together at startup.
  • This defeats router-based progressive disclosure.
  • Every new session receives the names, descriptions, and paths of unselected child skills.
  • Large shared bundles create unnecessary context/token overhead and may introduce ambiguous duplicate triggers.

The fix should apply to the general installed-skill boundary, not only symlinked packages:

  1. Discover only direct children of each configured skill root.
  2. Treat <root>/<skill>/SKILL.md as the sole startup entry.
  3. Let a triggered router explicitly read nested child SKILL.md files.
  4. Optionally provide a documented discovery-depth or exclude configuration.

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.

Kerbz-dev · 1 month ago

Windows variant of this, with a more severe failure mode.

Environment:

  • Codex Desktop 26.715.4045.0
  • codex-cli/app-server 0.144.0
  • Windows 11 x64

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/list walked 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/list with forceReload: true, the response returned errors: [] 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_modules link 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.

igamenovoer · 1 month ago

+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.

kylesnowschwartz · 1 month ago

Hit me as well - having skills inside of a /references folder activates recursive skill loading and I have to work around it.