app-server: network denied for compound shell commands but allowed for single commands (0.144.4, macOS Seatbelt)
What happens
On the app-server dispatch path, a shell command gets network access if the script contains exactly one simple command, and is silently denied network (DNS) the moment the script is compound — same thread, same config, seconds apart.
Minimal pair. The only difference is a prepended true; :
/bin/zsh -lc 'curl -s -m 8 -o /dev/null -w "NET:%{http_code} ERR:%{errormsg}" https://api.github.com'
→ NET:200 ERR:
/bin/zsh -lc 'true; curl -s -m 8 -o /dev/null -w "NET:%{http_code} ERR:%{errormsg}" https://api.github.com'
→ NET:000 ERR:Could not resolve host: api.github.com
Same shell, same flags, same URL, same thread, same config.toml. Reproduced repeatedly (single-command turns 4/4 succeeded; compound turns 5/5 failed).
Filesystem writes are denied in the same turns (mktemp -d → Operation not permitted), which is expected for read-only — the network denial is the surprising part, and it is not what config.toml asks for.
Environment
codex-cli 0.144.4- macOS 26.5.2 (Seatbelt)
- Dispatch:
codex app-serverdriven by a JSON-RPC client, thread started withsandbox: "read-only",approvalPolicy: "never" - The cwd is a trusted project (
[projects."…"] trust_level = "trusted"), so this is not the projectless case from #33055
Why it matters
approvalPolicy: "never" removes the escalation path (#25229), so the command does not prompt — it just fails. Worse, the failure misreports itself: gh auth status in an affected turn says "The token in default is invalid", sending you after a credentials bug that does not exist. The real cause is DNS denial.
Concretely: we run automated code review through the app-server. A reviewer that silently cannot run gh pr diff still returns a confident-looking verdict. That is a correctness hazard, not just an inconvenience.
The CLI does not do this
Identical machine, identical moment:
codex exec -s read-only … curl https://api.github.com → 200
app-server (read-only) … curl https://api.github.com → 000
codex exec reached the network in read-only, workspace-write, and default modes. Only the app-server path denies.
Things that do NOT fix it (all verified, in case they save someone time)
sandbox_workspace_write.network_access = true— no effect on 0.144.x (the key belongs to the older sandbox system).- A permissions profile (
default_permissions+[permissions.<name>].network.enabled = true) — no effect on this path. My initial "it works" result was a confounded measurement: I had changed the profile and the command shape at once. Isolating them showed the profile does nothing and the command shape was carrying the entire effect. network.domainsallow/deny — inert without the network proxy running: with"**.github.com" = "allow"and an explicit"example.com" = "deny",example.comstill returned 200, and no proxy env vars reached the sandbox. Percodex-rs/network-proxy/README.mdthe proxy is what enforces the allowlist, and it is acargo run -p codex-network-proxydev component with no shipped user-facing command — so the allowlist is not usable in a released build.- Restarting the daemon — a freshly spawned broker returned 200 then 000 on consecutive tasks. Not staleness.
Methodology note
The codex sandbox subcommand applies a stricter default than the real dispatch path and blocks DNS outright. It is a misleading probe — early runs failed there and passed under codex exec. Measure on the path you actually ship on.
Guess at the mechanism
The command-shape dependence suggests the sandbox/escalation decision is made from a parse of the command: a single recognised simple command takes one route, while anything compound falls back to a stricter default that drops the thread's network policy. #10390 reports the seatbelt sandbox unconditionally setting CODEX_SANDBOX_NETWORK_DISABLED=1, and #21955 reports thread networkAccess persisting as false — both are consistent with the effective policy being re-derived rather than inherited. I have not read the Rust, so treat this as a hypothesis; the minimal pair above is the reliable part.
Related open issues
Same class, none acknowledged: #33055 (0.144.2/macOS, projectless scheduled tasks get read-only + no network), #21976 (automations DNS-dead while interactive works), #25229 (automations pinned to never, cannot escalate), #10390 (macOS network_access = true silently ignored), #10702, #21955.