Symlinked SKILL.md files are not discovered in skills loader (only symlinked directories are followed)

Resolved 💬 6 comments Opened Mar 25, 2026 by miking38 Closed Mar 25, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

codex-cli 0.116.0

What subscription do you have?

Enterprise

Which model were you using?

gpt-5.3-codex

What platform is your computer?

Darwin 25.3.0 arm64 arm

What terminal emulator and version are you using (if applicable)?

iTerm2 3.6.6

What issue are you seeing?

Symlinked SKILL.md files are not discovered by the skills loader, so those skills do not appear in $ command suggestions.

From codex-rs/core/src/skills/loader.rs, the likely cause is:

  • symlink entries are handled in a separate branch
  • that branch only follows symlinks that point to directories
  • symlink-to-file entries are skipped
  • the parse path only runs when file_type.is_file() && file_name == "SKILL.md"

So a SKILL.md symlink never reaches parsing.

What steps can reproduce the bug?

  1. Create a valid skill directory with a valid SKILL.md frontmatter file.
  2. Replace that SKILL.md with a symlink to another valid SKILL.md file.
  3. Start/reload Codex in that workspace.
  4. Open $ skill suggestions.

Observed:

  • skills with regular SKILL.md are listed
  • skills with symlinked SKILL.md are not listed

What is the expected behavior?

A SKILL.md symlink that resolves to a regular file should be discovered and loaded the same as a regular SKILL.md.

Potential fix idea in loader.rs:

  • allow file candidates with:

file_type.is_file() || file_type.is_symlink()

  • and for symlinks, verify the target is a file (e.g. fs::metadata(&path)?.is_file()) before parsing.

Additional information

_No response_

View original on GitHub ↗

6 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #15345
  • #14767

Powered by Codex Action

etraut-openai contributor · 3 months ago

This is by design. The packaging boundary of a skill is its directory.

vegerot · 3 months ago

I have already fixed this locally and pushed the fix to my fork.

Branch: https://github.com/vegerot/codex/tree/mchcopl/bugfixes-skills-symlink-source-build
Relevant commit in that branch: a99d20f4a9c3 (fix(skills): Load symlinked skill files)

This branch contains a change that loads symlinked SKILL.md files directly instead of only following symlinked directories. If this approach looks acceptable, please merge the branch or cherry-pick the fix commit.

vegerot · 3 months ago

@etraut-openai May I ask _why_ this is the design? I understand that "the packaging boundary of a skill is its directory", but why block symlinks inside the skill itself?

light-merlin-dark · 3 months ago

_I've been struggling with this issue as well. Even GPT 5.4 xHigh noticed serious room for improvement here. I present to you this argument below:_

I think the current framing conflates two different concerns:

  1. what defines the packaging boundary of a skill
  2. what install mechanism is allowed to point at that package

If the packaging boundary is the skill directory, that is a reasonable rule. But rejecting a symlinked SKILL.md file is not the only way to preserve that rule.

The cleaner implementation would be:

  • allow a symlinked SKILL.md file to be discovered
  • canonicalize it to the target path
  • treat the target file’s parent directory as the skill directory for any sibling metadata/assets/scripts resolution

That preserves the directory boundary while still allowing a wrapper install at ~/.codex/skills/<slug>/SKILL.md.

Right now the behavior is asymmetric:

  • symlinked directories are followed in core-skills/src/loader.rs
  • perCwdExtraUserRoots is already supported by app-server
  • but a symlinked SKILL.md file is skipped

So Codex already accepts external skill locations in other forms. Disallowing only the file-level entry point creates a narrow implementation boundary rather than a consistent packaging rule.

This matters in practice because wrapper installs are useful for keeping canonical skills in versioned repos while avoiding top-level directory symlink pollution in harness roots. That is a legitimate workflow, especially for users syncing skills across multiple harnesses.

If the intended product decision is “full directory installs only,” that should probably come with first-class support for durable extra skill roots in the desktop/TUI flow. Otherwise the current behavior leaves a gap:

  • full-directory external roots are conceptually allowed
  • but the most surgical wrapper install is rejected
  • and the desktop client does not expose a durable extra-root workflow

So I think the most coherent options are:

  • support symlinked SKILL.md by canonicalizing before metadata/asset resolution, or
  • formally require full-directory installs and add first-class persistent extra skill roots in the client

The current middle state is hard to reason about because the protocol surface and the loader behavior are not aligned.

mrtomatwo · 1 month ago

@etraut-openai having a folder as package boundary is well and good.
The symlink is me explicitly adding another file into this very folder, quite the same as to me copying another file into it.

My use case is that I do not want to scatter multiple copies of my skills across my system but symlink a single source of truth.

Treating symlinks just like files is a well established pattern in the Unix world and codex treating them the same seems like an obvious choice.

Your argument does not address this issue.
Please reconsider re-opening and fixing this.