[BUG] Regression since 2.1.259 on macOS: EACCES traversing /Library/Managed Preferences is treated as an unreadable managed policy and fails closed (works in 2.1.258)
Preflight Checklist
- [x] I have searched existing issues and this hasn't been reported yet
- [x] This is a single bug report (please file separate reports for different bugs)
- [x] I am using the latest version of Claude Code
What's Wrong?
On macOS, if /Library/Managed Preferences is not traversable by the current user,
every managed-preferences probe returns EACCES, and since 2.1.259 that is treated
as an unreadable managed policy instead of an absent one. The result is two
fatal-severity startup warnings and an armed policy_unreadable_fail_close auth
gate.
This is the macOS counterpart of #91816 (WSL / /mnt/c inaccessible, reported as a
2.1.259 regression). Different source, same defect: EACCES on ancestor
traversal is not distinguished from "a policy exists here and I may not read it".
On my machine a Jamf policy left the directory at mode 000:
$ stat -f '%Sp %Su:%Sg %N' "/Library/Managed Preferences"
d--------- root:wheel /Library/Managed Preferences
macOS default for that directory is drwxr-xr-x root:wheel. That is an MDM-side
misconfiguration and not Claude Code's fault — but the handling of it is, for two
reasons.
1. It is a regression. 2.1.257 and 2.1.258 used existsSync, which returnsfalse on EACCES, so an unreadable plist produced no error record at all
(plistStdouts ended up [], die() empty, no fail-close, no stderr output):
// 2.1.257 / 2.1.258 — import { existsSync as m } from "fs"
function iCt(){return(async()=>{{let t=war(),o=(await Promise.all(t.map(async({path:s,label:n})=>{
if(!m(s))return{stdout:"",label:n,ok:!1};
let{stdout:u,status:a}=await l(Sar,[...bar,s]);
return{stdout:u,label:n,ok:a==="ok"&&!!u}
}))).find((s)=>s.ok);
return{plistStdouts:o?[{stdout:o.stdout,label:o.label}]:[],...}
2.1.261 switched to accessSync(path, R_OK) and forgives only two errnos, soEACCES now becomes a fatal record carrying an unreadReason:
// 2.1.261
try{p(o,w.R_OK)}catch(l){
let c=l&&typeof l==="object"&&"code"in l?l.code:void 0;
if(c==="ENOENT"||c==="ENOTDIR")return null; // absent → fine
return{stdout:null,unreadReason:l instanceof Error?l.message:String(l),...s} // EACCES → fatal
}
That record then reaches the auth gate, which fails closed:
if(U6()){let x=die()[0];
if(x){ ... await wn("auth_force_login_org","policy_unreadable_fail_close",{errno:U}),
{valid:!1,message:`Unable to read managed policy settings. ...`}}}
2. The error message names a file that does not exist. It reports the path it
was probing, not the path that actually failed. On my machinecom.anthropic.claudecode.plist is present in neither location — my organization
pushes no Claude Code policy at all:
$ sudo ls "/Library/Managed Preferences/com.anthropic.claudecode.plist" \
"/Library/Managed Preferences/v.vazquez/com.anthropic.claudecode.plist"
ls: ...: No such file or directory (both)
So the diagnostic points at a nonexistent file and says nothing about the
directory that is actually unreadable. That is genuinely misleading: it reads as
"this plist exists and is locked down", which sent me looking for a plist
format problem before I found the 000 mode. accessSync already has the
errno; on EACCES the code could stat the ancestor chain and name the offending
directory instead.
What Should Happen?
Either of these would have prevented the incident; the first is preferable:
- Treat
EACCESon ancestor traversal as an absent managed source, matching
2.1.258 and matching the reasoning in #91816 — a directory no user can traverse
cannot have delivered a policy key under any version, so no enforcement
guarantee is lost by proceeding.
- At minimum, report the path that actually failed. On
EACCES, walk up and say
e.g. "/Library/Managed Preferences" is not traversable (mode 000); managed rather than naming a leaf plist that may not exist.
preferences cannot be read
Worth noting the blast radius of the current behavior: a 000 managed-preferences
directory voids MDM policy for every app on the machine (Chrome, Defender,
Nudge, Zoom, TCC, FileVault escrow). Claude Code is in a good position to be the
tool that says so clearly.
Error Messages/Logs
Startup, every session, both profiles (observed directly):
Managed settings failed to load; policies from the failed source are NOT in effect:
per-user managed preferences: Managed settings document (per-user managed preferences) could not be read: EACCES: permission denied, access '/Library/Managed Preferences/v.vazquez/com.anthropic.claudecode.plist'; none of its settings are in effect.
device-level managed preferences: Managed settings document (device-level managed preferences) could not be read: EACCES: permission denied, access '/Library/Managed Preferences/com.anthropic.claudecode.plist'; none of its settings are in effect.
And the fail-close, which is what I originally hit:
Unable to read managed policy settings.
This machine may require organization login enforcement, but the policy file failed to load.
Contact your administrator.
Detail: per-user managed preferences: Managed settings document (per-user managed preferences) could not be read: EACCES: permission denied, access '/Library/Managed Preferences/v.vazquez/com.anthropic.claudecode.plist'; none of its settings are in effect.
Steps to Reproduce
- macOS.
sudo chmod 000 "/Library/Managed Preferences"(a Jamf policy did this
on my machine; the mode is what matters, not how it got there).
- Ensure
com.anthropic.claudecode.plistexists in neither
/Library/Managed Preferences/ nor /Library/Managed Preferences/<username>/,
i.e. no Claude Code policy is deployed.
- Run
claude -p 'say ok'as a non-root user. - 2.1.261: two fatal managed-settings warnings on stderr; first-party auth hits
the Unable to read managed policy settings. fail-close.
2.1.258: silent, works.
sudo chmod 755 "/Library/Managed Preferences"→ warnings disappear
immediately, confirming the directory mode is the sole trigger.
Claude Model
Not sure / Multiple models
Is this a regression?
Yes, this worked in a previous version
Last Working Version
2.1.258
Claude Code Version
2.1.261
Platform
AWS Bedrock
Operating System
macOS
Terminal/Shell
zsh
Additional Information
macOS 26.6.2 (build 25G83), arm64 (M2), Darwin 25.6.0. Jamf-managed (DEP, user
approved). Not a local admin at the time of failure.
Version comparison was done by static analysis of the installed binaries under~/.local/share/claude/versions/ (2.1.257, 2.1.258, 2.1.261), so the 2.1.258
behavior above is read from that build's code rather than from a live run on the
broken directory. 2.1.259 and 2.1.260 were not available locally to bisect
exactly, but #91816 independently identifies 2.1.259 as the regression point for
the equivalent WSL path, which is consistent.
Things I checked that do not provide a workaround, in case it saves someone
time: CLAUDE_CODE_MANAGED_SETTINGS_PATH pointed at a readable directory does not
suppress it (the plist probe still runs — output was byte-identical with and
without it), and there is no env var or flag that skips the macOS
managed-preferences read. Fixing the directory mode is the only remedy, which
requires admin the affected user may not have.