Standalone Windows CLI `:workspace` profile creates unsupported split roots and breaks `apply_patch`

Open 💬 9 comments Opened Jul 23, 2026 by toyfrog
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

codex-cli 0.145.0

What subscription do you have?

ChatGPT Plus

Which model were you using?

GPT-5.6

What platform is your computer?

Windows 11 Pro, x64, build 26200.

Shell: PowerShell 7.

Summary

The standalone Codex CLI on native Windows selects the built-in :workspace permission profile, which includes both the active workspace and system temporary directories as writable roots. The unelevated restricted-token sandbox cannot enforce that split writable-root set, so apply_patch refuses to run before it reads or changes an existing workspace file.

This is not limited to Codex Desktop. Live process inspection showed that the session was hosted directly by the standalone lowercase CLI executable, with no packaged Codex.exe UI process running.

The safe refusal to run unsandboxed is correct. The defect is that a built-in CLI permission profile produces a policy shape that the selected supported Windows backend cannot enforce.

Actual behavior

Updating an existing workspace file with apply_patch fails before file access:

apply_patch verification failed: Failed to read file to update <workspace>\<file>:
failed to prepare fs sandbox:
failed to prepare windows sandbox wrapper:
windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed

The failure reproduced on three consecutive apply_patch attempts. No target file was changed.

Ordinary sandboxed PowerShell reads and commands continue to work.

Steps to reproduce

  1. Run the standalone Codex CLI on native Windows.
  2. Configure the fallback Windows sandbox:

``toml
[windows]
sandbox = "unelevated"
``

  1. Use the built-in :workspace permission profile. In the reproduced session, the sandboxed command environment reported:

``text
CODEX_PERMISSION_PROFILE=:workspace
CODEX_SANDBOX_NETWORK_DISABLED=1
``

  1. Open a writable Git workspace.
  2. Ask Codex to update an existing file using the internal apply_patch tool.
  3. Observe that the tool fails before reading the file with the split-root error above.

The effective writable-root set supplied to the session contained:

  • the active workspace;
  • C:\tmp;
  • the :tmpdir special root; and
  • the :slash_tmp special root.

No user-configured additional writable directory was required.

Research and diagnostic steps performed

  1. Reproduced the exact apply_patch failure three consecutive times against existing workspace files.
  2. Verified that normal sandboxed PowerShell commands and workspace reads work.
  3. Inspected the process ancestry using the non-mutating PowerShell process API:

``text
pwsh.exe
parent -> C:\Users\<user>\AppData\Local\Programs\OpenAI\Codex\bin\codex.exe
``

  1. Enumerated running OpenAI/Codex processes. The primary runtime was lowercase standalone codex.exe; no separate packaged Codex.exe Desktop/UI process was present.
  2. Verified command resolution:

``text
Get-Command codex
-> C:\Users\<user>\AppData\Local\Programs\OpenAI\Codex\bin\codex.exe
``

  1. Inspected relevant session markers and confirmed CODEX_PERMISSION_PROFILE=:workspace.
  2. Inspected the user configuration. The relevant configuration contained only:

``toml
[windows]
sandbox = "unelevated"
``

It did not define sandbox_mode, default_permissions, [sandbox_workspace_write], or a custom [permissions.*] profile.

  1. Ran codex doctor --json --all. Configuration and installation checks completed, but the doctor workflow did not perform an actual apply_patch create/update/delete health probe and therefore did not detect this failure.
  2. Compared the observed policy with the current permissions documentation. The built-in :workspace profile is documented to allow writes inside active workspace roots and system temporary directories.
  3. Reviewed related reports #30712, #31220, and #32314. Those establish related split-root behavior, but #30712 and #31220 frame it primarily as Desktop-managed injection, while #32314 includes CLI exposure as one part of a larger elevated-sandbox performance report.

Root-cause analysis

The evidence supports this causal chain:

  1. The standalone CLI selects the built-in :workspace profile.
  2. By definition, :workspace makes both the active workspace and system temporary directories writable.
  3. These are disjoint filesystem roots on native Windows.
  4. The unelevated restricted-token backend cannot safely enforce every split read/write policy.
  5. The apply_patch filesystem wrapper detects the unsupported policy and correctly refuses to fall back to unsandboxed execution.
  6. Therefore, the standard built-in write profile and the supported unelevated Windows backend are mutually incompatible for this tool path.

This was ruled out as:

  • a Codex Desktop background process;
  • a repository-specific configuration problem;
  • an NTFS permission problem on the target file;
  • malformed patch syntax;
  • a Git-state problem; or
  • an authentication problem.

Expected behavior

A built-in permission profile offered by the standalone CLI should always resolve to a policy that the selected supported sandbox backend can enforce.

At least one of the following should occur:

  1. The unelevated Windows backend supports the split writable-root set used by :workspace.
  2. On unelevated Windows, :workspace consolidates temporary writes under the active workspace—for example, through a workspace-local runtime temp directory—so the backend receives one writable root.
  3. Codex omits system temporary directories from the effective writable set when the active tool operation does not require them.
  4. Session startup rejects the incompatible profile/backend combination with an actionable message before work begins, rather than advertising a writable workspace and failing only when apply_patch is invoked.

apply_patch should be able to create, update, and delete files inside the active workspace without full access, shell-based file rewriting, or an approval that broadens authority.

Security and workflow impact

The current behavior disables the precise, auditable edit path and pressures agents toward shell-based rewrites, full access, or escalated execution. Refusing to run unsandboxed is the right security behavior; the durable fix is to prevent the default CLI profile from creating an unenforceable policy.

Suggested deterministic regression test

On native Windows with windows.sandbox="unelevated" and the built-in :workspace profile:

  1. Assert that the effective permission policy is accepted at session startup.
  2. Use internal apply_patch to create, update, and delete a disposable file beneath the workspace.
  3. Assert that each operation succeeds.
  4. Assert that writes outside the workspace and approved runtime-temp boundary fail.
  5. Assert that no unsandboxed fallback or escalation occurred.

Related issues

  • #30712 — Desktop-managed split writable roots break apply_patch.
  • #31220 — Desktop sandbox modes expose incompatible shell and patch behavior.
  • #32314 — CLI and Desktop reproduction reported as part of elevated-sandbox performance analysis.

This report isolates the standalone CLI profile/backend incompatibility and includes process-level evidence that Desktop was not involved.

View original on GitHub ↗

9 Comments

github-actions[bot] contributor · 1 month ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #34088
  • #34970

Powered by Codex Action

bbingz · 1 month ago

Analysis (community)

Thank you for the careful report — the process ancestry, exact config, and writable-root enumeration make this very easy to reason about from source.

  • Environment / versions (from the issue): codex-cli 0.145.0, Windows 11 Pro x64 build 26200, PowerShell 7; config only [windows] sandbox = "unelevated"; session markers CODEX_PERMISSION_PROFILE=:workspace and CODEX_SANDBOX_NETWORK_DISABLED=1; process tree under standalone …\OpenAI\Codex\bin\codex.exe (not Desktop UI).
  • Repro status: Not re-run on Windows here (no Windows host for this pass). Treat the OP’s three consecutive apply_patch failures as the primary runtime evidence. What follows is source-level confirmation on current main that the observed error string and failure path match a real policy/backend mismatch for the default CLI profile.
  • Observed vs expected:
  • Observed: apply_patch fails before reading the target with

windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed
after failed to prepare fs sandbox / failed to prepare windows sandbox wrapper. Effective writable set includes workspace + C:\tmp + :tmpdir + :slash_tmp. Ordinary sandboxed shell still works.

  • Expected: A built-in profile the CLI advertises (:workspace) should resolve to a policy the selected Windows backend can enforce, without weakening the correct refuse-unsandboxed behavior.

Root-cause hypothesis

High confidence that this is a default profile shape vs unelevated overlay checker mismatch, not a bad patch or NTFS issue.

  1. Built-in :workspace always grants temp specials.

PermissionProfile::workspace_write() (codex-rs/protocol/src/models.rs) calls workspace_write_with(..., exclude_tmpdir_env_var=false, exclude_slash_tmp=false).
FileSystemSandboxPolicy::workspace_write (codex-rs/protocol/src/permissions.rs) therefore adds Write on :slash_tmp and :tmpdir (plus project roots and the usual .git / .agents / .codex carveouts).

  1. :slash_tmp resolves to /tmp whenever that path is a directory.

resolve_file_system_special_path(SlashTmp) builds AbsolutePathBuf("/tmp") and includes it if is_dir(). On Windows that commonly surfaces as C:\tmp, matching the issue’s writable-root list.

  1. apply_patch hits the unelevated restricted-token overlay path.

FS helper prep ends in resolve_windows_restricted_token_filesystem_overrides (codex-rs/sandboxing/src/windows.rs). With WindowsSandboxLevel::RestrictedToken / unelevated, that function compares:

  • legacy roots from the compatibility SandboxPolicy projection, vs
  • split roots from FileSystemSandboxPolicy::get_writable_roots_with_cwd.

If the two BTreeSets differ, it hard-errors with the exact string above and sets write_roots_override: None (unelevated only layers extra deny-write carveouts).

  1. On Windows those sets diverge for the built-in default.

Legacy SandboxPolicy::get_writable_roots_with_cwd (codex-rs/protocol/src/protocol.rs) only adds /tmp under cfg!(unix) && !exclude_slash_tmp.
Split policy still resolves :slash_tmp whenever /tmp exists as a dir — platform-independent.
So with C:\tmp present, split includes an extra root the legacy projection never had → equality check fails → refuse unsandboxed (correct security posture).

  1. The Windows runtime already treats temp differently than the overlay checker.

ResolvedWindowsSandboxPermissions::writable_roots_for_cwd (codex-rs/windows-sandbox-rs/src/resolved_permissions.rs) strips SlashTmp/Tmpdir specials and re-adds temp via TEMP/TMP. Elevated prep can emit write_roots_override when sets diverge; unelevated does not. Existing unit coverage (windows_restricted_token_allows_workspace_write_profiles in codex-rs/core/src/exec_tests.rs) only asserts unelevated success when temps are excluded (exclude_tmpdir_env_var=true, exclude_slash_tmp=true) — i.e. not the built-in default.

  1. Why shell can still look fine: paths that never require direct runtime enforcement / FS-wrapper prep can avoid this equality check; apply_patch verification/update does not.

Related class (same error surface, different entry points): #30712, #31220, #32314, #33806, #34970. #34088 closed after switching to elevated — a workable user workaround, not a code fix for the default CLI + unelevated combination.

High-level fix outline (not a PR)

Prefer a small, policy + comparison alignment change over expanding unelevated capability first:

  1. Stop advertising Unix :slash_tmp as a Windows writable special for the built-in :workspace profile (or default exclude_slash_tmp on Windows / when the backend is unelevated restricted-token). Keep temp writability via the existing TEMP/TMP path in resolved_permissions.
  2. In resolve_windows_restricted_token_filesystem_overrides, compare writable roots using the same Windows-normalized root set the runtime already builds (strip SlashTmp/Tmpdir, remap temp) so protocol specials do not invent a second incompatible root set for the equality check alone.
  3. Optional UX: early session/codex doctor diagnostic when unelevated + multi-root profile is still unenforceable for the apply_patch / direct-FS path — today doctor does not probe create/update/delete under the selected backend.

Alternatives considered (heavier or riskier):

  • Teach unelevated to accept write_roots_override like elevated (real capability/security review).
  • Force elevated for apply_patch only (inconsistent; hits elevated perf/setup issues from #32314 / #31220).

Non-goals / do not weaken:

  • Do not remove refuse-to-run-unsandboxed.
  • Do not drop .git / .agents / .codex carveouts.
  • Do not paper over Desktop-only root injection (#30712 / #34970) without also fixing the CLI default path shown here.

Happy to drop any of (1)–(3) if the team already has a preferred backend-side direction.

Test ideas

What should fail before / pass after if the team (or an invitee) implements:

  • Unit (Windows target): PermissionProfile::workspace_write() with default temps included + WindowsSandboxLevel::RestrictedToken + a layout where AbsolutePathBuf("/tmp") exists as a directory → unsupported_windows_restricted_token_sandbox_reason is None and resolve_windows_restricted_token_filesystem_overrides returns Ok(_), not the split-root error.
  • Extend windows_restricted_token_allows_workspace_write_profiles to cover built-in defaults, not only exclude_* = true.
  • Regression: unelevated + :workspace apply_patch create / update / delete of a disposable workspace file succeeds with no unsandboxed fallback; write outside workspace still denied.
  • Optional: doctor (or session startup) probe that exercises the same FS-wrapper path so this class is visible without a full agent turn.

Questions for maintainers

  1. Is the durable fix intended in the Windows default profile shape (exclude / stop advertising :slash_tmp, align temp to TEMP/TMP), or in the unelevated backend (multi-root write_roots_override)?
  2. Is C:\tmp required to repro, or do other multi-root cases (extra --add-dir, plugins, Desktop viz roots as in #34970) hit the same equality check without it?
  3. Should codex doctor grow an apply_patch create/update probe under the selected Windows backend?
  4. For triage: is shell success under the same session always expected when needs_direct_runtime_enforcement stays false, while only direct-FS tools fail?

Please correct any of the above — especially if the team already treats Windows :slash_tmp as intentional or has an in-flight elevated/unelevated redesign.

Happy to refine this analysis with more pointers or a tighter repro matrix. If the team later wants an invited PR on an agreed approach, I’d be glad to help; no unsolicited PR from me.

bbingz · 1 month ago

Analysis (community)

Thank you for the careful report — the process ancestry, exact config, and writable-root enumeration make this very easy to reason about from source.

  • Environment / versions (from the issue): codex-cli 0.145.0, Windows 11 Pro x64 build 26200, PowerShell 7; config only [windows] sandbox = "unelevated"; session markers CODEX_PERMISSION_PROFILE=:workspace and CODEX_SANDBOX_NETWORK_DISABLED=1; process tree under standalone …\OpenAI\Codex\bin\codex.exe (not Desktop UI).
  • Repro status: Not re-run on Windows here (no Windows host for this pass). Treat the OP’s three consecutive apply_patch failures as the primary runtime evidence. What follows is source-level confirmation on current main that the observed error string and failure path match a real policy/backend mismatch for the default CLI profile.
  • Observed vs expected:
  • Observed: apply_patch fails before reading the target with

windows unelevated restricted-token sandbox cannot enforce split writable root sets directly; refusing to run unsandboxed
after failed to prepare fs sandbox / failed to prepare windows sandbox wrapper. Effective writable set includes workspace + C:\tmp + :tmpdir + :slash_tmp. Ordinary sandboxed shell still works.

  • Expected: A built-in profile the CLI advertises (:workspace) should resolve to a policy the selected Windows backend can enforce, without weakening the correct refuse-unsandboxed behavior.

Root-cause hypothesis

High confidence that this is a default profile shape vs unelevated overlay checker mismatch, not a bad patch or NTFS issue.

  1. Built-in :workspace always grants temp specials.

PermissionProfile::workspace_write() (codex-rs/protocol/src/models.rs) calls workspace_write_with(..., exclude_tmpdir_env_var=false, exclude_slash_tmp=false).
FileSystemSandboxPolicy::workspace_write (codex-rs/protocol/src/permissions.rs) therefore adds Write on :slash_tmp and :tmpdir (plus project roots and the usual .git / .agents / .codex carveouts).

  1. :slash_tmp resolves to /tmp whenever that path is a directory.

resolve_file_system_special_path(SlashTmp) builds AbsolutePathBuf("/tmp") and includes it if is_dir(). On Windows that commonly surfaces as C:\tmp, matching the issue’s writable-root list.

  1. apply_patch hits the unelevated restricted-token overlay path.

FS helper prep ends in resolve_windows_restricted_token_filesystem_overrides (codex-rs/sandboxing/src/windows.rs). With WindowsSandboxLevel::RestrictedToken / unelevated, that function compares:

  • legacy roots from the compatibility SandboxPolicy projection, vs
  • split roots from FileSystemSandboxPolicy::get_writable_roots_with_cwd.

If the two BTreeSets differ, it hard-errors with the exact string above and sets write_roots_override: None (unelevated only layers extra deny-write carveouts).

  1. On Windows those sets diverge for the built-in default.

Legacy SandboxPolicy::get_writable_roots_with_cwd (codex-rs/protocol/src/protocol.rs) only adds /tmp under cfg!(unix) && !exclude_slash_tmp.
Split policy still resolves :slash_tmp whenever /tmp exists as a dir — platform-independent.
So with C:\tmp present, split includes an extra root the legacy projection never had → equality check fails → refuse unsandboxed (correct security posture).

  1. The Windows runtime already treats temp differently than the overlay checker.

ResolvedWindowsSandboxPermissions::writable_roots_for_cwd (codex-rs/windows-sandbox-rs/src/resolved_permissions.rs) strips SlashTmp/Tmpdir specials and re-adds temp via TEMP/TMP. Elevated prep can emit write_roots_override when sets diverge; unelevated does not. Existing unit coverage (windows_restricted_token_allows_workspace_write_profiles in codex-rs/core/src/exec_tests.rs) only asserts unelevated success when temps are excluded (exclude_tmpdir_env_var=true, exclude_slash_tmp=true) — i.e. not the built-in default.

  1. Why shell can still look fine: paths that never require direct runtime enforcement / FS-wrapper prep can avoid this equality check; apply_patch verification/update does not.

Related class (same error surface, different entry points): #30712, #31220, #32314, #33806, #34970. #34088 closed after switching to elevated — a workable user workaround, not a code fix for the default CLI + unelevated combination.

High-level fix outline (not a PR)

Prefer a small, policy + comparison alignment change over expanding unelevated capability first:

  1. Stop advertising Unix :slash_tmp as a Windows writable special for the built-in :workspace profile (or default exclude_slash_tmp on Windows / when the backend is unelevated restricted-token). Keep temp writability via the existing TEMP/TMP path in resolved_permissions.
  2. In resolve_windows_restricted_token_filesystem_overrides, compare writable roots using the same Windows-normalized root set the runtime already builds (strip SlashTmp/Tmpdir, remap temp) so protocol specials do not invent a second incompatible root set for the equality check alone.
  3. Optional UX: early session/codex doctor diagnostic when unelevated + multi-root profile is still unenforceable for the apply_patch / direct-FS path — today doctor does not probe create/update/delete under the selected backend.

Alternatives considered (heavier or riskier):

  • Teach unelevated to accept write_roots_override like elevated (real capability/security review).
  • Force elevated for apply_patch only (inconsistent; hits elevated perf/setup issues from #32314 / #31220).

Non-goals / do not weaken:

  • Do not remove refuse-to-run-unsandboxed.
  • Do not drop .git / .agents / .codex carveouts.
  • Do not paper over Desktop-only root injection (#30712 / #34970) without also fixing the CLI default path shown here.

Happy to drop any of (1)–(3) if the team already has a preferred backend-side direction.

Test ideas

What should fail before / pass after if the team (or an invitee) implements:

  • Unit (Windows target): PermissionProfile::workspace_write() with default temps included + WindowsSandboxLevel::RestrictedToken + a layout where AbsolutePathBuf("/tmp") exists as a directory → unsupported_windows_restricted_token_sandbox_reason is None and resolve_windows_restricted_token_filesystem_overrides returns Ok(_), not the split-root error.
  • Extend windows_restricted_token_allows_workspace_write_profiles to cover built-in defaults, not only exclude_* = true.
  • Regression: unelevated + :workspace apply_patch create / update / delete of a disposable workspace file succeeds with no unsandboxed fallback; write outside workspace still denied.
  • Optional: doctor (or session startup) probe that exercises the same FS-wrapper path so this class is visible without a full agent turn.

Questions for maintainers

  1. Is the durable fix intended in the Windows default profile shape (exclude / stop advertising :slash_tmp, align temp to TEMP/TMP), or in the unelevated backend (multi-root write_roots_override)?
  2. Is C:\tmp required to repro, or do other multi-root cases (extra --add-dir, plugins, Desktop viz roots as in #34970) hit the same equality check without it?
  3. Should codex doctor grow an apply_patch create/update probe under the selected Windows backend?
  4. For triage: is shell success under the same session always expected when needs_direct_runtime_enforcement stays false, while only direct-FS tools fail?

Please correct any of the above — especially if the team already treats Windows :slash_tmp as intentional or has an in-flight elevated/unelevated redesign.

Happy to refine this analysis with more pointers or a tighter repro matrix. If the team later wants an invited PR on an agreed approach, I’d be glad to help; no unsolicited PR from me.

bbingz · 1 month ago

Building on the root-cause analysis above with commit archaeology and a mechanism refinement for why /tmp becomes C:\tmp on Windows. Verified against main as of 5dd992a (2026-07-24).

Commit archaeology - where the platform divergence came from. The two divergent code paths were introduced at different times:

  • The legacy cfg!(unix) gate in protocol.rs:1211 is very old: git log -S "cfg!(unix)" -- codex-rs/protocol/src/protocol.rs returns d26224472 / #2355 (2025-08-15, "fix: introduce codex-protocol crate").
  • The split-policy platform-independent SlashTmp resolver in permissions.rs came with the permission-profile system: git log -S "SlashTmp" -- codex-rs/protocol/src/permissions.rs returns #13434 (2026-03-06, "config: add initial support for the new permission profile config language"), #14174 (2026-03-12, "centralize filesystem permissions precedence"), and #19772 (2026-04-27, "permissions: derive config defaults as profiles").

So the divergence emerged when the split policy added a platform-independent SlashTmp alongside the older cfg!(unix) legacy projection. On Windows the two disagree, and resolve_windows_restricted_token_filesystem_overrides (windows.rs:137-158) hard-errors when the legacy vs split root BTreeSets differ. (Full history - not a shallow clone - shows these introducers. The policy-shape divergence is not a tip-of-main regression - it dates to the 2026-03 permission-profile work; the hard-error equality check is from #14172 (2026-03-24, restricted-token split carveouts), and it began to surface on direct-FS/apply_patch after #28359 (2026-06-17) ran the fs-helper through the Windows sandbox wrapper (also extracted into codex-rs/sandboxing/src/windows.rs) - not from the elevated-sandbox #14568.)

Mechanism refinement - why /tmp becomes C:\tmp. The SlashTmp resolver is at permissions.rs:1525-1531: FileSystemSpecialPath::SlashTmp => { let slash_tmp = AbsolutePathBuf::from_absolute_path("/tmp").expect("/tmp is absolute"); if !slash_tmp.as_path().is_dir() { return None; } Some(slash_tmp) } (the .expect is at :1527). AbsolutePathBuf::from_absolute_path("/tmp") (codex-rs/utils/absolute-path/src/lib.rs:58-61) calls absolutize::absolutize (absolutize.rs:14); since /tmp is not is_absolute() on Windows (it has a root but no drive prefix), absolutize falls back to absolutize_from(path, std::env::current_dir()?) (absolutize.rs:19), and the Windows path_with_base (absolutize.rs:57-59, #[cfg(windows)]) joins /tmp onto that base because path.has_root() - yielding e.g. C:\tmp (the drive letter comes from the process's current directory; see the test windows_root_relative_path_uses_base_prefix at absolutize.rs:156). The .expect("/tmp is absolute") does not panic because absolutize succeeds. Caveat: this is source-level Windows path semantics, not a Windows runtime re-run on my side; the OP's list shows C:\tmp.

Not fixed as of 5dd992a. All three divergent lines are unchanged; the most recent touches to these files through that tip (e.g. #34469, #34598, #34816, #34845, #34930, #35020, #35063) are unrelated to the SlashTmp platform divergence.

Test gap on the direct resolver. resolve_windows_restricted_token_filesystem_overrides has two direct callers in exec_tests.rs: inside windows_restricted_token_supports_full_read_split_write_read_carveouts (call at :661, fn at :620) and windows_restricted_token_rejects_unreadable_split_carveouts (call at :714, fn at :678). Both use custom FileSystemSandboxPolicy::restricted(...) profiles (project-roots write plus extra read or deny carveouts), not the built-in :workspace profile. No unit test hits resolve_windows_restricted_token_filesystem_overrides / the legacy-vs-split equality check with built-in :workspace defaults (temps included), leaving the equality-check divergence uncovered. A failing test would build a :workspace profile on Windows and assert the legacy vs split root sets agree (or that the restricted-token overlay does not hard-error on the resolved temps path).

toyfrog · 1 month ago

Reversible local mitigation experiment and rollback

Until the product defect is resolved, I plan to test whether narrowing the stock workspace temporary roots restores apply_patch on native Windows. This is an experiment, not a proposed product fix.

Before any change:

  1. Copy %USERPROFILE%\.codex\config.toml to a timestamped adjacent backup.
  2. Record the original and backup SHA-256 values and the exact configuration diff.
  3. Capture the effective writable roots from a fresh baseline session.

First phase:

  1. Set sandbox_workspace_write.exclude_slash_tmp = true without changing the :tmpdir policy.
  2. Start a fresh session.
  3. Verify that the repository remains writable, existing C:\tmp content remains readable, a new C:\tmp write is denied or requires approval, and a normal %TEMP% temporary-file operation succeeds.
  4. Record the apply_patch result verbatim. This phase is not expected to remove the split root by itself because :tmpdir remains writable.

Any experiment that also excludes or relocates :tmpdir will be reviewed separately. It must use only process-scoped or repository-scoped temporary storage and must not persistently rewrite Windows user-level or machine-level TEMP or TMP variables.

Rollback:

  1. End the experimental Codex session.
  2. Restore %USERPROFILE%\.codex\config.toml from the recorded backup.
  3. Start a fresh session.
  4. Verify that the restored configuration hash matches the baseline and that the stock workspace, :slash_tmp, and :tmpdir writable-root set has returned.

The experiment will not delete, move, or repurpose C:\tmp. The rollback therefore consists only of restoring one known configuration backup and starting a fresh session.

toyfrog · 1 month ago

Local mitigation implementation baseline

Owner authorization was given to complete the split-root workaround rather than stop after the diagnostic first phase.

Pre-change evidence:

  • Codex CLI: 0.145.0
  • User config: %USERPROFILE%\.codex\config.toml
  • Baseline SHA-256: 31D6EB92E359E15A8EFCA9F7AF050DF90C528075B1532D4209CBCEC9F85400FA
  • Rollback copy: %USERPROFILE%\.codex\config.toml.backup-20260724T155555Z
  • Rollback-copy SHA-256: 31D6EB92E359E15A8EFCA9F7AF050DF90C528075B1532D4209CBCEC9F85400FA
  • Persistent Windows user and machine TEMP/TMP values are unchanged.

Exact planned configuration diff:

+[sandbox_workspace_write]
+exclude_slash_tmp = true
+exclude_tmpdir_env_var = true

Both exclusions are required for this mitigation because retaining :tmpdir would retain a disjoint writable root. The next evidence comment will record the resulting configuration hash, effective fresh-session permissions, an apply_patch create/update/delete probe, standard Windows temporary-file behavior, and rollback readiness.

toyfrog · 1 month ago

Local mitigation implemented and verified

Final configuration:

[sandbox_workspace_write]
exclude_slash_tmp = true
exclude_tmpdir_env_var = true

Evidence:

  • Updated config SHA-256: 8D7D97ED539559EA8F3FFA2AEA61C67FF9B02BDD53BB23AB8E08325515756B7E
  • Rollback copy: %USERPROFILE%\.codex\config.toml.backup-20260724T155555Z
  • Rollback SHA-256: 31D6EB92E359E15A8EFCA9F7AF050DF90C528075B1532D4209CBCEC9F85400FA
  • codex exec --ephemeral --strict-config --sandbox workspace-write started successfully with approvals disabled, proving the installed CLI accepted the configuration.
  • The fresh process emitted completed internal file_change events for creating, updating, and deleting a disposable workspace file. The original split-root apply_patch error did not recur.
  • The final probe workspace contained zero files.
  • Default %TEMP% and direct C:\tmp write attempts were rejected by policy; neither external file was created.
  • codex sandbox -P :workspace successfully ran PowerShell and Git reads.
  • Under that same profile, with UV_CACHE_DIR, TEMP, and TMP scoped to the repository for the command, uv run pytest tests/test_requirements_register.py -q completed: 15 passed.
  • All disposable probe directories were removed after verification.
  • Persistent Windows process, user, and machine TEMP/TMP configuration was not changed.

Operational consequence: new Codex sessions now receive a workspace-only writable-root shape, which restores internal apply_patch. Sandboxed tools that require temporary writes must use a workspace-local temporary directory or request explicit escalation; the normal external Windows temp location is no longer automatically writable.

A pre-existing .pytest_cache ACL produced one cache-write warning during the focused test, and the pre-existing .codex-tmp/ tree remains unmodified. Neither was created by this mitigation.

Exact rollback command:

Copy-Item -LiteralPath "$env:USERPROFILE\.codex\config.toml.backup-20260724T155555Z" -Destination "$env:USERPROFILE\.codex\config.toml" -Force

After rollback, start a fresh Codex process and verify that the original SHA-256 and stock temporary writable roots return. Existing sessions retain the permission profile loaded at their own startup, so this mitigation likewise requires a fresh process before use.

toyfrog · 1 month ago

Follow-on: disable login-shell profile loading under Constrained Language

After restarting into the workspace-only writable-root mitigation, shell commands began emitting this startup error:

Microsoft.PowerShell_profile.ps1: Cannot dot-source this command because it was defined in a different language mode.

Diagnosis:

  • The restricted-token shell reports ConstrainedLanguage.
  • A login-shell invocation reproduces the profile error while the requested command itself can still exit 0.
  • The same command with profile loading disabled produces no profile error.
  • %USERPROFILE%\Documents\PowerShell\Microsoft.PowerShell_profile.ps1 contains only conditional VS Code shell-integration initialization, which Codex CLI commands do not require.

Owner-approved configuration diff:

+allow_login_shell = false

The key will be inserted at the top level of %USERPROFILE%\.codex\config.toml; the existing workspace-root exclusions remain unchanged.

Pre-change evidence:

  • Current config SHA-256: 8D7D97ED539559EA8F3FFA2AEA61C67FF9B02BDD53BB23AB8E08325515756B7E
  • Second-stage rollback copy: %USERPROFILE%\.codex\config.toml.backup-20260724T165233Z-pre-no-login
  • Rollback-copy SHA-256: 8D7D97ED539559EA8F3FFA2AEA61C67FF9B02BDD53BB23AB8E08325515756B7E

Partial rollback for only this follow-on change is: restore the second-stage backup and start a fresh Codex process. The earlier full rollback remains available separately.

toyfrog · 1 month ago

Follow-on no-login fix implemented and verified

Final added setting:

allow_login_shell = false

Evidence:

  • Updated config SHA-256: AFE00F2DDFFC10118698D2D9E40454D33AA751674BE26DF24EDB9F0A367DDD45
  • The setting is top-level at line 7; the workspace temporary-root exclusions remain unchanged.
  • A fresh codex exec --ephemeral --strict-config --sandbox workspace-write process invoked the shell with the login parameter omitted.
  • Codex resolved that invocation to pwsh.exe -NoProfile.
  • The command exited 0 and printed:

``text
LOGIN_PROFILE_PROBE_OK
LanguageMode=ConstrainedLanguage
``

  • No Microsoft.PowerShell_profile.ps1 error appeared.

Partial rollback for only this follow-on change:

Copy-Item -LiteralPath "$env:USERPROFILE\.codex\config.toml.backup-20260724T165233Z-pre-no-login" -Destination "$env:USERPROFILE\.codex\config.toml" -Force

Start a fresh Codex process after rollback. The currently running parent conversation retains the configuration loaded at its startup, so its commands must continue to request non-login execution explicitly until that process is restarted.