openai / openai/codex

app-server thread/start ignores default_permissions; explicit SandboxMode silently discards the permission profile

Open
#46,252 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

app-server bug CLI sandbox
Dominant language
Rust
Stars
125k
Forks
19.4k
PR merge metrics
PR metrics pending

Description

What version of Codex CLI is running?

codex-cli 0.154.0 (npm, running under Node 24.16.0)

What subscription do you have?

Business

What platform is your computer?

macOS 14.7.5, arm64 (Seatbelt sandbox)

What issue are you seeing?

A project-level .codex/config.toml that selects a permission profile via default_permissions works correctly through codex exec, but there is no way to get the same behavior through the app-server thread/start path. Passing sandbox: null does not fall back to the configured profile, and ThreadStartParams exposes no field for selecting one.

Separately, passing an explicit SandboxMode to codex exec silently discards the profile rather than composing with it or warning.

This makes permission profiles unusable for any client built on the app-server protocol.

Config used

.codex/config.toml in the project root:

default_permissions = "dev-local"

[features]
network_proxy = true

[permissions.dev-local]
extends = ":workspace"

[permissions.dev-local.network]
enabled = true
allow_local_binding = true

[permissions.dev-local.network.domains]
"localhost" = "allow"
"127.0.0.1" = "allow"
"rubygems.org" = "allow"
"index.rubygems.org" = "allow"
What steps can reproduce the bug?

Probe used throughout (connects to a local Postgres on 5432):

PROBE='ruby -rsocket -e "begin; Socket.tcp(%q(127.0.0.1), 5432, connect_timeout: 2) { }; puts %q(CONNECTS); rescue => e; puts e.class; end"'

1. codex exec with no explicit sandbox, profile is honored (expected):

codex exec "Run exactly this and show the raw output: $PROBE"
# -> CONNECTS

2. codex exec with an explicit sandbox mode, profile is silently discarded:

codex exec --sandbox workspace-write "Run exactly this and show the raw output: $PROBE"
# -> Errno::EPERM

Adding -c sandbox_workspace_write.network_access=true does not help for raw TCP. It does activate the HTTP proxy — the error for an external domain changes from Could not resolve host to domain is not on the allowlist for the current sandbox mode, but connect() to a local port still returns EPERM.

3. app-server thread/start no way to reach the behavior from (1).

Using a client that speaks the app-server protocol, all of the following still yield EPERM:

Attempt Result
sandbox: "workspace-write" Errno::EPERM
sandbox: null Errno::EPERM
sandbox: null + config: { default_permissions: "dev-local" } in ThreadStartParams Errno::EPERM
sandbox: null + spawning the server as codex app-server -c 'default_permissions="dev-local"' Errno::EPERM
all of the above with the shared broker disabled, so the process is spawned fresh with cwd = project root Errno::EPERM
What this is not

Not credentials, and not the service. The denial happens at the connect() syscall. Probing a port where nothing is listening discriminates the two cases:

Destination Inside sandbox Outside sandbox
127.0.0.1:1 (nothing listening) Errno::EPERM (errno 1) Errno::ECONNREFUSED (errno 61)
127.0.0.1:5432 (Postgres) Errno::EPERM connects
127.0.0.1:6379 (Redis) Errno::EPERM connects

A closed port also returns EPERM inside the sandbox, so the syscall is being denied wholesale before any destination-specific behavior.

Not approvalPolicy. The app-server client sends approvalPolicy: "never", and ApprovalsReviewer in the schema mentions that sandbox escapes and blocked network access are routed as approval requests, so this looked like a likely cause. It is not:

codex exec -c 'approval_policy="never"' "Run exactly this and show the raw output: $PROBE"
# -> CONNECTS

Not the broker or a stale daemon. Reproduced with the broker disabled and with the daemon killed beforehand so a fresh process spawns with cwd set to the project root.

Schema observation

From codex app-server generate-json-schema, v2/ThreadStartParams.json has these top-level fields:

approvalPolicy, approvalsReviewer, baseInstructions, config, cwd, developerInstructions, 
ephemeral, model, modelProvider, personality, sandbox, serviceName, serviceTier, 
sessionStartSource, threadSource

sandbox is SandboxMode | null where SandboxMode is the enum read-only | workspace-write | danger-full-access. There is no field for selecting a permission profile, and null does not resolve to default_permissions.

What is the expected behavior?

Any app-server client is limited to the three legacy sandbox modes. For a project that needs a local database to run its test suite, the only mode that works is danger-full-access, which disables isolation entirely, far broader than the narrowly scoped profile the user configured and which already works under codex exec.

Additional information

Either of these would unblock the use case:

  1. Make sandbox: null in thread/start resolve to the configured default_permissions, matching codex exec with no explicit mode.
  2. Add an explicit permission-profile field to ThreadStartParams.

Independently, it would help if passing an explicit SandboxMode alongside a configured profile warned rather than silently discarding the profile. That divergence is what made this hard to diagnose.

Not sure if this could be related to the following other issues:

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start by comparing the permission-resolution paths for codex exec and app-server thread/start, using the reported ThreadStartParams schema and the sandbox/profile configuration described here. Trace how sandbox: null, an explicit SandboxMode, and default_permissions are handled. Done means app-server clients can select or inherit the configured profile and explicit sandbox/profile conflicts no longer fail silently.

Written by the indexing model from the issue text.

Assessment

Tech stack
rust
Domain
api, security
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.