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?
- Create a valid skill directory with a valid
SKILL.mdfrontmatter file. - Replace that
SKILL.mdwith a symlink to another validSKILL.mdfile. - Start/reload Codex in that workspace.
- Open
$skill suggestions.
Observed:
- skills with regular
SKILL.mdare listed - skills with symlinked
SKILL.mdare 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_
6 Comments
Potential duplicates detected. Please review them and close your issue if it is a duplicate.
Powered by Codex Action
This is by design. The packaging boundary of a skill is its directory.
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.mdfiles directly instead of only following symlinked directories. If this approach looks acceptable, please merge the branch or cherry-pick the fix commit.@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?
_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:
If the packaging boundary is the skill directory, that is a reasonable rule. But rejecting a symlinked
SKILL.mdfile is not the only way to preserve that rule.The cleaner implementation would be:
SKILL.mdfile to be discoveredThat preserves the directory boundary while still allowing a wrapper install at
~/.codex/skills/<slug>/SKILL.md.Right now the behavior is asymmetric:
core-skills/src/loader.rsperCwdExtraUserRootsis already supported by app-serverSKILL.mdfile is skippedSo 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:
So I think the most coherent options are:
SKILL.mdby canonicalizing before metadata/asset resolution, orThe current middle state is hard to reason about because the protocol surface and the loader behavior are not aligned.
@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.