macOS seatbelt blocks com.apple.FSEvents, causing directory fs.watch/Watchpack failures in sandboxed execution
What version of Codex CLI is running?
codex-cli 0.117.0-alpha.10
What subscription do you have?
Pro
Which model were you using?
gpt-5.4
What platform is your computer?
Darwin 25.3.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
Codex Desktop app (embedded runtime; TERM=dumb, SHELL=/bin/zsh).
Also reproducible via the public CLI using `codex sandbox macos --full-auto`.
What issue are you seeing?
On macOS, Codex's sandboxed execution path appears to deny Mach lookups to com.apple.FSEvents, which causes directory fs.watch() to fail with:
EMFILE: too many open files, watch
In real projects, this can surface as repeated Watchpack/Next.js dev-server failures, including apparent false-positive next.config.ts change detection and unnecessary restarts.
I can reproduce this outside the app with the public CLI sandbox command:
codex sandbox macos --full-auto --log-denials -- /opt/homebrew/bin/node -e 'const fs=require("fs"); const dir=process.argv[1]; const w=fs.watch(dir); let done=false; w.on("error", err=>{ if(done) return; done=true; console.error("watch-error", err.code, err.message); process.exit(1); }); setTimeout(()=>{ if(done) return; done=true; w.close(); console.log("watch-ok"); process.exit(0); }, 1500);' /Users/<redacted>/path/to/project
Actual output:
watch-error EMFILE EMFILE: too many open files, watch
=== Sandbox denials ===
(node) mach-lookup com.apple.FSEvents
(node) mach-lookup com.apple.logd
(node) mach-lookup com.apple.system.notification_center
As a control, file watching succeeds under the same sandboxed execution path:
codex sandbox macos --full-auto --log-denials -- /opt/homebrew/bin/node -e 'const fs=require("fs"); const target=process.argv[1]; const w=fs.watch(target); let done=false; w.on("error", err=>{ if(done) return; done=true; console.error("watch-error", err.code, err.message); process.exit(1); }); setTimeout(()=>{ if(done) return; done=true; w.close(); console.log("watch-ok"); process.exit(0); }, 1500);' /Users/<redacted>/path/to/project/next.config.ts
Output:
watch-ok
This appears to be specific to directory watching, not to fs.watch() in general.
What steps can reproduce the bug?
- On macOS, run:
codex sandbox macos --full-auto --log-denials -- /opt/homebrew/bin/node -e 'const fs=require("fs"); const dir=process.argv[1]; const w=fs.watch(dir); let done=false; w.on("error", err=>{ if(done) return; done=true; console.error("watch-error", err.code, err.message); process.exit(1); }); setTimeout(()=>{ if(done) return; done=true; w.close(); console.log("watch-ok"); process.exit(0); }, 1500);' /path/to/a/readable/project-directory
- Observe that sandboxed directory watching fails with
EMFILE.
- Observe that
--log-denialsreportsmach-lookup com.apple.FSEvents.
- As a control, run the same script against a file path instead of a directory path and observe that it succeeds.
This also reproduces in a real Next.js project:
pnpm dev
pnpm dev --webpack
Inside the Codex sandbox, Watchpack repeatedly reports EMFILE, and Next may falsely detect next.config.ts changes and restart the dev server.
What is the expected behavior?
Directory fs.watch() should work for readable project paths in Codex's macOS sandboxed execution environment.
As a result:
- standalone directory watchers should not fail with
EMFILE
- Watchpack should not emit repeated watcher errors
- Next.js dev should not falsely detect
next.config.tschanges or restart due to sandbox-induced watch failures
Additional information
I investigated this locally and found that the current macOS seatbelt base policy appears to be missing the following rule:
(allow mach-lookup
(global-name "com.apple.FSEvents")
)
Adding that rule to the seatbelt base policy fixed both:
- standalone sandboxed directory
fs.watch()repros
- minimal Watchpack repros
It does not appear to bypass path restrictions on its own. With that rule added, directory watching succeeds for explicitly readable roots, while unrelated paths outside those roots still fail with EPERM.
This looks like a sandbox policy bug or sandbox compatibility bug in Codex, rather than a generic Node/Watchpack RLIMIT_NOFILE issue.
Workaround:
WATCHPACK_POLLING=trueavoids the issue for Next/Watchpack-based stacks, but it is only a workaround.
3 Comments
I prepared a fix for this on a public fork branch:
Repro summary:
directory fs.watch()/ Watchpack-style directory watchers can fail inside sandboxed execution because the sandbox policy blocks lookup ofcom.apple.FSEvents.Root cause:
Fix summary:
(global-name "com.apple.FSEvents")to the base seatbelt policy.fs.watch()under the sandbox.Validation:
git diff --checkcargo fmt --check --allcargo test -p codex-core --test all directory_fs_watch_works_under_seatbelt -- --nocaptureCODEX_SANDBOX=seatbelt, so the heavier live path is skipped rather than hanging.I also tried to open a PR from this branch, but GitHub rejected PR creation for my account (
CreatePullRequestpermission denied). If a maintainer wants this as an invited PR, I can open it immediately from the branch above.This is also an issue for Sites (generating websites within Codex), HMR doesn't work well. I implemented a workaround for Sites but would be good to find a long term fix :)
https://github.com/openai/openai/pull/1070974
It took me way to long to figure this out (like 3 hours and 50% of my plus allowance for investigation). (Commenting for activity bump)