app-server: network denied for compound shell commands but allowed for single commands (0.144.4, macOS Seatbelt)

Open 💬 0 comments Opened Jul 16, 2026 by wrightbuilt

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 -dOperation 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-server driven by a JSON-RPC client, thread started with sandbox: "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.domains allow/deny — inert without the network proxy running: with "**.github.com" = "allow" and an explicit "example.com" = "deny", example.com still returned 200, and no proxy env vars reached the sandbox. Per codex-rs/network-proxy/README.md the proxy is what enforces the allowlist, and it is a cargo run -p codex-network-proxy dev 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.

View original on GitHub ↗