[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)

Status Open
Reported on v2.1.259
Maintainer reply None cached
Activity 0 comments · opened Sep 5, 2026

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 returns
false 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, so
EACCES 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 machine
com.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:

  1. Treat EACCES on 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.

  1. 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
preferences cannot be read
rather than naming a leaf plist that may not exist.

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

  1. 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).

  1. Ensure com.anthropic.claudecode.plist exists in neither

/Library/Managed Preferences/ nor /Library/Managed Preferences/<username>/,
i.e. no Claude Code policy is deployed.

  1. Run claude -p 'say ok' as a non-root user.
  2. 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.

  1. 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.

View original on GitHub ↗