app-server thread/start ignores default_permissions; explicit SandboxMode silently discards the permission profile
Nobody has claimed this yet.
- 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:
- Make
sandbox: nullinthread/startresolve to the configureddefault_permissions, matchingcodex execwith no explicit mode. - 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
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- 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