0.118.0 sandbox write regression on Linux

Resolved 💬 17 comments Opened Apr 1, 2026 by CrypticError Closed Apr 21, 2026
💡 Likely answer: A maintainer (github-actions[bot], contributor) responded on this thread — see the highlighted reply below.

What version of Codex CLI is running?

0.118.0

What subscription do you have?

Pro

Which model were you using?

gpt-5.4

What platform is your computer?

Linux 6.14.0-37-generic x86_64 x86_64

What terminal emulator and version are you using (if applicable)?

Ghostty

What issue are you seeing?

File write's don't work, prompting with
command failed; retry without sandbox?

This does not happen in the same setup under 0.117.0, which I have reverted back to

I am using the following settings in codex config

approval_policy = "on-request"
sandbox_mode = "workspace-write"

What steps can reproduce the bug?

Writing a file? Possibly only diff format, unsure

What is the expected behavior?

For the initial write to work within sandbox

Additional information

_No response_

View original on GitHub ↗

17 Comments

github-actions[bot] contributor · 3 months ago

Potential duplicates detected. Please review them and close your issue if it is a duplicate.

  • #15228
  • #15505
  • #16331

Powered by Codex Action

jasny · 3 months ago

This seems to be apply_patch specific. Write files via sed or other shell commands work normally. For more info see #16407

tomas-langer · 3 months ago

I see exactly the same regression after upgrade to 0.118.0

jabowery · 3 months ago

work around on Ubuntu with apparmor:

$ cat fix_codex_apparmor_v2.sh
#!/usr/bin/env bash
set -euo pipefail

CODEX_PROFILE_FILE="/etc/apparmor.d/codex-native"
BWRAP_PROFILE_FILE="/etc/apparmor.d/codex-bwrap"
TRACE_LOG="/tmp/codex-apparmor-trace.log"
KEEP_TRACE=0
EXPLICIT_CODEX_BIN=""
declare -a EXPLICIT_BWRAP_PATHS=()

action_user() {
  if [[ -n "${SUDO_USER:-}" && "${SUDO_USER}" != "root" ]]; then
    printf '%s' "$SUDO_USER"
  else
    id -un
  fi
}

INVOKING_USER="$(action_user)"

log() {
  printf '[fix_codex_apparmor] %s\n' "$*"
}

warn() {
  printf '[fix_codex_apparmor] WARNING: %s\n' "$*" >&2
}

die() {
  printf '[fix_codex_apparmor] ERROR: %s\n' "$*" >&2
  exit 1
}

usage() {
  cat <<'EOF2'
Usage:
  sudo bash fix_codex_apparmor_complete_v2.sh [--codex-bin PATH] [--bwrap PATH] [--keep-trace]

What it does:
  * Detects the native Codex executable actually used on this machine.
  * Detects the bwrap path(s) Codex executes.
  * Writes targeted AppArmor profiles for the native Codex binary and bwrap.
  * Reloads those profiles and tests `codex sandbox linux /bin/true`.

Options:
  --codex-bin PATH   Use this Codex executable path instead of searching PATH.
  --bwrap PATH       Add a bwrap path explicitly. You may repeat this option.
  --keep-trace       Keep the strace output at /tmp/codex-apparmor-trace.log.
  -h, --help         Show this help.
EOF2
}

parse_args() {
  while (($#)); do
    case "$1" in
      --codex-bin)
        (($# >= 2)) || die "--codex-bin requires a path"
        EXPLICIT_CODEX_BIN="$2"
        shift 2
        ;;
      --bwrap)
        (($# >= 2)) || die "--bwrap requires a path"
        EXPLICIT_BWRAP_PATHS+=("$2")
        shift 2
        ;;
      --keep-trace)
        KEEP_TRACE=1
        shift
        ;;
      -h|--help)
        usage
        exit 0
        ;;
      *)
        die "Unknown argument: $1"
        ;;
    esac
  done
}

require_root() {
  [[ "${EUID}" -eq 0 ]] || die "Run this script with sudo or as root."
}

run_shell_as_invoking_user() {
  local cmd="$1"
  if [[ "$(id -un)" == "$INVOKING_USER" ]]; then
    bash -lc "$cmd"
  else
    sudo -u "$INVOKING_USER" -H bash -lc "$cmd"
  fi
}

run_cmd_as_invoking_user() {
  if [[ "$(id -un)" == "$INVOKING_USER" ]]; then
    "$@"
  else
    sudo -u "$INVOKING_USER" -H -- "$@"
  fi
}

canonicalize_path() {
  local path="$1"
  if [[ -e "$path" ]]; then
    readlink -f "$path"
  else
    printf '%s\n' "$path"
  fi
}

find_codex_wrapper() {
  local wrapper

  if [[ -n "$EXPLICIT_CODEX_BIN" ]]; then
    wrapper="$(canonicalize_path "$EXPLICIT_CODEX_BIN")"
    [[ -x "$wrapper" ]] || die "--codex-bin path is not executable: $wrapper"
    printf '%s\n' "$wrapper"
    return 0
  fi

  wrapper="$(run_shell_as_invoking_user 'command -v codex 2>/dev/null || true')"
  [[ -n "$wrapper" ]] || die "Could not find 'codex' in ${INVOKING_USER}'s PATH. Re-run with --codex-bin /full/path/to/codex."
  canonicalize_path "$wrapper"
}

trace_codex_execs() {
  local wrapper="$1"
  if ! command -v strace >/dev/null 2>&1; then
    warn "strace is not installed; auto-detection will be more limited."
    return 0
  fi

  rm -f "$TRACE_LOG"
  log "Tracing Codex once to discover the actual executable paths it uses"
  if ! run_cmd_as_invoking_user strace -f -e trace=execve -o "$TRACE_LOG" "$wrapper" sandbox linux /bin/true >/dev/null 2>&1; then
    true
  fi
}

extract_execve_paths() {
  [[ -f "$TRACE_LOG" ]] || return 0
  sed -n 's/.*execve("\([^"]*\)".*/\1/p' "$TRACE_LOG"
}

search_common_native_codex_paths() {
  local wrapper="$1"
  local wrapper_dir wrapper_parent npm_root
  wrapper_dir="$(dirname "$wrapper")"
  wrapper_parent="$(dirname "$wrapper_dir")"
  npm_root="$(run_shell_as_invoking_user 'npm root -g 2>/dev/null || true')"

  {
    find "$wrapper_dir" -maxdepth 8 -type f -perm -111 -path '*/vendor/*/codex/codex' 2>/dev/null || true
    find "$wrapper_parent" -maxdepth 8 -type f -perm -111 -path '*/vendor/*/codex/codex' 2>/dev/null || true
    if [[ -n "$npm_root" && -d "$npm_root" ]]; then
      find "$npm_root" -maxdepth 8 -type f -perm -111 -path '*/vendor/*/codex/codex' 2>/dev/null || true
    fi
  } | awk '!seen[$0]++'
}

select_native_codex_path() {
  local wrapper="$1"
  local candidate="" path

  if [[ -n "$EXPLICIT_CODEX_BIN" ]]; then
    candidate="$(canonicalize_path "$EXPLICIT_CODEX_BIN")"
    [[ -x "$candidate" ]] || die "--codex-bin path is not executable: $candidate"
    printf '%s\n' "$candidate"
    return 0
  fi

  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    [[ -x "$path" ]] || continue
    if [[ "$(basename "$path")" == "codex" && "$path" != "$wrapper" && ( "$path" == *"/vendor/"* || "$path" == *"@openai/codex"* || "$path" == *"unknown-linux"* ) ]]; then
      candidate="$path"
    fi
  done < <(extract_execve_paths)

  if [[ -n "$candidate" ]]; then
    printf '%s\n' "$candidate"
    return 0
  fi

  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    [[ -x "$path" ]] || continue
    if [[ "$(basename "$path")" == "codex" && "$path" != "$wrapper" ]]; then
      candidate="$path"
    fi
  done < <(extract_execve_paths)

  if [[ -n "$candidate" ]]; then
    printf '%s\n' "$candidate"
    return 0
  fi

  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    [[ -x "$path" ]] || continue
    candidate="$path"
    break
  done < <(search_common_native_codex_paths "$wrapper")

  if [[ -n "$candidate" ]]; then
    printf '%s\n' "$candidate"
    return 0
  fi

  if [[ -x "$wrapper" && "$(basename "$wrapper")" == "codex" ]]; then
    printf '%s\n' "$wrapper"
    return 0
  fi

  die "Could not determine the native Codex binary path automatically. Re-run with --codex-bin PATH."
}

collect_bwrap_paths() {
  local -a found=()
  local path explicit

  if [[ -x /usr/bin/bwrap ]]; then
    found+=("$(canonicalize_path /usr/bin/bwrap)")
  fi

  while IFS= read -r path; do
    [[ -n "$path" ]] || continue
    [[ -x "$path" ]] || continue
    if [[ "$(basename "$path")" == "bwrap" ]]; then
      found+=("$(canonicalize_path "$path")")
    fi
  done < <(extract_execve_paths)

  for explicit in "${EXPLICIT_BWRAP_PATHS[@]}"; do
    explicit="$(canonicalize_path "$explicit")"
    [[ -x "$explicit" ]] || die "--bwrap path is not executable: $explicit"
    found+=("$explicit")
  done

  printf '%s\n' "${found[@]}" | awk 'NF && !seen[$0]++'
}

write_codex_profile() {
  local codex_bin="$1"
  log "Writing $CODEX_PROFILE_FILE for $codex_bin"
  cat >"$CODEX_PROFILE_FILE" <<EOF2
abi <abi/4.0>,
include <tunables/global>

@{codex_bin} = $codex_bin

profile codex-native @{codex_bin} flags=(unconfined) {
  userns,
  @{codex_bin} mr,

  include if exists <local/codex-native>
}
EOF2
}

write_bwrap_profile() {
  local -a bwrap_paths=("$@")
  local i=0

  log "Writing $BWRAP_PROFILE_FILE"
  {
    printf 'abi <abi/4.0>,\n'
    printf 'include <tunables/global>\n\n'
    for path in "${bwrap_paths[@]}"; do
      ((++i))
      printf '@{bwrap_bin_%d} = %s\n' "$i" "$path"
    done
    printf '\n'
    i=0
    for path in "${bwrap_paths[@]}"; do
      ((++i))
      cat <<EOF2
profile codex-bwrap-$i @{bwrap_bin_$i} flags=(unconfined) {
  userns,
  @{bwrap_bin_$i} mr,

  include if exists <local/bwrap>
}

EOF2
    done
  } >"$BWRAP_PROFILE_FILE"
}

reload_apparmor() {
  command -v apparmor_parser >/dev/null 2>&1 || die "apparmor_parser is not installed."
  log "Reloading AppArmor profiles"
  apparmor_parser -r "$CODEX_PROFILE_FILE"
  if [[ -f "$BWRAP_PROFILE_FILE" ]]; then
    apparmor_parser -r "$BWRAP_PROFILE_FILE"
  fi
}

show_environment() {
  log "AppArmor status"
  if command -v aa-status >/dev/null 2>&1; then
    aa-status --enabled >/dev/null 2>&1 && aa-status --enabled && true || warn "aa-status reports AppArmor is not enabled"
  else
    warn "aa-status is not installed"
  fi

  log "Relevant sysctls"
  sysctl kernel.apparmor_restrict_unprivileged_userns 2>/dev/null || true
  sysctl kernel.apparmor_restrict_unprivileged_unconfined 2>/dev/null || true
}

verify_codex_sandbox() {
  local wrapper="$1"
  log "Testing Codex sandbox as ${INVOKING_USER}: $wrapper sandbox linux /bin/true"
  if run_cmd_as_invoking_user "$wrapper" sandbox linux /bin/true; then
    log "Codex sandbox test succeeded"
  else
    warn "Codex sandbox test still failed"
    warn "If the trace did not reveal the real native binary or bwrap path, re-run with --keep-trace and explicit --codex-bin/--bwrap arguments."
    return 1
  fi
}

print_summary() {
  local wrapper="$1"
  local codex_bin="$2"
  shift 2
  local -a bwrap_paths=("$@")

  cat <<EOF2

Done.

Detected wrapper:
  $wrapper
Detected native Codex binary:
  $codex_bin
Detected bwrap path(s):
$(for p in "${bwrap_paths[@]}"; do printf '  %s\n' "$p"; done)
Installed profiles:
  $CODEX_PROFILE_FILE
$(if [[ -f "$BWRAP_PROFILE_FILE" ]]; then printf "  %s\n" "$BWRAP_PROFILE_FILE"; else printf "  (no bwrap profile written)\n"; fi)

If you want to inspect the generated profiles:
  sudo cat $CODEX_PROFILE_FILE
$(if [[ -f "$BWRAP_PROFILE_FILE" ]]; then printf "  sudo cat %s\n" "$BWRAP_PROFILE_FILE"; fi)
EOF2

  if [[ "$KEEP_TRACE" -eq 1 && -f "$TRACE_LOG" ]]; then
    cat <<EOF2

Trace kept at:
  $TRACE_LOG
EOF2
  fi
}

cleanup() {
  if [[ "$KEEP_TRACE" -ne 1 ]]; then
    rm -f "$TRACE_LOG"
  fi
}

main() {
  parse_args "$@"
  require_root

  local wrapper codex_bin
  local -a bwrap_paths=()

  wrapper="$(find_codex_wrapper)"
  log "Using Codex wrapper: $wrapper"

  trace_codex_execs "$wrapper"
  codex_bin="$(select_native_codex_path "$wrapper")"
  log "Using native Codex binary: $codex_bin"

  mapfile -t bwrap_paths < <(collect_bwrap_paths)

  write_codex_profile "$codex_bin"
  if ((${#bwrap_paths[@]} > 0)); then
    write_bwrap_profile "${bwrap_paths[@]}"
  else
    warn "No bwrap path was detected. Skipping the bwrap profile and installing only the native Codex profile."
    rm -f "$BWRAP_PROFILE_FILE"
  fi
  reload_apparmor
  show_environment
  verify_codex_sandbox "$wrapper"
  print_summary "$wrapper" "$codex_bin" "${bwrap_paths[@]}"
  cleanup
}

main "$@"
Manerba · 3 months ago

Additional signal from a separate Linux repro that appears to be the same regression family.

I opened #16790 with a more specific failure mode.

What is new in that report:

  • same machine / same repo / same day
  • working under 0.117.0
  • failing under 0.118.0
  • approval_policy:"on-request" in both cases
  • plan_type:"pro" in both cases
  • failing session does not just prompt on writes; even basic sandboxed reads fail before command execution

Exact stderr from the failing 0.118.0 sandbox bootstrap:

bwrap: Can't create file at /root/kiara/.codex: Permission denied

This affects even:

  • pwd
  • rg --files ...
  • wc -l ...
  • id
  • env

Extra host context from the repro:

  • /root is 701 root:root
  • /root/kiara is 755 kiara:kiara
  • /root/kiara/.codex does not exist outside the sandbox when inspected

So this looks broader than an apply_patch-only problem. In this repro, 0.118.0 appears to fail in sandbox bootstrap itself when touching first-time project-local .codex, and the approval prompts are downstream of that failure.

Issue with details: #16790

scharalambous3 · 3 months ago

Experiencing a similar regression where all edits fail in sandbox. Not sure if the root cause is the same but I also experienced this as a regression upgrading to v118.0

TheEnolaGay · 3 months ago

Additional reproduction/details from another Linux 0.118.0 setup:

Environment

  • Codex CLI: 0.118.0
  • Platform: Linux x86_64
  • Sandbox mode: workspace-write
  • Approval policy shown in session metadata: on-request

Observed behavior

  • In workspace-write, normal file edits trigger the TUI prompt "Would you like to make the following edits?" / patch approval repeatedly.
  • This still happens after using /approvals and switching back to default.
  • The same workflow works under full access, which strongly suggests the problem is isolated to the restricted/sandboxed approval path rather than repo permissions or the patch itself.

Important distinction

  • I saw two related symptoms:
  1. Earlier sessions had real sandbox write failures (EROFS / Read-only file system) for paths like ~/.codex, ~/.npm, and some .git lock/config writes when writable roots were too narrow.
  2. After widening writable roots and restarting, normal apply_patch-style edits still triggered patch approval in workspace-write even without a corresponding sandbox write error.

Evidence from local logs

  • Session metadata showed approval_policy = on-request and workspace-write sandbox.
  • A normal apply_patch edit was attempted, and the log then showed op.dispatch.patch_approval without an ERROR codex_core::tools::router sandbox failure for that edit.
  • /approvals -> default did not change the behavior.
  • danger-full-access/full access avoids the problem.

This makes it look like there may be both:

  • a 0.118.0 sandbox writable-roots regression on Linux, and
  • a separate or related regression in workspace-write + on-request edit approvals / patch_approval routing.

If useful I can provide the exact log excerpts showing apply_patch followed by patch_approval without a write error.

smerrill · 3 months ago

Confirming this regression has come back on Ubuntu 24.04.

ghosty141 · 3 months ago

Also happens for me n 0.120.0. Edits with sed work. bwrap itself also only worked after using the solution described here: https://github.com/openai/codex/issues/14919#issuecomment-4223539341

tomas-langer · 3 months ago
Also happens for me n 0.120.0. Edits with sed work. bwrap itself also only worked after using the solution described here: #14919 (comment)

Same here: codex-cli 0.120.0
I even tried to add a custom .codex/config.toml in the repository with an explicit writable root, still fails.

Summary from codex itself:

- Expected behavior: apply_patch should edit the file directly with no approval prompt,
    because the file is inside an allowed writable root and no escalated command was requested.
  - Actual behavior: Codex UI prompted: “Would you like to make the following edits? Reason:
    command failed; retry without sandbox?” and offered approval options. The patch was then
    aborted and not applied.
  - Why this is unexpected: apply_patch is not a shell command that should need unsandboxed
    retry, and edits to the same repo were already allowed earlier in the session.

Relevant:
<img width="1125" height="545" alt="Image" src="https://github.com/user-attachments/assets/b575a2c3-ba00-41a5-9965-f2234dd071b8" />

slumtrimpet · 3 months ago

I keep peeking at later versions hoping things are de-sucked and keep having to npm install -g @openai/codex@0.117 and then tell codex to ignore updates until the next version...

In the last several releases, 0.114.0 and 0.117.0 were the only two that were usable on Ubuntu 24.

luispabon · 3 months ago

Confirmed on Ubuntu 25.10 and v0.120.0

staskorz · 3 months ago

Running this resolved the issue for me:
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0

Source: Codex Docs (Sandbox)

Update:

To make this setting survive reboots, run the following:
echo 'kernel.apparmor_restrict_unprivileged_userns=0' | sudo tee /etc/sysctl.d/60-apparmor-namespace.conf

And then run the following to apply:
sudo sysctl --system

danielkec · 3 months ago

Additional 0.121.0 repro that appears to be in the same apply_patch / misleading sandbox-retry family.

Environment

  • Codex CLI: 0.121.0
  • Model shown by /status: gpt-5.4
  • Provider shown by /status: Oracle Code Assist
  • Approval policy: on-request
  • Sandbox mode: workspace-write
  • Later /status also showed: workspace-write with network access

What happened

  • In a normal workspace repo, Codex attempted a trivial apply_patch edit to:

env-config/src/main/java/com/oracle/helidon/oci/envconfig/OciEnvLocationOverrideBlueprint.java

  • Intended change:
  • Optional<String> faultDomain();
  • -> Optional<Integer> faultDomain();
  • The TUI prompted:
Would you like to make the following edits?
Reason: command failed; retry without sandbox?
  • After the prompt, the target file remained unchanged and git diff -- env-config/src/main/java/com/oracle/helidon/oci/envconfig/OciEnvLocationOverrideBlueprint.java was empty.

Evidence from persisted session logs

  • Session ID: 019d964e-7b44-7f52-8126-012751b2835b
  • Main JSONL:

~/.codex/sessions/2026/04/16/rollout-2026-04-16T14-40-19-019d964e-7b44-7f52-8126-012751b2835b.jsonl

  • First attempt turn: 019d9656-5ca0-7993-a55d-c90780e6ee76
  • shows custom_tool_call for apply_patch
  • then custom_tool_call_output = aborted by user after 248.2s
  • Resumed turn: 019d9662-5eea-79e2-bed4-63494ed7890d
  • shows more reads/searches and then another custom_tool_call for apply_patch
  • the persisted log ends at the patch request
  • I could not find a matching failed exec_command_end for that resumed turn that would explain the sandbox retry prompt

Why this looks useful

  • This did not look like a normal writable-roots problem.
  • The approval reason shown to the user was command failed; retry without sandbox?, but in the saved JSONL trace for the resumed turn there was no corresponding failed shell command persisted.
  • That makes this look like either:
  • an apply_patch / patch-approval routing failure, or
  • a stale/generic error reason being surfaced by the TUI for a broader class of failures.

This may be the same regression family as earlier apply_patch approval-loop reports, but the missing failed-command evidence seems worth capturing explicitly.

fjolne · 3 months ago

Same issue on NixOS 25.11 and codex 0.121.0. Resolved by setting sandbox_workspace_write.network_access = false or removing this setting entirely. Diagnostic from codex:

the internal Linux sandbox helper panics because the legacy sandbox policy says network_access = true while the split policy for apply_patch derives network_access = false.
viyatb-oai contributor · 3 months ago

this should be fixed in 0.122.0! please let us know if you still it.

algogrit · 1 month ago

Still facing issues in Ubuntu 24.04 LTS using codex-cli 0.136.0.