[BUG] sandbox denyRead seems ineffective

Resolved 💬 10 comments Opened Mar 8, 2026 by brightbyte Closed Apr 28, 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?

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.

View original on GitHub ↗

This issue has 10 comments on GitHub. Read the full discussion on GitHub ↗