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)

Ouverte
#92,278 0 commentaires 0 réactions 0 personnes assignées Voir sur GitHub
area:core bug has repro platform:macos regression
Langage dominant
Python
Étoiles
145k
Forks
23.1k
Métriques de merge des PR
Métriques de PR en attente

Description

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

Guide de contribution

Aucun guide de contribution indexé pour ce dépôt

Piste de recherche

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.

Rédigé par le modèle d'indexation à partir du texte de l'issue.

Évaluation

Stack technique
javascript, macos, zsh
Domaine
authentication, operating-systems, security
Type d'issue
Bug
Difficulté
4/5
Temps estimé
3-5 jours
Activité
Active
Clarté
Plutôt claire
Accessibilité débutants
38/100

Recevez les nouvelles issues par e-mail

Un résumé court des issues GitHub adaptées aux débutants.