Kernel Sandbox/System Policy intermittently denies ALL file access (not just mount syscall) on NFS mounts

I'm seeing a recurring issue on macOS 26.5.2 (build 25F84) where the kernel's Sandbox/System Policy layer intermittently denies file access on NFS mount points from local network servers. Posting here in case anyone recognizes this pattern or has a workaround, and flagging it since I've also filed a Feedback Assistant report (with a live-captured sysdiagnose) for the same issue.

WHAT HAPPENS Two independent NFS mounts to two separate, unrelated servers on my LAN start failing simultaneously with "Operation not permitted." The kernel log shows:

kernel: (Sandbox) System Policy: mount_nfs(PID) deny(1) file-mount /path/to/mount

Critically, it's not limited to the mount syscall - within the same few-second window, System Policy also denies ls, perl, diskutil, and even umount -f on the exact same path, for otherwise unrelated processes. So it looks like a transient, path-scoped kernel decision rather than something specific to NFS or the mount syscall.

It self-heals anywhere from seconds to ~30 minutes later, then recurs - documented 30-80+ occurrences/day via a background watchdog script.

WHAT I'VE RULED OUT

  • Server-side cause: two independent servers on different hardware fail identically at the same instant.
  • Network issue: checked network logs in the same window, no correlated connectivity event.
  • Third-party kext conflict: kextstat shows zero third-party kexts loaded.
  • syspolicyd database corruption: no "ASP: Validation category" signature present.
  • TCC/Full Disk Access: already granted; the denying layer is kernel Sandbox "System Policy," not TCC.

QUESTION Has anyone else run into System Policy denying file-mount/file-read-data/file-unmount on network volume paths intermittently like this? Is there any userland way to inspect or reset whatever internal state drives this decision (I haven't found one - no spctl/tccutil/sysctl lever that touches it)?

Happy to share more log excerpts if useful.

I've also filed a Feedback Assistant report

What was that bug number?

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@" + "apple.com"

Bug number: FB23576006

Happy to attach additional sysdiagnose captures if useful — I have an automated watchdog that grabs one live whenever the deny(1) pattern recurs.

I'm seeing a recurring issue on macOS 26.5.2 (build 25F84) where the kernel's Sandbox/System Policy layer intermittently denies file access on NFS mount points from local network servers.

Did this JUST start happening in 26.5? If not, do you know when it might have started? There was change at that time (r.171126338) which also introduced this error:

2026-07-05 10:52:14.292554+0200 diskutil: (StorageKit) [com.apple.storagekit:general] -[SKManager _diskForPath:isNetworkMount:]: open(<private>) failed: errno=1

I'm not sure how that would then connect to the failure you're seeing, but I think it might be connected to this.

or has a workaround,

So, the one thing I would try is moving the mount point out of "/Users/Shared/" and into a location you more "directly" control- either a new directory in your home directory or a new directory you create within the system hierarchy. Creating your own directory helps shift you "out" of the systems "normal" view, which can reduce the possibility of unrelated activity disrupting "your" work.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Thanks for looking into this.

Re: timing — I can't give you a clean before/after-26.5 comparison. This specific NFS mount setup only went live on 2026-07-03, and I was already on 26.5.1/26.5.2 (25F84) at that point — so I have no data on this exact mount configuration under an earlier OS version. I did run a different mount method (SSHFS/macFUSE) at the same path before that, on earlier OS builds, but it failed differently (kext/Gatekeeper corruption), not with this deny(1) signature — so I can't say this is a regression tied to a specific OS version, only that it's new since I switched mount methods.

Re: the StorageKit/r.171126338 log line — the timestamp you posted (2026-07-05 10:52:14+0200) lands within seconds of a deny(1) cluster I already captured a full sysdiagnose for (10:52:11–10:52:54, attached to FB23576006). I'll dig through that archive specifically for the StorageKit/SKManager entries around r.171126338 and report back what I find.

Re: the workaround — good lead, and it lines up with something I found independently: during that same cluster, two completely independent NFS mounts under /Users/Shared/ (different remote hosts, different networks) were denied simultaneously with an identical signature, and the denial hit every process touching that path (ls, diskutil, umount, python3), not just mount_nfs. That's consistent with /Users/Shared/ itself being the scoped resource rather than something specific to NFS. I'll relocate one of the two mounts outside /Users/Shared/ (e.g. into my home directory) and watch whether it stops seeing deny(1) while the other one (still under /Users/Shared/) keeps failing — that would be a strong isolation test. Will report results either way.

One thing that would help: is there any more context you can share on what r.171126338 actually changed? Knowing whether it touches StorageKit/diskarbitrationd's interaction with mount_nfs specifically vs. something in the Sandbox layer generally would help me target the sysdiagnose search.

Found it — and it's more than a timestamp coincidence.

I extracted system_logs.logarchive from the sysdiagnose I already had (2026-07-05, attached to FB23576006) and searched for the StorageKit/SKManager signature around that timestamp. Your log line is there exactly as shown (10:52:14.292554+0200, PID 28562). But the line immediately preceding it, 16 microseconds earlier, same PID, is this:

10:52:14.292538 kernel: (Sandbox) System Policy: diskutil(28562) deny(1) file-read-data /Users/Shared/Stefan-Hem 10:52:14.292554 diskutil: (StorageKit) [SKManager _diskForPath:isNetworkMount:]: open(<private>) failed: errno=1

Same process, same microsecond, errno=1 = EPERM. This isn't a separate bug that r.171126338 introduced — it's diskutil's own StorageKit code logging the exact kernel Sandbox denial I've been reporting, at the moment diskutil itself got caught in a deny(1) window. So r.171126338 (whatever it changed) likely just added visibility into an open() call that was already going to fail — it didn't cause the failure.

For context, here's the full sequence from that same ~7-second window, all against /Users/Shared/Stefan-Hem, all denied identically:

10:52:11.066 Python(28544) deny(1) file-read-data (my own watchdog script)

10:52:11.094 ls(28547) deny(1) file-read-data 10:52:13.109 ls(28548) deny(1) file-read-data 10:52:14.254 perl(28557) deny(1) file-read-data 10:52:14.263 perl(28560) deny(1) file-read-data 10:52:14.268 umount(28561) deny(1) file-unmount 10:52:14.292 diskutil(28562) deny(1) file-read-data ← your StorageKit line follows immediately 10:52:18.007 mount_nfs(28569) deny(1) file-mount

Six unrelated processes/binaries denied within 7 seconds, all on the same path, all with identical Sandbox System Policy signature. That's what convinced me this is path-scoped rather than mount_nfs- or process-specific — diskutil is just one more victim in the list, not a separate codepath.

If it's useful, I have the same signature repeating across ~9 sysdiagnose captures now (2026-07-05 through 2026-07-07) if you want more samples to compare PIDs/timing against your side of r.171126338.

Re: the workaround test — running now. I've mounted the same RaspberryPi NFS export in parallel at a second location outside /Users/Shared/ (my home directory), alongside the untouched production mount, and I'm logging deny(1) hits separately for each over the next week. Will report back with actual counts rather than a guess.

One thing that would help: is there any more context you can share on what r.171126338 actually changed?

So, the actual change here is primarily to add the log message you're seeing (as well as a few others). The method itself basically does exactly what you'd expect from the name, which is to lookup a "disk" object that corresponds to a given path. As part of that, it's calling "open" on the path so that it can then use fstatfs to retrieve f_mntonname.

And, yes...

So r.171126338 (whatever it changed) likely just added visibility into an open() call that was already going to fail — it didn't cause the failure.

...this is exactly what happened.

So, the message I'd most like to understand is here:

10:52:14.268 umount(28561) deny(1) file-unmount 

Is that something you/your script initiated? If so, do you know what its arguments were and what it actually logged? And what triggered diskutil?

Covering a few odds and ends:

Network issue: checked network logs in the same window, no correlated connectivity event.

I've spent FAR more hours than I like to think about trying to determine network state from our console logs (mostly on iOS, but the issues are similar), so I'd be careful about assuming that a "clean" log means nothing happened on the network. Frankly, our logging just isn't that clear.

I'll relocate one of the two mounts outside /Users/Shared/ (e.g. into my home directory) and watch whether it stops seeing deny(1) while the other one (still under /Users/Shared/) keeps failing — that would be a strong isolation test.

That's a great idea, but the other test I'd add into the mix is adding additional folders and files "beside" both mount points. Something like:

  • /Users/Shared/testfile.txt
  • /Users/Shared/Stefan-Hem/testfile.txt
  • /Users/Shared/Stefan-Hem/TestFolder/testfile.txt

...then test access to those all those objects at the point your watchdog detects a problem. Actually, I'd probably even try calling "open":

open <path> 
open <path inside volume>

The idea here is to pin down EXACTLY what the system is denying access to at the point you detect a problem. Note that "open" is useful here because it DOESN'T actually "open" anything. What it actually does is push the request into LaunchServices, basically replicating what happens when you double-click on an object. That means it differentiates between:

  1. The system has denied access to this "process" (or related processes).

  2. The system has denied access to "you".

Similarly, trying it against a "collection" of files will help pin down exactly which objects the system has a problem with and which it doesn't. Note that "open" succeeding is going to mean the files spontaneously open up, which will obviously get really annoying, so you'll probably turn that part off if it works.

Finally, for logging/diagnostic purposes, I would add a clear time "gap" of 1-2s between any "diagnostic" test (like the opens above) and any "active" manipulation (like unmounting). Spreading out the activity would make it easier to follow exactly what's going on, particularly if some operation succeeds and some fail.

Will report back with actual counts rather than a guess.

Sounds good!

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Kevin — really appreciate the time you're putting into this, especially the level of detail (the "process vs. session" distinction on open in particular was something I hadn't considered). Thank you.

Re: the umount(28561) deny(1) file-unmount line — yes, that's my own remediation script, and it explains the diskutil(28562) call too, not a separate observation.

My watchdog's health check failed for the mount, which triggers a recovery script. That script does (paraphrased):

if is_mounted && !is_fs_ok: /sbin/umount -f "$VOLUME" || /usr/sbin/diskutil unmount force "$VOLUME"

So the sequence is:

  1. umount -f (PID 28561) — denied (file-unmount)
  2. because that command failed, the || fallback on the same line fires: diskutil unmount force (PID 28562) — also denied (file-read-data), which is what produced the StorageKit errno=1 log line 16 microseconds later.

So the diskutil call isn't a separate/independent data point — it's literally the next step on the same script line, triggered because the first command was denied. The whole chain (umount attempt → diskutil fallback → StorageKit error log) is one causal sequence, every step blocked by what looks like the same path-scoped policy decision.

Combined with the earlier finding (ls, perl, python3, mount_nfs all denied identically in the same ~7s window), that's now 8 different binaries — including my own recovery tooling trying to clean up a broken mount — all blocked on the same path. Nothing on that list is privileged or unusual; it's plain user-owned processes doing ordinary I/O under /Users/Shared/Stefan-Hem.

Good call on being skeptical of "clean" network logs — noted, I won't lean on absence-of-magicsock-lines as proof nothing happened on the network going forward.

On the isolation test: I've already extended it along the lines you suggested, before writing this reply. In addition to the parallel mount outside /Users/Shared/ that was already running, I now have:

  • sentinel files at 5 points: outside /Users/Shared entirely, at /Users/Shared/ itself, at the mount root, one folder deeper, and on the outside-mount control path
  • each one probed two ways: a raw open() from my own watching process, and open -g <path> (LaunchServices, backgrounded so it doesn't steal focus) per your suggestion — so I can tell "my process got denied" from "the system-mediated open path got denied" too
  • both probes spaced ~1.5s apart per file, so the timeline in the log stays unambiguous instead of a burst

It only fires when a live deny(1) cluster is actually detected (not every cycle), so I'm not hammering the system — but it's live now, so the next cluster will produce real counts instead of a guess. One known wrinkle: I haven't found a clean way to auto-dismiss the TextEdit windows the open probe spawns (tried a few AppleScript approaches, no luck yet) — for now they just pile up one at a time and I close them by hand, exactly the annoyance you flagged.

Will report back with actual data once the next cluster hits.

Hi Kevin,

Following up with the actual sentinel/isolation data, as promised.

Since the extended probe went live (2026-07-08), it has fired 7 times (2026-07-11 10:23 / 10:44, 2026-07-12 07:55 / 08:15 / 08:42 / 08:53 / 09:27), each time a deny(1) cluster was detected. Raw open() results per location:

  • outside_shared (/Users/Shared/testfile.txt) — sibling of the mount, not itself a mount point — denied 0/7
  • home_baseline (~/watchdog_sentinel.txt) — plain local file, sanity control — denied 0/7
  • stefan_hem_root (/Users/Shared/Stefan-Hem/...) — production NFS mount root — denied 4/7
  • stefan_hem_subdir (/Users/Shared/Stefan-Hem/TestFolder/...) — one level deeper in the same mount — denied 4/7
  • test_pi_root (~/nfs-scope-test/raspberrypi/...) — control NFS mount to a second server, deliberately placed OUTSIDE /Users/Shared/ — denied 7/7

The control mount I set up specifically to test your workaround suggestion (relocate outside /Users/Shared/) was denied more consistently than the production mount that's actually under /Users/Shared/ — 7/7 vs 4/7. Both mounts sit on completely independent servers/hardware, so this isn't a server-side coincidence. Based on this, moving the mount point doesn't look like it would resolve the issue for us; the denial doesn't appear to be scoped to the /Users/Shared/ path specifically, more generally to NFS mount points regardless of where they're rooted in the filesystem.

One more wrinkle worth flagging: for test_pi_root, the two probe methods disagreed at least once — at 2026-07-11 10:23, the raw open() call was denied but the open -g <path> (LaunchServices) call succeeded. In every later occurrence both methods were denied together. Not sure if that's meaningful or just a timing artifact of the probe (1.5s between the two calls), but flagging it in case it's useful.

Happy to keep running this and report a larger sample if it's helpful — the probe is unattended and only fires on an actual cluster (5 min cooldown), so it costs nothing to leave running.

One more wrinkle worth flagging: for test_pi_root, the two probe methods disagreed at least once — at 2026-07-11 10:23, the raw open() call was denied but the open -g <path> (LaunchServices) call succeeded.

Unfortunately, after taking a closer look, "open" doesn't quite do the test I wanted. It basically works the way I described but it also preflight the paths it's given. There are also some tricky issues around exactly what event a process is allowed to emit, both of which mean "open" can end up failing even if the user/app target would be allowed to access the file.

The better test here would be to see if the Finder by calling NSWorkspace.activateFileViewerSelectingURLs. That method actually works through the "Show in Finder" service (which you could also use instead), which means it avoids all sandbox/security issues. The Finder is directly given the path as a string, so all that matters is its own access restrictions.

The control mount I set up specifically to test your workaround suggestion (relocate outside /Users/Shared/) was denied more consistently than the production mount that's actually under /Users/Shared/ — 7/7 vs 4/7.

Interesting. That's not what I expected, but I can see a certain sense to it. The paths you're using are inside your home directory and /Users/Shared is intentionally one of the most unrestricted directories on the system, so it does make some sense that we'd "better" protect those other directories.

That leads me back to your question here:

...or has a workaround

What you do actually "need" to work here? My (unconfirmed) intuition is that this isn't a "general access" issue. Putting that in more concrete terms, I still suspect that things like navigating and opening files through the Finder actually work fine, even after you've entered this "broken" state. However, more "programmatic" access like shell scripts or background/daemon access are failing.

If that understanding is correct, then the other workaround option would be to use the "File Access Temporary Exceptions" to "hard code" access to these specific items into whatever you're using, effectively sidestepping the sandbox.

Note that the term "Temporary" here can be somewhat misleading, particularly for these particular file access entitlements. When we introduced them, they were labeled "temporary" to make it clear that we intended to introduce new entitlements which replaced this functionality and that developers would be expected to adopt those new entitlements. And, in fact, that's exactly what happened for many of these entitlements. However, the odd issue with the File Access entitlements is that:

  1. They serve a very useful role as a workaround option for situations/cases/bugs we didn't anticipate.

  2. Their basic behavior is so straightforward that it's difficult to imagine any other way this entitlement could work that worked for #1 and actually worked differently in any fundamental way.

While it's obviously possible they might someday be removed, I also don't consider them particularly "high risk" or see any problem with using them as an easy solution in focused contexts like this[1].

[1] I recommend against them in more broad/consumer applications, but the issue there is really about the complexity of broad app usage and configuration, NOT that the entitlement itself might go away. As an example, the issue with using this to give your app access to "<home dir>/MyCoolAppDir/" isn't that the entitlement might go away, it's that you’re forcing the user to have a "<home dir>/MyCoolAppDir/" directory, even if that's not where they want your app to store stuff.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Kevin,

Thanks for the clarification on open vs. NSWorkspace.activateFileViewerSelectingURLs / "Show in Finder" — that's a useful distinction I hadn't considered (preflighting + the event-emission restrictions).

I've already added a "reveal" probe alongside the existing raw-read and open -g probes, using open -g -R <path>, which goes through the same Finder-reveal service you described rather than opening the file's content. It's wired into the same sentinel locations and fires on the next confirmed deny(1) cluster. One cluster hit today right after I deployed it, but the timing meant the new probe only got a clean result on the control location outside any mount (not denied, as expected) — no denial data on the mount points themselves yet. I'll follow up with real numbers once a few more clusters accumulate, same as the raw/open() data I posted last time.

To answer your question directly — what do we actually need to work: the process driving most of this is a background daemon (launchd, no interactive session, no Terminal window) that needs continuous, unattended read/write access — checking git status, writing health-check/log data — across several independent project directories on these mounts. There's no Finder interaction anywhere in that path; a human isn't browsing these files. So if your intuition is right that Finder-mediated access survives while programmatic/background access is what's actually broken, that's consistent with what we're seeing (the deny(1) hits are on ls, perl, diskutil, mount_nfs, and our own Python daemon — never anything Finder-initiated). It would also mean the "Show in Finder" test is useful for confirming the diagnosis, but doesn't by itself point to a usable workaround for this specific use case.

That leads to a question about the File Access Temporary Exceptions suggestion: those are App Sandbox entitlements, which apply to code-signed apps that have opted into App Sandbox. The processes actually hitting this (the launchd daemon, shell scripts, Terminal-invoked tools) aren't sandboxed apps at all — they're plain CLI processes. Is the temporary-exception entitlement applicable outside of App Sandbox in any form, or is there a different mechanism (e.g. something at the launchd/daemon level, or a signed helper tool) that would be the right analog for a non-sandboxed background process needing the same kind of "hard-coded" path exception?

Will report back with the reveal-probe numbers as soon as the next cluster gives us real data.

I've already added a "reveal" probe alongside the existing raw-read and open -g probes, using open -g -R <path>, which goes through the same Finder-reveal service you described rather than opening the file's content.

Unfortunately, I'm not sure this will work or not. While "open -g -R <path>" does eventually call activateFileViewerSelectingURLs, what it actually does is:

  1. Perform its own existence checks (which could conceivably fail).

  2. Calls NSWorkspace.selectFile(_:inFileViewerRootedAtPath:), which does its own existence checks.

...and then calls activateFileViewerSelectingURLs. Depending on the process state, it's possible that 1 or 2 might fail, even though the Finder might actually still be able to access the file. What's different about "activateFileViewerSelectingURLs" itself is that it doesn't actually implement ANY checks, but just sends the input directly to the Finder.

There's no Finder interaction anywhere in that path; a human isn't browsing these files.

One minor clarification here. The Finder itself isn't necessarily "special" here, as you'd actually see the same basic results across any user interact app that was working "correctly". It's relevant here because it’s an easy way to test what's going on, not because it's unique.

That leads to a question about the File Access Temporary Exceptions suggestion: those are App Sandbox entitlements, which apply to code-signed apps that have opted into App Sandbox. The processes actually hitting this (the launchd daemon, shell scripts, Terminal-invoked tools) aren't sandboxed apps at all — they're plain CLI processes.

Processes inherit the access of their children, so what's actually happening here is that your processes are "constraining" what our tools can do. FYI, that's why fully sandboxed apps (even on the App Store) are free to use command line tools like cp or rm. Sandbox inheritance means that the tools won't do anything the app couldn't do anyway.

Is the temporary-exception entitlement applicable outside of App Sandbox in any form, or is there a different mechanism?

Two different answers to this:

(1) Our actual entitlement implementation isn't that clear cut, as many of our checks just ask "does this process have the entitlement" regardless of sandbox state. I haven't specifically checked, but I'd expect that to be the case here.

(2) Unfortunately, the history of how the sandbox was introduced alongside the Mac App Store has made the meaning of "sandbox" less clear than it really should be. In common usage, "sandboxed" basically means something like "an app that has enabled the app sandbox and configured itself such that it operates within the limitations expected of an app store app". Implicitly, that means "sandboxed apps" are inherently more limited than "non-sandboxed apps".

The problem is that, in the context of the broader system, that ISN'T how the "sandbox" actually works. What the sandbox actually means to the system is basically "an app that's declared what we should allow it to do", with the system then enforcing those limitations. The critical difference in that second definition is that it DOESN'T actually impose artificial limits on what your process can do. Case in point, the Mac App Store may not allow your app to define arbitrary paths your app has access to, but the entitlement I just sent you sure will.

More importantly, VERY few apps ACTUALLY need to do so "everything" that's allowed with the app sandbox disabled. Some apps may need file system access, some IOKit access, and while other apps need low-level accessibility access. VERY few need all of those. In that context, enabling the sandbox is basically just a matter of formalizing what your process actually needs to do, then declaring it so your process can't be subverted into doing things it shouldn't.

Moving to here:

(e.g. something at the launchd/daemon level, or a signed helper tool) that would be the right analog for a non-sandboxed background process needing the same kind of "hard-coded" path exception?

So, one thing to clarify here is that focusing too much on the "App Sandbox" can be very misleading. Quinn has a full post about this here, but the summary is that there are multiple different file access systems (Quinn lists 5), any one of which can block access.

In terms of turning all of this into a stable solution, my approach would probably be along these lines:

(A) Move your mount point out of /Users/Shared/ and into a fixed location outside of the system’s "managed" hierarchy. My personal inclination would be to create a new APFS volume within the same APFS container, but it could pretty much be anywhere outside of a user’s home directory and/or a designated directory.

One thing to understand here is that the goal of this step ISN'T to give your app access to the object, it's to stabilize the systems handling so that you don't have to worry about behavior shifting dynamically. Jumping back to your earlier message as an example, I believe "test_pi_root" failed because it was inside your home directory, but that consistent failure means that once it's working, you'll know it won't break again.

(B) Enable the App Sandbox and then guarantee access by adding on the "right" entitlements and/or configurations. I know that's a bit vague, but unfortunately, over time, the systems become complex enough that the overlapping layers make it difficult to know exactly what's rejecting you and why. Keep in mind that, from the systems’ perspective, the "normal" way apps gain access to a file system object is that the user directly GAVE them access to that file/object, so this kind of hard-coded access isn't considered the primary approach.

That leads to the other option I overlooked, which is to give your process Full Disk Access. For testing purposes, you can do that through the UI, but in a more "production" configuration, I would probably use Apple Configurator to create a config profile you could use to enable it more "automagically".

I'll also say that throwing FDA into your testing mix would be worthwhile. My assumption is that it would "just work", but if it doesn't work, then that implies something weirder/more complex.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

Hi Kevin,

FDA update: enabled it via System Settings on 2026-07-14 for both /usr/bin/python3 (the exact binary launchd runs the monitoring daemon with) and Terminal — confirmed blue/on for both.

Two days later, three real deny(1) clusters hit on 2026-07-16 (20:58, 21:03, 21:09 local). My daemon tags every denial event with a live FDA check (opens its own ~/Library/.../TCC.db as the test) run from inside the actual long-running check, not an ad-hoc shell test:

  • All three clusters logged FDA: no
  • Access was denied identically to before: mount_nfs / ls / diskutil all still hit "Operation not permitted" on the same paths

One thing that rules out the obvious "stale process" explanation: the daemon has no KeepAlive, so launchd spawns a completely fresh python3 process every 30s — there's no long-lived process that could be missing a refreshed grant.

So it looks like (1) FDA doesn't fix the underlying denial, and (2) the daemon isn't actually getting FDA despite the GUI showing it granted for that exact binary — which is itself odd and maybe worth its own look. I don't have a way to independently query the system TCC database to confirm the grant landed there (would need sudo I don't have queued up), but the daemon's own runtime check is consistently "no" across three separate live incidents.

So, it's always tricky to guess exactly what happened, but my guess would be that it's either this:

(2) the daemon isn't actually getting FDA despite the GUI showing it granted for that exact binary — which is itself odd and maybe worth its own look.

...or some other failure unrelated to the "normal" file system management process.

Assuming it's #2, the general issue here is that much of our higher-level system infrastructure was built with bundled applications in mind, not "bare" executables. We've extended that support some over time (case in point, the original FDA setting pane ONLY allowed bundled apps), but these assumptions are spread throughout the entire system, which makes them difficult to simply "remove".

My standard recommendation here is to just move the tool into a bundle and sidestep the issue; however, that's a more involved process for most of our built-in tools, as many of them are specifically signed so that they'll ONLY run if they're located at their intended location[1].

What I'd probably do instead is create a new "launcher app" whose only job was to run your Python job (using NSTask) and then exit once it was done. That bundled app can then be given FDA and/or our file access entitlements, and Python will then inherit that access from its parent.

[1] This helps protect against attacks that involve running the tools from one system version on a different system version.

__
Kevin Elliott
DTS Engineer, CoreOS/Hardware

---Update: it's been 12 days since we deployed the launcher-app approach (bundled Swift app running the Python job via NSTask, granted FDA on 2026-07-22) and the result looks very promising:

  • Before the change: recurring deny(1) file-mount clusters roughly every 1-3 days (confirmed via automatic sysdiagnose captures tied directly to the denial signature), most recently 2026-07-22 16:24 — about an hour before the launcher app went live.
  • Since 2026-07-22 17:38: zero deny(1) file-mount events in the system log, and zero new sysdiagnose captures, despite the same machine/network having plenty of ordinary flakiness (SSH timeouts, stale mounts) in that window — so the monitoring is definitely still exercising the failure paths, it's just not hitting the sandbox denial anymore.
  • This is by far the longest clean streak we've seen in this whole investigation (the path-scope and plain-FDA hypotheses both fell apart within days of testing). Your diagnosis — that bare binaries never get a stable FDA/TCC grant in a launchd context, and wrapping the job in a proper .app bundle fixes that — looks correct.
  • Will keep watching and report back if anything regresses, but wanted to confirm this as soon as it crossed a meaningful threshold. Thanks for sticking with this.
Kernel Sandbox/System Policy intermittently denies ALL file access (not just mount syscall) on NFS mounts
 
 
Q