Sandboxed commands can't reach the macOS keychain, and the tools that need it blame the credential

Status Open
Reported on v2.1.226
Maintainer reply ✓ Yes — bcherny
Activity 3 comments · opened Aug 15, 2026
💡 Likely answer: A maintainer (bcherny, collaborator) responded on this thread — see the highlighted reply below.

Sandboxed commands can't reach the macOS keychain, and the tools that need it blame the credential

Environment: Claude Code 2.1.226 (native), macOS darwin-arm64, sandbox enabled via user settings (sandbox.enabled: true), Seatbelt backend.

What happens

With the sandbox on, commands that use the macOS keychain fail in two different ways, and neither says "sandbox".

1. git — write-back denied, on every authenticated operation. Git calls the credential helper's approve (store) after each successful auth, so git-credential-osxkeychain tries to write and can't:

$ git fetch origin
failed to store: 100001
$ echo $?
0

Reads still work, so fetch and push succeed. Harmless day to day, but the credential can't be updated or erased from inside a session — a rotated token can't be persisted, and a rejected one can't be cleared.

2. gh — reports a valid token as invalid. Same token, same machine, seconds apart; the only difference is whether the command matched excludedCommands:

$ gh auth status                 # matches "gh *" → runs unsandboxed
  - Active account: true
  - Token: gho_****
  - Token scopes: 'repo', 'workflow', …

$ bash -c 'gh auth status'       # command starts with bash → sandboxed
  - The token in keyring is invalid.
  - To re-authenticate, run: gh auth refresh -h github.com

The second message is actively misleading: it instructs the user to re-authenticate a credential that is fine. (gh keeps working via its config-file token; only the status wording is wrong.)

Why this is worth addressing

The failure is loud but misattributed. A user hitting either message will go rotate a PAT, run gh auth refresh, or suspect GitHub — none of which is the problem. The sandbox is doing exactly what it should; it just doesn't get the credit, and the tools' own error text points the wrong way.

Why the existing levers don't cover it

  • excludedCommands is coarse and doesn't cover children. It removes the whole command from the boundary, and it matches the command Claude runs — not what that command spawns. gh * covers gh auth status; it does not cover bash -c 'gh auth status', a shell script that calls gh, or a Makefile target. Excluding git to silence case 1 would put every git command outside the sandbox, including the writes the sandbox exists to constrain.
  • allowUnixSockets / allowAllUnixSockets don't apply. macOS keychain access is Mach IPC to securityd, not a Unix socket.
  • sandbox.credentials only restricts. Its modes are deny and mask; there is no grant.

Requested change

Primary: an opt-in keychain grant, default off. There is a working precedent — Microsoft's mxc seatbelt backend ships seatbelt.keychainAccess, off by default, and documents exactly what the profile needs:

mach-lookup: com.apple.SecurityServer, com.apple.securityd, com.apple.trustd, com.apple.ocspd, com.apple.cfprefsd.daemon, com.apple.xpcd, and the com.apple.lsd.* family read: /private/var/db/mds, /private/var/protected/trustd read+write: ~/Library/Keychains, /private/var/folders "Off by default — opt in only when the inner workload genuinely needs Keychain access."

Something like sandbox.keychainAccess: true would fit the existing settings shape. The exposure is real and should be documented plainly — it makes the login keychain readable from inside the sandbox — which is exactly why default-off matters.

Secondary, and possibly sufficient on its own: fix the attribution. Most of the cost here is a wrong diagnosis, not a blocked capability. Two cheap options:

  1. Surface the keychain denial in the violation details already appended to a failed sandboxed command's output, so the sandbox's own explanation travels with the confusing message.
  2. Document the limitation in the sandboxing page (Limitations or Troubleshooting) — "keychain-backed credential helpers cannot write from inside the sandbox; tools may report this as an invalid credential."

If only one of these is feasible, the documentation fix alone would have saved this investigation.

Reproduction

  1. Enable the sandbox in ~/.claude/settings.json (sandbox.enabled: true) on macOS.
  2. In a repo whose origin is an authenticated HTTPS remote, run git fetch originfailed to store: 100001.
  3. With "excludedCommands": ["gh", "gh *"] set, compare gh auth status against bash -c 'gh auth status' → the second reports the keyring token as invalid.

View original on GitHub ↗

3 Comments

bcherny collaborator · 14 days ago

Thanks for the detailed diagnosis. What exists today: sandbox.excludedCommands runs matching commands outside the sandbox (which is why bare gh works), and Claude can also request an unsandboxed run for a specific command when the sandbox blocks something, unless you've turned that escape hatch off. Neither helps when the keychain-using tool is nested inside bash -c, and neither fixes the misleading error text, so leaving this open as an enhancement.

Docs: https://code.claude.com/docs/en/sandboxing

🤖 Generated with Claude Code

SatanicMechanic · 6 days ago

Confirming this on macOS 27.0 beta (BuildVersion 26A5416b), gh 2.98.0, Claude Code sandbox
enabled — with one addition that may be a useful diagnostic, and one variant failure mode.

The parenthetical in gh auth status tells you which credential source it used, and that is the
tell.
Same binary, same token, seconds apart, differing only by sandbox:

```
# unsandboxed
✓ Logged in to github.com account <redacted> (keyring)

  • Token: gho_****************
  • Token scopes: 'gist', 'read:org', 'repo'

# sandboxed
X Failed to log in to github.com account <redacted> (default)

  • The token in default is invalid.

```

When gh can reach the keyring it reports (keyring). When it can't, it falls back to the default
source, finds nothing usable there, and reports that as an invalid token. So (default) where you
expect (keyring) is a reliable signal that the keychain was unreachable rather than that anything
is wrong with the credential.

Variant failure mode: with no config-file token, gh hard-fails rather than degrading cosmetically.
The report notes gh "keeps working via its config-file token; only the status wording is wrong."
That fallback doesn't exist here — hosts.yml holds the account name and no oauth_token:

``yaml
github.com:
users:
<redacted>:
user: <redacted>
``

so the failure is real, not just wording:
``
$ gh api user
{ "message": "Requires authentication", "status": "401" }
$ echo $?
1
``
Verified against the unsandboxed run above: the token is valid and works fine outside the sandbox.

The sandbox reports no default keychain at all, which is a stronger statement than an IPC block:
``
$ security show-keychain-info
security: SecKeychainCopySettings <NULL>: A default keychain could not be found.
``

Filesystem reads of the Keychain paths are denied independently of the Mach IPC path:
``
$ ls -la ~/Library/Keychains/
ls: /Users/<redacted>/Library/Keychains/: Operation not permitted
`
This session's sandbox policy carries a
denyOnly filesystem rule covering ~/Library/Keychains,
/Users/*/Library/Keychains, and /Library/Keychains. So there appear to be two independent layers
— the securityd IPC block plus a filesystem deny on the Keychain paths. Worth confirming both are
intentional, since an opt-in
sandbox.keychainAccess` would need to lift both to work.

git credential-osxkeychain get fails silently rather than loudly:
``
$ echo -e "protocol=https\nhost=github.com" | git credential-osxkeychain get
$ echo $?
0
``
Empty output, exit 0 — git reads that as "no stored credential," so this path produces no error
message at all, wrong or otherwise.

+1 on the documentation fix. The current error text points at GitHub and at token rotation well
before the sandbox becomes a suspect.

SatanicMechanic · 6 days ago

Second data point from a different machine, which presents differently enough to be worth recording — my earlier comment was from a work laptop.

Personal machine: same gh 2.98.0, same macOS 27.0 (26A5416b), different Claude Code sandbox settings.

The (keyring) vs (default) tell is config-dependent, not universal. Here gh reports (keyring) and still fails:

X Failed to log in to github.com account <redacted> (keyring)

  • The token in keyring is invalid.

So (default) is a sufficient signal that the keychain was unreachable, but not a necessary one — don't rely on it to reproduce.

Only the securityd IPC block is inherent to the sandbox. On this machine ~/Library/Keychains/ is readable (login.keychain-db, metadata.keychain-db list fine) and there is no keychain entry in the session's denyRead policy, yet gh still can't use the credential. The filesystem deny in my earlier comment came from that machine's own settings, not from the sandbox. An opt-in sandbox.keychainAccess would therefore only need to lift the IPC layer in the default case.

excludedCommands is measurably insufficient, confirming the bash -c point above. With "gh" and "gh *" both set, stripping the injected proxy env leaves gh with no network at all:

$ env -u HTTPS_PROXY -u https_proxy -u HTTP_PROXY -u http_proxy -u ALL_PROXY -u all_proxy gh api /zen
error connecting to api.github.com

Direct egress is blocked, so it ran inside the sandbox despite the exclusion. With the proxy env left in place it instead fails TLS — curl returns 200 and gh fails on the identical URL, which is #77333. In practice the two issues compound: the exclusion doesn't apply to tool-invoked commands, so gh is sandboxed, and being sandboxed it hits both the keychain block and the proxy TLS failure.

git credential-osxkeychain get silent failure reproduces here too — empty output, exit 0. That one is machine-independent and is arguably the strongest case for the error-attribution half of this report, since it emits no error text at all.