Skip to content

Skill watcher follows directory symlinks and can recurse into huge external trees #30795

Description

@VitalyAnkh

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:

  1. A recursive watch on skills/.
  2. skills/result as a directory symlink to an outside target.
  3. Changes under the outside target do not produce events for the skills/ subscription.
  4. Changes under a real directory like skills/real/SKILL.md still do produce events.

Related issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    CLIIssues related to the Codex CLIapp-serverIssues involving app server protocol or interfacesbugSomething isn't workingperformanceskillsIssues related to skills

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions