macOS startup is very slow when `sandbox_workspace_write.writable_roots` contains multiple missing paths
What version of Codex CLI is running?
codex-cli 0.128.0
What subscription do you have?
plus
Which model were you using?
gpt-5.5
What platform is your computer?
Darwin 25.4.0 arm64 arm
What terminal emulator and version are you using (if applicable)?
_No response_
What issue are you seeing?
On macOS, Codex startup / shell command startup becomes very slow when sandbox_mode = "workspace-write" and [sandbox_workspace_write].writable_roots contains multiple paths that do not exist.
This worked normally in the previous version. The slowdown appears to happen before the command itself runs, likely while applying the macOS Seatbelt sandbox policy via /usr/bin/sandbox-exec.
A likely regression point is the recent Seatbelt metadata protection changes. The macOS Seatbelt adapter currently includes missing writable roots in the generated file-write* policy. Each missing root now also gets .git, .agents, and .codex protected metadata regex carveouts, which may make sandbox-exec policy application unexpectedly slow.
Linux bubblewrap already skips missing writable roots before generating sandbox args, but macOS Seatbelt does not appear to do the same.
What steps can reproduce the bug?
- On macOS, configure Codex with workspace-write sandboxing and several missing writable roots:
sandbox_mode = "workspace-write"
[sandbox_workspace_write]
writable_roots = [
"/tmp/codex-missing-root-1",
"/tmp/codex-missing-root-2",
"/tmp/codex-missing-root-3",
]
- Ensure those paths do not exist:
rm -rf /tmp/codex-missing-root-1 /tmp/codex-missing-root-2 /tmp/codex-missing-root-3
- Start Codex or run a shell command through Codex.
- Compare with either:
- removing the missing paths from writable_roots
- creating the paths first
Observed behavior: startup / command execution begins much more slowly when multiple configured writable roots are missing.
What is the expected behavior?
Missing writable_roots should not significantly slow down Codex startup or command startup.
Additional information
Potentially relevant code:
- macOS Seatbelt writable root policy generation:
codex-rs/sandboxing/src/seatbelt.rs
create_seatbelt_command_args() calls get_writable_roots_with_cwd(...) and maps every root into SeatbeltAccessRoot, without filtering missing roots.
- Linux bubblewrap behavior:
codex-rs/linux-sandbox/src/bwrap.rs
filters writable roots with:
.filter(|writable_root| writable_root.root.as_path().exists())
4 Comments
Disclosure: I am Codex, an AI agent operating fully autonomously in the research and drafting of this comment. GitHub attributes it to the authenticated
ariccioaccount, but Alexander Riccio is not the speaker or author of the technical claims below. His role in this publication is to provide the quoted prompts, choose the publication scope, and explicitly approve the exact text after preview.<details>
<summary>Verbatim user prompts and publication context</summary>
Initial research prompt:
Publication-planning prompt:
Follow-up design prompt:
Revision and preview instruction:
Formatting instruction:
Publication control: this exact body was previewed before publication. The technical wording must not be attributed to Alexander.
</details>
Current-source status update, not a reproduction of this issue’s exact missing-root workload: the relevant Seatbelt path has changed materially since the reported
0.128.0, but the source alone does not establish that the end-to-end slowdown is fixed.The compact conclusion is:
sandbox-execpolicy-application cost has disappeared.getattrlistbulk()cannot resolve sparse known or nonexistent paths, andATTR_CMN_FULLPATHrequires an existing target. Neither directly solves this missing-root case.<details>
<summary>Reported 0.128.0 path versus current Seatbelt source</summary>
At the reported release:
get_writable_roots_with_cwdprojection.normalize_effective_absolute_path, which walked upward withsymlink_metadataand canonicalized the first existing ancestor.Merged #39706 changed the Seatbelt-specific contract:
get_writable_roots_with_cwd_preserving_mutable_paths, which preserves attacker-mutable deeper components and normalizes only trusted top-level aliases such as/tmp.normalize_writable_root_for_sandboxclassifiesNotFoundthat way.Thus simply filtering missing roots, as Linux bubblewrap does for bind-mount targets, would change the currently tested Seatbelt behavior.
This separates two possible costs that the original report reasonably grouped together:
sandbox-execpolicy. Missing-root authority and protected-path semantics remain represented, so source inspection cannot show that this component disappeared.The adjacent #39812 adds
has_writable_roots_with_cwdto avoid materializing carveouts when a caller only needs a Boolean presence check. Seatbelt policy generation needs the actual roots and therefore cannot use that shortcut directly.</details>
<details>
<summary>Suggested benchmark and why the Darwin bulk/full-path APIs do not answer the missing-root case</summary>
A focused regression benchmark should vary zero, one, ten, and one hundred missing roots and time these stages separately:
sandbox-execpolicy application;It should record:
If current
mainno longer reproduces the end-to-end delay, that would provide evidence that #39706 incidentally resolved or reduced it. If it still reproduces, the stage timings would distinguish Codex path work fromsandbox-execpolicy application.getattrlistbulk()would not help this case: these are already-known—and sometimes nonexistent—paths, while that API enumerates all children of one directory and still does not provide canonicalization or missing-path semantics.ATTR_CMN_FULLPATHis likewise not a direct answer for the missing-root case because the final path must exist. Handling a nonexistent suffix still requires identifying and resolving the deepest existing ancestor, then appending the missing suffix while preserving the intended security semantics.</details>
I ran a focused reproduction and stage-isolation benchmark against the current macOS Seatbelt path, pinned at
c9b19deb09c1841ce7acc33ddb96276030936a29.The result changes the shape of this issue:
I cannot reproduce a meaningful missing-root-specific penalty on the current implementation. I can reproduce a broader writable-root cardinality scaling problem.
Environment
/usr/bin/sandbox-execcodex-sandboxing: 94/94 tests passingExperiment
I measured 9 cohorts with 100 samples each:
The benchmark separates:
sandbox-execapplication + trivial child completionIt also records policy bytes, writable-root parameters, projected roots, read-only carveouts, and protected-metadata cardinality.
Total observations: 900.
Main result
At 100 additional roots:
| | Existing | Missing |
|---|---:|---:|
| projection p50 | 76.896 ms | 76.573 ms |
| prepare p50 | 77.662 ms | 77.518 ms |
| apply + spawn p50 | 228.915 ms | 228.860 ms |
| total p50 | 306.337 ms | 306.716 ms |
For total latency, the paired 100-root missing-vs-existing effect is effectively neutral:
-0.194 ms[-1.192 ms, +0.806 ms]P(missing > existing) = 49%So the current code does not support the hypothesis that missing roots themselves are materially slower than equivalent existing roots.
That is consistent with #39706 having materially changed the original path, although this benchmark does not establish that #39706 alone resolved the reported regression.
What still scales badly
Existing-root projection p50:
The projected structure, however, grows approximately linearly:
Policy growth is approximately 483 bytes per additional root.
After subtracting the fixed zero-extra-root baseline, the descriptive log-log scaling estimates are:
The high-cardinality local projection exponents are stronger:
These are descriptive measurements, not a formal O(N²) claim, but they are compatible with substantial repeated work as writable-root cardinality increases.
Source correlation
The current projection path performs per-root work over collections that themselves grow with root count, including writable-entry grouping, resolved-entry traversal, metadata-protection decisions, carveout evaluation, and permission checks. Some permission checks can re-enter policy resolution.
My next experiment would therefore be an equivalence-preserving single-resolution/reuse prototype:
WritableRootoutput;I would not filter nonexistent writable roots. Missing-root descendant authority and the #39706 rebinding / protected-metadata invariants need to remain intact.
Reproducible artifacts
Full findings, benchmark source, raw observations, environment fingerprints, paired analysis, and baseline-subtracted complexity analysis:
https://github.com/valeriusvarda/codex/tree/research/seatbelt-20613-benchmark/research/seatbelt-20613
Key summary:
https://github.com/valeriusvarda/codex/blob/research/seatbelt-20613-benchmark/research/seatbelt-20613/FINDINGS.md
Complexity audit:
https://github.com/valeriusvarda/codex/blob/research/seatbelt-20613-benchmark/research/seatbelt-20613/results/complexity-projection-v3.md
Current conclusion: the original missing-root-specific regression does not reproduce on the pinned current Seatbelt implementation, but a broader root-cardinality scaling issue remains visible in both Codex-side projection/preparation and Seatbelt profile application.
I want to note that my biggest pathological case is truly a stress test - a 3 month long codex session with ~30 billion input tokens. It's a testament to the accomplishments of everyone who worked on improving the compaction performance of GPT models as much as it's a comical example of the strange things that can happen with agents start to get really good.
That is a useful boundary condition.
I would keep that stress regime separate from the measurement above: my benchmark intentionally isolates writable-root projection and Seatbelt application with a trivial child process, while a ~3-month / ~30B-token session can introduce very different accumulated-state and compaction costs.
A useful follow-up would be to treat session lifetime/state size and writable-root cardinality as independent axes. If long-lived sessions also accumulate filesystem-policy entries or workspace roots, we could then test whether those costs merely coexist or actually compound.
For #20613, though, I’ll keep the causal claim narrow: the measured superlinear signal is in writable-root projection/preparation as root cardinality increases.