Skill loader mislabels transient EMFILE ("too many open files") as "invalid SKILL.md files", with no retry

Open 💬 0 comments Opened Aug 3, 2026 by astradevkin

Codex CLI version

0.146.0

Platform

macOS

What happened

On startup, the TUI reported:

⚠ Skipped loading 4 skill(s) due to invalid SKILL.md files.

...for 4 skills out of roughly 100 in a cloud-synced skills folder ($HOME/.agents/skills). The affected skills were not actually invalid — their SKILL.md files parse and validate fine. Re-launching Codex skipped a different set of skills each time, which is not consistent with a content problem. The pattern points to a resource-exhaustion race, not a validation failure, but the UI presents it as the latter with no path to recovery.

Why (source walk)

Skill discovery fans out file reads with two layers of concurrency:

Multiplied together, that's up to ~512 concurrent file opens across roots. Per skill, loader.rs:714 additionally does a tokio::join! that reads two files at once (the SKILL.md body and its metadata file), roughly doubling the peak fd count for that window.

macOS gives terminal-launched processes a default soft limit of 256 open file descriptors (ulimit -n). With ~512+ concurrent opens possible against a 256-fd ceiling, some fraction of reads hit EMFILE (os error 24) — a transient, load-dependent failure, not a property of the file's content.

That EMFILE is then wrapped as a generic parse error:

// loader.rs:170
Read(std::io::Error),
...
// loader.rs:180
SkillParseError::Read(e) => write!(f, "failed to read file: {e}"),
...
// loader.rs:718
let contents = contents.map_err(SkillParseError::Read)?;

and surfaced in the TUI as an "invalid SKILL.md" warning with no distinction from an actual YAML/schema error:

// codex-rs/tui/src/app/startup_prompts.rs:56
"Skipped loading {error_count} skill(s) due to invalid SKILL.md files."

There's no retry anywhere in this path — a skill that loses the fd race is simply dropped for that session.

Observed signature matching this theory: with ~98 SKILL.md files scanned in alphabetical order, the 4 failures landed at ranks 79, 80, 81, and 83 — with rank 82 succeeding in between. A content-validity bug would fail the same skill(s) every time; a transient fd-exhaustion window failing an arbitrary contiguous-ish band, with different survivors run to run, is the signature of a resource race, not a parsing bug.

Repro sketch

  1. Populate a skills directory with on the order of 100 SKILL.md files (all individually valid).
  2. Lower the fd soft limit to make the race reliable: ulimit -n 256
  3. Launch the Codex TUI and observe which skills, if any, get reported as "invalid."
  4. Repeat the launch a few times — if different skills fail (or none do) on different runs despite unchanged content, that's the EMFILE race, not a content bug.

Suggested fixes (offered, not prescriptive)

  1. Distinguish EMFILE/ENFILE from genuine parse/validation errors in SkillParseError and surface a distinct, honest message (e.g. "N skill(s) skipped: too many open files — try again" vs. "N skill(s) invalid: <reason>").
  2. Retry on EMFILE (with backoff) before giving up on a given skill file.
  3. Bound global concurrent file opens with a single semaphore shared across root scans and skill loads, rather than multiplying two independent per-layer limits (8 × 64 ≈ 512).
  4. Optionally, attempt to raise RLIMIT_NOFILE toward the hard limit at startup (many CLIs do this defensively on macOS given its low default soft limit).

Workaround

Raise the fd limit before launching Codex:

ulimit -n 65536

View original on GitHub ↗