The same zsh script could list ~/Documents when I ran it in Terminal. Started as a LaunchAgent, it failed with:
ls: /Users/administrator/Documents: Operation not permitted
The LaunchAgent had the same user ID, the same $HOME, and the same script. That combination makes this look like a Unix permission problem. In this test it was not. The useful discriminator was the launch context: access succeeded from Terminal, failed from launchd, and still succeeded for a path outside the protected folder.
I reproduced this on macOS 15.6.1 (Darwin 24.6.0) with a LaunchAgent in gui/501. The probe was removed after the test.
Why chmod is the wrong first check
The obvious suspects were file ownership, a wrong home directory, or a job running as another user. The probe printed those facts before touching the files:
#!/bin/zsh
print -- "user=$(id -un) uid=$(id -u)"
print -- "home=$HOME pwd=$PWD"
/bin/ls "$HOME/Documents" 2>&1 | /usr/bin/head -5
/bin/cat "$HOME/Documents/vinh/working/CLAUDE.md" 2>&1 | /usr/bin/head -1
# Negative control: outside Documents
/bin/ls "$HOME/.pf004" 2>&1 | /usr/bin/head -5
The two runs produced this difference:
| Check | Terminal | LaunchAgent in gui/501
|
|---|---|---|
| User / uid |
administrator / 501
|
administrator / 501
|
$HOME |
/Users/administrator |
/Users/administrator |
ls ~/Documents |
Listed entries | Operation not permitted |
cat inside ~/Documents
|
Read the file | Operation not permitted |
ls ~/.pf004 |
Listed entries | Listed entries |
The working directory differed, but the script used absolute paths under $HOME, so PWD=/ did not explain the denial. The negative control mattered more: the LaunchAgent could read another directory owned by the same user. Changing ownership or mode bits would not explain why only the launch context changed the result.
The owning layer is the privacy context
On this machine, the access decision was attached to how the process was launched, not just to uid 501. Terminal had a privacy context that allowed access to the user's Documents folder. The process started by launchd did not inherit that access.
That is why these observations can all be true at once:
-
id -ureports the expected user. -
$HOMEpoints to the expected home directory. - Ordinary files elsewhere under that home directory are readable.
- A read under
~/DocumentsreturnsOperation not permitted.
The smallest diagnostic is therefore not another chmod. Run one identical read from the interactive application and from the scheduled process, then add a control path outside Desktop, Documents, or Downloads. If the user and path are correct, the control succeeds, and only the protected folder fails, investigate the macOS privacy context of the scheduled process.
A pipeline can hide the denial
My first probe contained this line:
/bin/ls "$HOME/Documents" 2>&1 | /usr/bin/head -5
print -- "status=$?"
It printed Operation not permitted and then printed status=0.
In zsh, the default status of a pipeline is the status of its last command. head successfully printed the error text, so the pipeline looked successful even though ls failed. This is enough to turn a visible privacy denial into missing downstream data.
For a diagnostic script, enable pipeline failure before the probe:
set -o pipefail
/bin/ls "$HOME/Documents" 2>&1 | /usr/bin/head -5
print -- "status=$?"
I checked the behavior with a minimal control: /usr/bin/false | /usr/bin/head -1 returned 0 by default and 1 with pipefail enabled. In a larger job, capture and handle the status immediately rather than letting another command overwrite $?.
What to change in an unattended job
If the job does not need a protected folder, move its required inputs and state to an unprotected application directory. The negative-control path under ~/.pf004 was readable from both launch contexts in this test.
If the job must read Documents, treat that as a separate permission requirement and test it from the actual LaunchAgent. A successful Terminal run is not proof that the scheduled process has the same access. I did not test Full Disk Access, a system LaunchDaemon, Desktop, or Downloads, so this result does not establish how those configurations behave.
The practical rule is narrow: same uid and same $HOME do not make Terminal and a LaunchAgent equivalent readers of ~/Documents. Compare the launch contexts, keep an outside-path negative control, and make the pipeline report the command that actually failed.
This article was originally published by DEV Community and written by Vinh Nguyen.
Read original article on DEV Community