[BUG] sandbox denyRead seems ineffective
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?
Claude can list by GPG private keys even though I denied access to ~/.gnupg/** in sandbox.filesystem.denyRead. I used Claude to verify the settings and confirm expected behavior, after which Claude suggested to report this as a bug.
This is with bubblewrap 0.11.0 on Ubuntu 25.10 using Claude 2.1.71.
Sandbox config
"sandbox": {
"enabled": true,
"autoAllowBashIfSandboxed": false,
"filesystem": {
"allowWrite": [
"//tmp/"
],
"denyRead": [
"~/.ssh/**",
"~/.gnupg/**",
"~/**.kdbx"
],
"denyWrite": [
"~/bin/",
"~/personal-sync",
"~/.claude/*"
]
},
},
This is from the file Claude is actually using, confirmed by breaking the JSON syntax and CLaude then refusing the read it.
What Should Happen?
Claude's attempt to execute Bash(gpg --list-secret-keys) should be denied by bubblewrap.
Error Messages/Logs
❯ the sandbox settings specify denyRead for "~/.gnupg/**", but Bash(gpg --list-secret-keys) still
succeeds. why is that?
● denyRead only applies to Claude Code's own Read/Glob/Grep tools — it controls what Claude's
file-reading tools can access, not what subprocess commands do at the OS level.
When gpg --list-secret-keys runs via the Bash tool, the gpg process reads ~/.gnupg/ directly
through normal OS file I/O, completely outside Claude Code's permission layer. Claude Code has no
way to intercept that.
To actually block it, you'd need to either:
- denyBash with a pattern matching gpg * — this prevents the Bash tool from running gpg commands at
all
- OS-level sandboxing (AppArmor, seccomp, a container) — enforces restrictions at the kernel level,
regardless of what Claude Code does
In short: Claude Code's denyRead is not a security boundary for subprocess execution. It only
guards the dedicated file tools.
❯ shouldn't the sandbox limit the access of the Bash tool and all processes it starts?
● claude-code-guide(Research Claude Code sandbox behavior)
⎿ Done (3 tool uses · 25.4k tokens · 18s)
(ctrl+o to expand)
● You're right — according to the docs, the sandbox should restrict all subprocesses at the OS level
(via bubblewrap on Linux). If gpg --list-secret-keys still succeeded, the likely cause is where
your denyRead is configured:
- permissions.denyRead — only blocks Claude's dedicated Read/Glob/Grep tools, not subprocess file
I/O
- sandbox.filesystem.denyRead — enforced at the OS level, blocks all subprocesses including gpg,
cat, etc.
If your setting is under permissions rather than sandbox.filesystem, that would explain exactly
what you observed. The two sections look similar but have very different enforcement levels.
You can check your config in ~/.claude/settings.json to see which section your denyRead for
~/.gnupg/** is in.
✻ Sautéed for 36s
❯ it'S in sandbox.fileystem
● Then it sounds like the sandbox may not actually be active. A few things to check:
1. Sandbox enabled? — Is sandbox.enabled (or equivalent) set to true in your
~/.claude/settings.json?
2. bubblewrap installed? — Claude Code uses bubblewrap on Linux:
which bwrap && bwrap --version
3. Path expansion — The ~ in ~/.gnupg/** might not be expanded correctly in the sandbox config. Try
using the absolute path /home/daniel/.gnupg/** instead.
4. Check effective config — Run /doctor or claude --show-config (if supported) to see what sandbox
settings are actually in effect.
Would you like me to look at your ~/.claude/settings.json directly to check the configuration?
❯ yes please
● Read 1 file (ctrl+o to expand)
● The config looks correct — sandbox is enabled and ~/.gnupg/** is in sandbox.filesystem.denyRead. So
the issue is likely one of:
1. ~ not being expanded — The sandbox config might not expand ~ to /home/daniel. Try replacing with
the absolute path:
"/home/daniel/.gnupg/**"
2. bubblewrap not functional — Check if it's installed and working:
which bwrap && bwrap --version
2. If bubblewrap isn't available or lacks the necessary privileges (e.g. needs setuid or user
namespaces), Claude Code may silently fall back to running without sandboxing.
3. One other issue — "//tmp/" in allowWrite has a double slash, which might indicate a config
parsing quirk, though that probably doesn't affect the gnupg rule.
I'd start with checking bwrap availability — if the sandbox can't launch, all sandbox.filesystem
rules would be silently ignored.
❯ The use of ~/ and // follows the documentation under "Sandbox path prefixes" on
https://code.claude.com/docs/en/settings
● Fetch(https://code.claude.com/docs/en/settings)
⎿ Received 123KB (200 OK)
● Good — your path prefixes are correct per the documentation. So ~/ and // are being interpreted as
intended.
That leaves the most likely culprit: bubblewrap not working properly. Can you run:
which bwrap && bwrap --version
If bubblewrap is missing or can't create user namespaces (common in some Linux environments where
unprivileged user namespaces are disabled), the sandbox would silently fail to launch and all
sandbox.filesystem rules would have no effect.
❯ output: /usr/bin/bwrap
bubblewrap 0.11.0
● bubblewrap is present. Next thing to check is whether unprivileged user namespaces are enabled,
which bubblewrap requires:
● Bash(cat /proc/sys/kernel/unprivileged_userns_clone 2>/dev/null; sysctl
kernel.unprivileged_userns_clone 2>/dev/null; sysctl user.max_user_namespaces 2>/dev/null)
⎿ 1
kernel.unprivileged_userns_clone = 1
user.max_user_namespaces = 249834
● User namespaces are enabled and working. At this point the configuration looks correct and the
system prerequisites are met — this points to a bug in Claude Code's sandbox implementation.
A few ways to investigate further:
1. Check if the sandbox is actually being invoked — run a command that should clearly be blocked
and see if it succeeds:
cat ~/.gnupg/trustdb.gpg
1. If that succeeds from within a Claude Code session, the sandbox rules are definitely not being
enforced.
2. Report the bug — if confirmed, this is worth filing at
https://github.com/anthropics/claude-code/issues with your settings and the bwrap --version output.
3. Workaround in the meantime — add denyBash permission rules as a belt-and-suspenders approach:
"deny": ["Bash(gpg *)"]
3. That blocks at the permission layer (which we've confirmed does work, as seen with ~/.ssh).
Steps to Reproduce
Generate a gnupg private key in the default location (under ~/.gnupg)
Create a sandbox configuration that contains
"denyRead": [
"~/.ssh/**",
"~/.gnupg/**",
"~/**.kdbx"
],
Tell Claude to list all your private keys.
Claude Model
Sonnet (default)
Is this a regression?
I don't know
Last Working Version
_No response_
Claude Code Version
2.1.71
Platform
Anthropic API
Operating System
Ubuntu/Debian Linux
Terminal/Shell
Other
Additional Information
I'm new to Claude, so please excuse me if I'm missing something obvious.
This issue has 10 comments on GitHub. Read the full discussion on GitHub ↗