Windows sandbox: SSH-dependency filtering misses ~/.ssh symlinked into the workspace → machine-wide "Bad owner or permissions"
Context
The experimental Windows sandbox makes a workspace writable by stamping an inheritable Write ACE (for a synthetic per-workspace SID, persisted in cap_sid) on the workspace root. Windows OpenSSH strictly refuses any .ssh/config or key file that a non-owner principal can write — so Codex already goes to real lengths to keep its ACEs off SSH-critical files: #18443 excludes .ssh when granting under USERPROFILE, and #18493 parses the user's ssh config (recursively, through Includes) and filters any writable root that would cover config, keys, or known-hosts.
There's a case that filtering misses: ~/.ssh being a symlink into the workspace itself — a standard dotfiles setup (%USERPROFILE%\.ssh → <dotfiles repo>\openssh\.ssh, kept in git).
Repro
%USERPROFILE%\.sshis a symlink to a directory insideC:\code\dotfiles.experimental_windows_sandbox = true; run Codex with cwdC:\code\dotfilesin workspace-write.- The sandbox stamps its inheritable ACE on
C:\code\dotfiles; it propagates down to the real.ssh\config. - Every consumer of
C:\Windows\System32\OpenSSH\ssh.exeon the machine now fails:
Bad permissions. Try removing permissions for user: UNKNOWN\UNKNOWN (S-1-5-21-…) on file C:/Users/<user>/.ssh/config.
Bad owner or permissions on C:\Users\<user>/.ssh/config
Removing the ACE doesn't stick: the SID is persistent by design, and the next sandboxed run in that cwd re-stamps the root.
The failure is also hard to attribute: it surfaces later, in unrelated tools (git push, IDE ssh integrations, agent-based commit signing), with an error naming an unresolvable SID that nothing connects back to Codex — so an affected user can spend considerable time debugging ssh, ACLs, or their dotfiles before the sandbox comes into view.
Why the filter misses it
filter_ssh_config_dependency_roots (setup.rs) only drops a root when the root is a direct child of USERPROFILE (user_profile_child_name). Here the granted root is C:\code\dotfiles — not under USERPROFILE — and the dependency paths from ssh_config_dependency_paths keep their %USERPROFILE%\.ssh\... spellings, never canonicalized through the symlink, so their containment in the granted root is invisible. (visit_config does canonicalize, but only for its visited dedup key, not the returned paths.)
Confirmed on codex-cli 0.142.3; the gap is still present in current main.
Suggested fix
Both halves already exist in the codebase: canonicalize each ssh_config_dependency_paths result, then containment-test against every candidate write root (workspace_write_root_contains_path). On a hit, silently dropping the root would be rough — a dotfiles repo as cwd would lose its entire writable workspace — so a loud warning naming the resolved path, or protecting the resolved .ssh directory's DACL, seems better. Any of these beats silent machine-wide ssh breakage.
Workaround (for anyone bitten)
Break inheritance on the resolved .ssh directory with a clean explicit set (SYSTEM + Administrators FullControl, owner FullControl, Users RX). That immunizes the ssh-checked files against the workspace-root ACE while leaving the sandbox's ACE functional.
---
Disclosure: this issue was investigated and written by Claude (Fable 5, Anthropic) working with the account owner, who reviewed it before posting.
This issue has 1 comment on GitHub. Read the full discussion on GitHub ↗