anthropics / anthropics/claude-code

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

Aperta
#92,278 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub
area:core bug has repro platform:macos regression
Lingua principale
Python
Stelle
145k
Fork
23.1k
Metriche di merge delle PR
Metriche PR in attesa

Descrizione

### Preflight Checklist

- [x] I have searched [existing issues](https://github.com/anthropics/claude-code/issues?q=is%3Aissue%20state%3Aopen%20label%3Abug) 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):

```js
// 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`:

```js
// 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:

```js
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.
2. 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):

```shell
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:

```shell
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).
2. Ensure `com.anthropic.claudecode.plist` exists in neither
`/Library/Managed Preferences/` nor `/Library/Managed Preferences//`,
i.e. no Claude Code policy is deployed.
3. Run `claude -p 'say ok'` as a non-root user.
4. 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.
5. `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.

Guida per i contributori

Nessuna guida per i contributori indicizzata per questo repository

Direzione di ricerca

Start by reproducing the macOS case with `sudo chmod 000 "/Library/Managed Preferences"` and no Claude Code plist, then compare the managed-preferences probe in installed versions 2.1.258 and 2.1.261. Verify that the fix avoids the fatal auth gate for an inaccessible ancestor, or that diagnostics identify the directory that failed rather than a nonexistent plist.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Valutazione

Stack tecnologico
javascript, macos, zsh
Ambito
authentication, operating-systems, security
Tipo di issue
Bug
Difficoltà
4/5
Tempo stimato
3-5 giorni
Stato di attività
Attiva
Chiarezza
Abbastanza chiara
Idoneità per principianti
38/100

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.