What happened?
I hit a reproducible hang/high-CPU case when starting Codex from a repo that had a directory symlink under its project skill root:
.codex/skills/result -> /nix/store/vhv8cnk1sk5pkqzywzrhmn3gavzvki7i-nixos-system-eva-26.05.20260523.64c08a7
That target is a large NixOS system closure. A stuck Codex process had a busy notify-rs inotify loop thread, and removing only that symlink made startup behave normally again.
The important part is not the literal name result; it is that the skills watcher recursively watches the skill root, and the underlying notify watcher follows directory symlinks by default. That can make a small repo-local skill root expand into a huge external tree.
Why this looks like a Codex bug
From the current Rust code:
codex-rs/app-server/src/skills_watcher.rs registers non-plugin skill roots as recursive watches.
codex-rs/file-watcher/src/lib.rs creates the notify watcher with the default config.
notify 8.2 defaults follow_symlinks to true, so recursive watches can traverse directory symlinks.
codex-rs/core-skills/src/loader.rs intentionally follows directory symlinks for user/repo/admin skills, and there are tests for loading skills via symlinked skill directories.
So changing only the skill loader policy would be too broad and would likely break valid symlinked skill setups. The issue is specifically the watcher behavior: a force-included skill root should be watched, but directory symlink targets beneath it should not be recursively watched as part of that root.
This is similar in shape to warpdotdev/warp#13252, where a force-included provider directory remains watched but directory symlinks below it are pruned from recursive repo watches.
Expected behavior
Codex should not recursively watch directory symlink targets beneath skill roots such as .codex/skills or .agents/skills.
A normal directory under the skill root should still be watched, and existing symlinked skill loading behavior should remain intact after restart/reload.
Actual behavior
A directory symlink under .codex/skills can cause Codex's recursive watcher to walk and watch the symlink target. If the target is a large tree such as a Nix store closure, startup can hang or burn CPU in the notify/inotify thread.
Minimal reproduction shape
On Linux:
mkdir -p repro/.codex/skills
mkdir -p outside/result-target/deep
ln -s ../../outside/result-target repro/.codex/skills/result
cd repro
codex
The problematic production case used a symlink to a much larger tree under /nix/store, but the shape above is the same.
Suggested fix direction
Use a recursive watch configuration that does not follow directory symlinks. In notify 8.2 this appears to be available as:
notify::Config::default().with_follow_symlinks(false)
This should be applied at the file-watcher layer rather than by disabling loader support for symlinked skill directories.
A regression test should cover:
- A recursive watch on
skills/.
skills/result as a directory symlink to an outside target.
- Changes under the outside target do not produce events for the
skills/ subscription.
- Changes under a real directory like
skills/real/SKILL.md still do produce events.
Related issues
What happened?
I hit a reproducible hang/high-CPU case when starting Codex from a repo that had a directory symlink under its project skill root:
That target is a large NixOS system closure. A stuck Codex process had a busy
notify-rs inotify loopthread, and removing only that symlink made startup behave normally again.The important part is not the literal name
result; it is that the skills watcher recursively watches the skill root, and the underlyingnotifywatcher follows directory symlinks by default. That can make a small repo-local skill root expand into a huge external tree.Why this looks like a Codex bug
From the current Rust code:
codex-rs/app-server/src/skills_watcher.rsregisters non-plugin skill roots as recursive watches.codex-rs/file-watcher/src/lib.rscreates thenotifywatcher with the default config.notify8.2 defaultsfollow_symlinkstotrue, so recursive watches can traverse directory symlinks.codex-rs/core-skills/src/loader.rsintentionally follows directory symlinks for user/repo/admin skills, and there are tests for loading skills via symlinked skill directories.So changing only the skill loader policy would be too broad and would likely break valid symlinked skill setups. The issue is specifically the watcher behavior: a force-included skill root should be watched, but directory symlink targets beneath it should not be recursively watched as part of that root.
This is similar in shape to warpdotdev/warp#13252, where a force-included provider directory remains watched but directory symlinks below it are pruned from recursive repo watches.
Expected behavior
Codex should not recursively watch directory symlink targets beneath skill roots such as
.codex/skillsor.agents/skills.A normal directory under the skill root should still be watched, and existing symlinked skill loading behavior should remain intact after restart/reload.
Actual behavior
A directory symlink under
.codex/skillscan cause Codex's recursive watcher to walk and watch the symlink target. If the target is a large tree such as a Nix store closure, startup can hang or burn CPU in the notify/inotify thread.Minimal reproduction shape
On Linux:
mkdir -p repro/.codex/skills mkdir -p outside/result-target/deep ln -s ../../outside/result-target repro/.codex/skills/result cd repro codexThe problematic production case used a symlink to a much larger tree under
/nix/store, but the shape above is the same.Suggested fix direction
Use a recursive watch configuration that does not follow directory symlinks. In
notify8.2 this appears to be available as:This should be applied at the file-watcher layer rather than by disabling loader support for symlinked skill directories.
A regression test should cover:
skills/.skills/resultas a directory symlink to an outside target.skills/subscription.skills/real/SKILL.mdstill do produce events.Related issues