[macOS][Sandbox] Read-only permission profile still allows writes to /tmp

Open 💬 2 comments Opened Jul 11, 2026 by mengyanglsy

What version of the Codex App are you using (From “About Codex” dialog)?

0.144.0-alpha.4

What subscription do you have?

ChatGPT Plus

What platform is your computer?

macOS 26.5.1, Apple Silicon (arm64)

What issue are you seeing?

A read-only permission profile explicitly denies temporary-directory writes, but the Codex App can still write to /tmp.

The active permission profile is selected correctly. The effective permission metadata shows:

  • Permission profile type: managed
  • Sandbox: read-only
  • Approval policy: never
  • Repository: read only
  • Network: restricted
  • :tmpdir: deny
  • :slash_tmp: deny
  • /private/tmp: deny
  • /private/var/tmp: deny
  • The real expanded $TMPDIR path: deny

Relevant redacted configuration:

default_permissions = "readonly-reviewer"
approval_policy = "never"

[permissions.readonly-reviewer]
extends = ":read-only"

[permissions.readonly-reviewer.filesystem]
":root" = "deny"
":minimal" = "read"
":tmpdir" = "deny"
":slash_tmp" = "deny"
"/private/tmp" = "deny"
"/private/var/tmp" = "deny"
"/private/var/folders/<redacted>/T" = "deny"
"/Users/<user>/<repository>" = "read"

There is no sandbox_mode or sandbox_workspace_write setting in the configuration.

Steps to reproduce:

touch /tmp/codex-readonly-test
echo $?
test -e /tmp/codex-readonly-test
echo $?

Actual result:

touch exit code: 0
The file exists.
No permission approval was requested.

Control test against the read-only repository:

touch /Users/<user>/<repository>/.readonly-test

Actual result:

touch: Operation not permitted

Other controls also work correctly:

  • Repository writes are rejected.
  • Read-only Git commands work.
  • Local command network access is blocked.
  • Approval policy remains never.

Expected result:

Writing to /tmp, /private/tmp, $TMPDIR, or any explicitly denied temporary path should be rejected with Operation not permitted.

The repository restriction works, but the temporary-directory deny rules are ignored. This allows a task presented as read-only to create or modify files in a shared temporary directory.

What steps can reproduce the bug?

  1. Create and select a read-only permission profile that denies :tmpdir, :slash_tmp, /private/tmp, /private/var/tmp, and the expanded $TMPDIR path.
  1. Set the approval policy to never.
  1. Completely restart the Codex App and confirm that the read-only profile is active.
  1. Run the following command inside a Codex task without requesting or approving additional permissions:

touch /tmp/codex-readonly-test

  1. Check the exit code and whether the file exists:

echo $?
test -e /tmp/codex-readonly-test
echo $?

Actual result:

  • touch exits with code 0.
  • The file exists.
  • No permission approval is requested.

Control test:

touch /Users/<user>/<repository>/.readonly-test

The control test is correctly rejected with:

touch: Operation not permitted

Repository writes are therefore blocked correctly, but the temporary-directory deny rules are not enforced.

What is the expected behavior?

When the active permission profile denies :tmpdir, :slash_tmp, /private/tmp, /private/var/tmp, and the expanded $TMPDIR path, every attempt to create, modify, or delete a file in those locations should be rejected by the macOS sandbox with Operation not permitted.

The command must not create the file and must not silently succeed. The behavior should match the correctly enforced read-only restriction on the repository.

Additional information

The repository write restriction, read-only Git commands, network restriction, and approval_policy = "never" were independently tested and work as expected.

Only temporary-directory protection fails.

The active custom read-only profile was confirmed as selected. The app displays the effective permission profile type as "managed". No additional permission was requested or approved during the reproduction.

No sandbox_mode or sandbox_workspace_write keys are present in config.toml.

View original on GitHub ↗

2 Comments

Echolonius · 1 month ago

Reproduced this by reading the policy-construction source (static analysis — I don't have a Mac to confirm live, so flagging that up front). The behavior looks intended-but-buggy: the temp-dir writes come from a hardcoded defaults section that is appended after the profile's rules, and macOS sandbox-exec policies are last-match-wins.

Where the writes are allowed. codex-rs/sandboxing/src/restricted_read_only_platform_defaults.sbpl contains unconditional write allows:

(allow file-read* file-test-existence file-write* (subpath "/tmp"))
(allow file-read* file-write* (subpath "/private/tmp"))
(allow file-read* file-write* (subpath "/var/tmp"))
(allow file-read* file-write* (subpath "/private/var/tmp"))

Why they win over the profile's deny. In seatbelt.rs::create_seatbelt_command_args the sections are concatenated in this order:

let mut policy_sections = vec![
    MACOS_SEATBELT_BASE_POLICY.to_string(),
    file_read_policy,
    file_write_policy,     // profile-derived writable roots
    deny_read_policy,
    network_policy,
];
if include_platform_defaults {
    policy_sections.push(MACOS_RESTRICTED_READ_ONLY_PLATFORM_DEFAULTS.to_string());
}

The base policy starts (deny default), and Seatbelt applies the last matching rule. A read-only profile enforces "no tmp writes" simply by not adding tmp to the writable roots (so nothing re-allows it) — it never emits a trailing (deny file-write* (subpath "/tmp")). The platform-defaults block is pushed last and re-allows file-write* on all four tmp subpaths, so for any read-only profile that includes platform defaults, the tmp allow is the last matching rule and wins. This matches the report exactly: repo-path writes are denied (no override re-allows them), but tmp writes succeed and no approval is requested.

Why it triggers for this config. FileSystemSandboxPolicy::include_platform_defaults returns true for a Restricted policy without full-disk read that grants read to :minimal — which extends = ":read-only" produces — so this block is included precisely for the read-only-reviewer profile in the report.

Note on scope: the four allows are unconditional (not gated on whether the profile denies tmp), so this isn't specific to an explicit :tmpdir = "deny" — any read-only profile using platform defaults gets writable tmp. The explicit deny lines in the config just make the gap visible; they aren't what's being ignored, because they're not translated into a trailing seatbelt deny.

Fix directions (each independently sufficient):

  • Emit the profile's tmp denials as trailing (deny file-write* (subpath ...)) rules appended after the platform defaults, so an explicit deny always has the last word.
  • Or split the read-only platform defaults so the tmp entries are file-read*/file-test-existence only (matching the read-only intent), and let a separate opt-in add tmp writes where a workspace-write profile actually wants them.
  • Or gate the tmp file-write* allows on the effective policy rather than emitting them unconditionally in the read-only defaults.

The read-only-only variant is the more conservative default: a profile named read-only writing to a shared /tmp is a confused-deputy surprise, even if tmp is low-value, since a task presented as read-only can still drop or alter files another process may consume.

BGMLAI · 1 month ago

If the profile explicitly denies temp locations, a platform default exception must not silently override it. It would help to expose the final compiled sandbox policy, including every implicit writable path and the rule that granted it. A regression should test /tmp, /private/tmp, the resolved Darwin TMPDIR, symlink aliases, and file creation through child processes. When an explicit deny conflicts with a required runtime scratch path, startup should fail with an explanation rather than widening access.