[macOS][Codex App][File Provider] Restored OneDrive workspaces get System Policy EPERM after cold relaunch despite valid grants

Open 💬 2 comments Opened Jul 13, 2026 by dianshili

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

26.707.62119 (5211); bundled codex-cli 0.144.2

What subscription do you have?

ChatGPT Pro

What platform is your computer?

macOS Tahoe 26.5.2 (25F84); Darwin 25.5.0 arm64 arm; Apple Silicon

What issue are you seeing?

Summary

After signing out, fully quitting, and cold-relaunching Codex Desktop (installed here as ChatGPT.app; bundle com.openai.codex), previously selected OneDrive/File Provider workspaces can become unreadable to restored background Git workers and to later shell, Git, Node.js, and Python processes.

The paths still exist and metadata checks may succeed, but real directory access fails with EPERM / Operation not permitted even though Full Disk Access and the OneDrive File Provider domain grant for com.openai.codex remain allowed.

Representative errors:

Operation not permitted
EPERM: operation not permitted, scandir
PermissionError: [Errno 1] Operation not permitted
fatal: Unable to read current working directory: Operation not permitted

This is best classified as a Codex Desktop × macOS OneDrive File Provider interoperability defect. The evidence points primarily to Desktop workspace-lifecycle and child-process spawning behavior, but a provider-side interaction has not been eliminated.

The account switch is not the direct permission cause: on the reproduced cold launch, the first Git denial occurred about 69.4 seconds before authentication of the newly selected account completed. In this reproduction, the logout/relogin workflow was accompanied by a full quit and cold relaunch. The evidence implicates that process-lifecycle transition, not the identity of the newly authenticated account.

Impact

Once the state occurs, every task rooted in the affected OneDrive workspace is blocked. This includes workspace initialization, Git discovery, pwd/ls, Git, Node.js fs.readdir*, Python os.listdir, and tools whose startup working directory is inside the workspace.

Because the Desktop app restores multiple saved workspace roots, a single cold launch can generate a large burst of background Git denials across several workspaces before the user opens a task.

Established observations

  • The selected workspace is a direct workspace under a OneDrive File Provider root, not merely an indirect writable_roots entry.
  • Full Disk Access for com.openai.codex remained allowed.
  • The TCC FileProviderDomain grant for com.openai.codex and com.microsoft.OneDrive-mac.FileProvider/OneDrive remained allowed.
  • The installed app's bundle/code-signing identity matched the client identity recorded in those grants.
  • The relevant TCC rows were not revoked or changed during the reproduction window.
  • The app is not App Sandbox-enabled; the failure is still reported as a macOS System Policy denial.
  • ls, Git, Node.js, and Python all fail against the same OneDrive directory.
  • Equivalent probes succeed in /private/tmp and a normal local directory. An iCloud Drive control path also succeeded on this machine.
  • A probe launched outside the Codex workspace Seatbelt profile still fails against the OneDrive path. The Codex workspace sandbox is therefore not a sufficient explanation.
  • The failure persists when OneDrive and its File Provider extension are running.
  • A prior OneDrive update/provider restart temporarily coincided with recovery in an already-running Desktop process. The mechanism of that temporary recovery is unknown; a later cold-relaunch failure did not recover after an ordinary Desktop restart.
  • The kernel emits System Policy deny(1) file-read-data, which makes POSIX mode bits/ACLs an insufficient or unlikely explanation.

Normalized from the sanitized unified log:

(Sandbox) System Policy: git(<pid>) deny(1) file-read-data \
/Users/<user>/Library/CloudStorage/OneDrive-<tenant>/<test-workspace>/<redacted>

Apple's macOS filesystem guidance explains that EPERM can reflect sandbox/MAC enforcement and that MAC attribution depends on identifying the responsible code; unusual child behavior such as daemonization can break that relationship:

https://developer.apple.com/forums/thread/678819

Relative causal timeline

T+0 is the cold Desktop launch after logout and full application exit.

| Relative time | Observed event |
|---|---|
| T+0.000s | LaunchServices starts the new Desktop process |
| T+2.269s | New bundled app-server records its first event |
| T+4.927s | First large burst of Git System Policy deny file-read-data events |
| T+74.346s | Authentication of the newly selected account completes |
| T+77.346s | New local authentication state is written |
| T+118.772s | A later bundled Codex process is denied in the OneDrive tree |
| T+~229s | Read-only shell, Git, Node.js, and Python probes confirm persistent failure |

The ordering is the important point: the initial denial predates new-account authentication by approximately 69.419s.

What steps can reproduce the bug?

Preconditions

  1. Select a directory inside the OneDrive File Provider tree directly as a Codex Desktop workspace.
  2. Keep at least one Git repository in a saved OneDrive workspace.
  3. Confirm that the currently running Desktop process can initially read the workspace.
  4. Confirm that Codex Desktop has its normal macOS file permissions. In this reproduction, both Full Disk Access and the OneDrive File Provider domain grant were allowed.

Steps

  1. Open Codex Desktop with the OneDrive workspace selected.
  2. Sign out of the current ChatGPT account.
  3. Fully quit Codex Desktop and wait for its existing processes to exit.
  4. Cold-launch Codex Desktop.
  5. Temporarily leave authentication incomplete while the app restores saved workspace roots and begins background Git discovery.
  6. Complete authentication.
  7. Open a task rooted in the restored OneDrive workspace.
  8. Run the following read-only probe.
#!/bin/zsh
# Read-only reproduction probe. It performs no writes.

set -u
ROOT="${1:?Usage: $0 /absolute/path/to/workspace}"

run_probe() {
  printf '\n[%s]\n' "$1"
  shift
  "$@"
  local rc=$?
  printf 'exit=%d\n' "$rc"
  return 0
}

run_probe stat /usr/bin/stat -f '%N %Sp' "$ROOT"
run_probe ls /bin/ls -la "$ROOT"
run_probe pwd /bin/zsh -c 'cd "$1" && /bin/pwd -P' _ "$ROOT"
run_probe node node -e \
  'const fs=require("fs"); console.log(fs.readdirSync(process.argv[1]).length)' \
  "$ROOT"
run_probe python python3 -c \
  'import os,sys; print(len(os.listdir(sys.argv[1])))' \
  "$ROOT"
run_probe git /usr/bin/env GIT_OPTIONAL_LOCKS=0 \
  git -c core.fsmonitor=false -C "$ROOT" \
  rev-parse --is-inside-work-tree

Actual result

The metadata-oriented stat may succeed, while cd/pwd, ls, Node.js, Python, and Git fail with EPERM or equivalent Operation not permitted errors.

Control result

The same tool families succeed against /private/tmp and a normal local directory from the same Desktop lifecycle. This is not a general inability of the app to execute those tools.

What is the expected behavior?

A workspace explicitly selected by the user and still authorized for Codex Desktop should remain readable after logout and a cold application restart.

If usable File Provider access cannot be restored, the app should validate access before launching background Git discovery, stop scanning that root after EPERM, and present one actionable “Reauthorize/reselect workspace” flow. It should not repeatedly launch workers against a directory that macOS will deny.

Additional information

Read-only implementation audit

The installed production bundle at /Applications/ChatGPT.app/Contents/Resources/app.asar was inspected read-only. The following are normalized descriptions of generated/minified behavior, not claims about stable source symbols.

Restored workspace lifecycle

The observed Desktop flow persists saved workspace roots as path strings. Folder selection and restoration are effectively handled as:

selected file path -> stat/path resolution -> saved root -> background scan

I did not find an actual directory-enumeration gate before restored-root Git discovery, a File Provider lifecycle revalidation path, or an explicit EPERM recovery flow in this build. A path existing or passing stat() is not proof that a new process lifecycle can enumerate it.

Closed Desktop Git-worker path

The packaged Desktop Git wrapper has a normalized call shape equivalent to:

gitExecutor(command, {
  cwd: workspace,
  killProcessTree: true
});

The local process host maps this on macOS to a spawn equivalent to:

spawn(command, args, {
  cwd: workspace,
  detached: true
});

Node documents that on non-Windows platforms detached: true makes the child the leader of a new process group and session:

https://nodejs.org/api/child_process.html#optionsdetached

This is a plausible Desktop path consistent with the first background Git denial burst. It is not proven to be the unique cause without a same-signature A/B build.

Open-source bundled Codex path

Bundled Codex 0.144.2 contains a separate new-session path for later shell/pipe/snapshot children:

The relevant open-source change was merged in PR #9477:

https://github.com/openai/codex/pull/9477

This is a candidate mechanism for later interactive/non-TTY child failures, not a proven regression. It must not be conflated with the closed Desktop worker that can launch the initial cold-start Git scan before an interactive shell command runs.

Leading hypotheses requiring a signed A/B build

Two mechanisms may be independent or interacting:

  1. Restored File Provider access is stale or not revalidated. The new Desktop lifecycle restores raw paths and begins background work before verifying a real open/readdir from both the parent and worker context.
  2. New-session child isolation changes macOS responsibility attribution. Desktop detached:true and bundled Codex setsid() may prevent File Provider/MAC access from being attributed to the already authorized app.

A OneDrive/File Provider interaction may also participate. The current evidence does not justify assigning sole responsibility to either OpenAI, Microsoft, or macOS at the single-line level.

Requested signed A/B experiment

Please test builds with the same OpenAI signature, bundle identifier, entitlements, and unchanged TCC state:

| Build | Desktop Git worker | Bundled Codex child |
|---|---|---|
| A | current new session (detached:true) | current setsid() |
| B | inherit parent session | current setsid() |
| C | current detached:true | same session/new process group, or no detach for diagnosis |
| D | inherit parent session | same session/new process group, or no detach for diagnosis |

For each build, compare:

  • restored raw path followed immediately by background scanning; and
  • restored path followed by a real parent-and-child directory probe, with an appropriate reauthorization/reselection flow if that probe returns EPERM.

Suggested privacy-preserving instrumentation: build number, random test-run ID, PID/PPID/PGID/session relationship, executable identity, provider class, probe stage, errno, and whether the child session matches the parent. Do not log paths, filenames, directory entries, account names, emails, Git remotes, or document content.

<details>
<summary>Diagnostic A/B sketches — not production patches and not run on the affected Mac</summary>

These sketches are provided only to help OpenAI build a signed causal test. They have not been compiled or executed on the reporter's Mac. They are not user-side workarounds. Modifying/re-signing the installed app would change the code-signing/TCC conditions and invalidate the experiment.

Desktop worker diagnostic flag

 const child = spawn(command, args, {
   cwd,
-  detached: killProcessTree,
+  detached:
+    killProcessTree &&
+    !(
+      process.platform === "darwin" &&
+      featureFlags.fileProviderResponsibleCodeAB === "inherit-session"
+    ),
 });

This deliberately trades away current process-tree cleanup behavior in the diagnostic variant; it is not suitable for release. A production design must preserve cancellation and descendant cleanup, potentially with a signed native/Rust supervisor that uses a new process group without creating a new session, after validating that design with macOS TCC/File Provider.

Bundled Codex diagnostic policy

Conceptually, route all current non-TTY setsid() call sites through an internal policy selected by the signed build:

enum NonTtyIsolation {
    NewSession,      // current setsid() behavior
    NewProcessGroup, // diagnostic: setpgid(0, 0), same session
}

fn isolate_child(mode: NonTtyIsolation) -> std::io::Result<()> {
    match mode {
        NonTtyIsolation::NewSession => detach_from_tty(),
        NonTtyIsolation::NewProcessGroup => set_process_group(),
    }
}

Resolve the flag before entering pre_exec; do not parse environment variables, allocate, log, or take locks inside pre_exec. Apply the policy consistently to spawn.rs, pipe.rs, and shell_snapshot.rs. Preserve tests for both session modes and verify that killpg() terminates children and grandchildren without orphans.

Workspace validation gate

// Illustrative pseudocode.
async function activateRestoredWorkspace(root: string) {
  try {
    await enumerateAtMostOneEntryWithoutLogging(root);
    await processSupervisor.probeDirectoryWithoutLogging(root);
  } catch (error) {
    if (error?.code === "EPERM" || error?.code === "EACCES") {
      suspendBackgroundScans(root);
      markWorkspaceAuthorizationRequired(root);
      presentOneReselectOrReauthorizeAction(root);
      return;
    }
    throw error;
  }

  scheduleGitDiscovery(root);
}

Do not hard-code OneDrive-* path matching. Use the platform mechanism appropriate to this distribution to classify/revalidate File Provider access. Security-scoped bookmarks should not be assumed mandatory for the current non-App-Sandbox build without further platform validation.

Minimum regression assertions

  • Authorized local, OneDrive, and iCloud workspaces survive cold restart and logout/relogin.
  • No Git scan begins until parent and worker directory probes pass.
  • A stopped provider produces a bounded unavailable state, not a retry storm.
  • EPERM produces one actionable reauthorization/reselection prompt.
  • Same-session/new-process-group mode preserves process-tree cancellation with no orphaned helpers.
  • Interactive CLI job control and PTY behavior do not regress.
  • Diagnostic telemetry contains no user paths, filenames, account identifiers, emails, remotes, or directory entries.

</details>

Related report, not a duplicate

Issue #16179 reports a closely related iCloud/File Provider Operation not permitted case despite Full Disk Access:

https://github.com/openai/codex/issues/16179

It is not the same reproduction: #16179 fails for an indirect iCloud writable_roots path while direct-open succeeds; this report concerns a previously selected direct OneDrive workspace that becomes unreadable after restoration in a cold Desktop lifecycle.

Current issue search did not find an exact open OneDrive + EPERM report in openai/codex.

Safety and contact

I do not recommend that users patch app.asar, alter the bundled Codex binary, re-sign the application, reset TCC, or grant Full Disk Access separately to Git/Node/Python. Those actions change the security context under investigation and could create new risks.

I am available to test an OpenAI-signed diagnostic build and provide additional sanitized diagnostics. Please contact me in this issue; if sensitive logs are required, I can use an official private support channel.

View original on GitHub ↗

This issue has 2 comments on GitHub. Read the full discussion on GitHub ↗