codex - 💡(How to fix) Fix Bug: user-installed skills under `~/.codex/skills` are skipped when `SKILL.md` is a symlink [1 comments, 2 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openai/codex#17344Fetched 2026-04-11 06:17:18
View on GitHub
Comments
1
Participants
2
Timeline
5
Reactions
0
Timeline (top)
labeled ×3closed ×1commented ×1

Root Cause

The wrapper-directory pattern is attractive because it avoids top-level directory-symlink pollution while preserving one canonical skill source.

Code Example

/Users/me/_dev/advising/skills/advising/SKILL.md

---

/Users/me/.codex/skills/advising/SKILL.md -> /Users/me/_dev/advising/skills/advising/SKILL.md

---

[[skills.config]]
path = "/Users/me/.codex/skills/advising/SKILL.md"
enabled = true
RAW_BUFFERClick to expand / collapse

What version of the Codex App are you using (From “About Codex” dialog)?

Version 26.406.31014 (1395)

What subscription do you have?

ChatGPT Pro

What platform is your computer?

Darwin 25.3.0 arm64 arm

What issue are you seeing?

Codex currently skips valid user skills installed under ~/.codex/skills/<slug>/SKILL.md when the SKILL.md file itself is a symlink to a canonical skill file stored elsewhere, for example in a repo under _dev/.../skills/.../SKILL.md.

This breaks a common wrapper-install pattern:

  • keep the canonical skill in a normal repo
  • install a lightweight wrapper directory in ~/.codex/skills/<slug>/
  • symlink only SKILL.md back to the canonical source

The skill is valid, enabled, and readable, but it does not appear in Codex skills/list or the desktop UI.

This blocks tools like SkillSync from using a clean symlink-first install strategy for Codex.

Without an upstream fix, users are forced to:

  • duplicate or materialize skill files into ~/.codex/skills
  • maintain custom extra-root injection outside the normal Codex desktop flow
  • accept drift between canonical repo skills and Codex-visible installs

The wrapper-directory pattern is attractive because it avoids top-level directory-symlink pollution while preserving one canonical skill source.

What steps can reproduce the bug?

  1. Create a valid skill in a normal repo:
/Users/me/_dev/advising/skills/advising/SKILL.md
  1. Install a Codex wrapper directory:
/Users/me/.codex/skills/advising/SKILL.md -> /Users/me/_dev/advising/skills/advising/SKILL.md
  1. Ensure it is enabled in ~/.codex/config.toml:
[[skills.config]]
path = "/Users/me/.codex/skills/advising/SKILL.md"
enabled = true
  1. Ask Codex app-server for skills/list for a workspace.

  2. Observe that advising is missing.

  3. Repeat the same request, but inject perCwdExtraUserRoots that points to the repo-backed skills root.

  4. Observe that advising now appears immediately.

What is the expected behavior?

If ~/.codex/skills/<slug>/SKILL.md exists, is enabled, and resolves to a valid file, Codex should parse it and include it in skills/list and the desktop skills UI.

Symlinked SKILL.md files should behave the same as regular files.

Additional information

In codex-rs/core-skills/src/loader.rs, the loader handles symlinks like this:

  • if the symlink resolves to a directory, it follows it
  • otherwise it continues

Relevant code:

  • codex-rs/core-skills/src/loader.rs around the file_type.is_symlink() branch

The specific behavior is visible here in current main:

  • symlink branch follows directories only
  • file symlinks, including SKILL.md, are dropped

This appears to be the direct reason user wrapper installs are invisible.

In the symlink branch, if:

  • the resolved metadata is a file, and
  • the entry name is SKILL.md

then call parse_skill_file(&path, scope) instead of skipping it.

That is a small, targeted fix and should preserve the existing directory-follow behavior.

Pseudo-shape:

if file_type.is_symlink() {
    if !follow_symlinks {
        continue;
    }

    let metadata = match fs::metadata(&path) {
        Ok(metadata) => metadata,
        Err(e) => { ... }
    };

    if metadata.is_dir() {
        ...
        continue;
    }

    if metadata.is_file() && file_name == SKILLS_FILENAME {
        match parse_skill_file(&path, scope) {
            Ok(skill) => outcome.skills.push(skill),
            Err(err) => { ... }
        }
        continue;
    }

    continue;
}

There is also a related but larger UX gap:

- app-server already supports `perCwdExtraUserRoots`
- the desktop/TUI currently sends `skills/list` with `per_cwd_extra_user_roots: None`

That makes extra roots a hidden protocol capability rather than a durable desktop feature.

However, the symlinked-`SKILL.md` bug above is the smaller and more direct fix for wrapper installs under `~/.codex/skills`.


Protocol/docs and tests already support extra roots:

- `codex-rs/app-server/README.md` documents `perCwdExtraUserRoots`
- `codex-rs/app-server/tests/suite/v2/skills_list.rs` includes coverage for extra roots

Desktop/TUI currently does not pass them:

- `codex-rs/tui/src/app.rs` sends `SkillsListParams { ..., per_cwd_extra_user_roots: None }`


This would make Codex interoperate cleanly with repo-backed skill managers that install safe wrapper directories into `~/.codex/skills` while keeping canonical skills in versioned repos.

It would also reduce the need for copied/materialized installs and avoid unnecessary skill drift.

extent analysis

TL;DR

The issue can be fixed by modifying the codex-rs/core-skills/src/loader.rs file to handle symlinks to SKILL.md files by parsing them instead of skipping.

Guidance

  • Modify the codex-rs/core-skills/src/loader.rs file to add a condition to check if the symlink resolves to a file and the entry name is SKILL.md, and if so, call parse_skill_file(&path, scope) instead of skipping it.
  • Update the codex-rs/tui/src/app.rs file to pass per_cwd_extra_user_roots when sending SkillsListParams to enable extra roots support in the desktop UI.
  • Test the changes with the provided steps to reproduce the bug to ensure the fix works as expected.
  • Consider updating the documentation and tests to reflect the changes and ensure consistency across the codebase.

Example

A possible code change could be:

if file_type.is_symlink() {
    if !follow_symlinks {
        continue;
    }

    let metadata = match fs::metadata(&path) {
        Ok(metadata) => metadata,
        Err(e) => { ... }
    };

    if metadata.is_dir() {
        ...
        continue;
    }

    if metadata.is_file() && file_name == SKILLS_FILENAME {
        match parse_skill_file(&path, scope) {
            Ok(skill) => outcome.skills.push(skill),
            Err(err) => { ... }
        }
        continue;
    }

    continue;
}

Notes

The fix assumes that the parse_skill_file function is correctly implemented and can handle symlinks to SKILL.md files. Additionally, the changes to the codex-rs/tui/src/app.rs file are necessary to enable extra roots support in the desktop UI.

Recommendation

Apply the workaround by modifying the codex-rs/core-skills/src/loader.rs file to handle symlinks to SKILL.md files, as this is a targeted fix that preserves the existing directory-follow behavior.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING