The same zsh script could list when I ran it in Terminal.
Started as a LaunchAgent, it failed with: The LaunchAgent had the same user ID, the same , 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 , 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 .
The probe was removed after the test.
Why 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: The two runs produced this difference: Check Terminal LaunchAgent in User / uid / / Listed entries inside Read the file Listed entries Listed entries The working directory differed, but the script used absolute paths under , so 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 did not inherit that access.
That is why these observations can all be true at once: reports the expected user. points to the expected home directory.
Ordinary files elsewhere under that home directory are readable.
A read under returns .
The smallest diagnostic is therefore not another .
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: It printed and then printed .
In zsh, the default status of a pipeline is the status of its last command. successfully printed the error text, so the pipeline looked successful even though failed.
This is enough to turn a visible privacy denial into missing downstream data.
For a diagnostic script, enable pipeline failure before the probe: I checked the behavior with a minimal control: returned by default and with 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 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 do not make Terminal and a LaunchAgent equivalent readers of .
Compare the launch contexts, keep an outside-path negative control, and make the pipeline report the command that actually failed.