[BUG] sandbox denyRead seems ineffective

Status Closed — not planned
Reported on v2.1.71
Maintainer reply None cached
Activity 10 comments · opened Mar 8, 2026 · 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 ↗

10 Comments

github-actions[bot] · 7 months ago

Found 2 possible duplicate issues:

  1. https://github.com/anthropics/claude-code/issues/31925
  2. https://github.com/anthropics/claude-code/issues/31121

This issue will be automatically closed as a duplicate in 3 days.

  • If your issue is a duplicate, please close it and 👍 the existing issue instead
  • To prevent auto-closure, add a comment or 👎 this comment

🤖 Generated with Claude Code

brightbyte · 7 months ago

#31925 is different because it's about permissions not being effective, not sandbox.

#31121 is different because it reports allowWrite to be ineffective, as opposed to denyRead being ineffective.

However, #17727 seems relevant, and so does #16626. Though I did test without the wildcards, and still had the same problem.

MaxwellCalkin · 7 months ago

This is a significant security gap — if denyRead rules in the sandbox configuration don't actually prevent subprocess access, the entire sandboxing model breaks down for sensitive paths like ~/.gnupg, ~/.ssh, and credential stores.

Root Cause Hypothesis

The likely issue is in how bubblewrap bind mounts are constructed. For denyRead to work, the paths need to be either:

  1. Not mounted into the sandbox at all (excluded from --bind/--ro-bind arguments), or
  2. Mounted with no access (e.g., overlaid with an empty directory via --tmpfs)

If the sandbox uses a broad --bind / / or --bind $HOME $HOME mount, then granular denyRead exclusions won't work unless they're explicitly handled as overrides (e.g., --tmpfs ~/.gnupg after the broad bind).

Recommended Fix Pattern

\\\`bash

After the broad home directory bind:

--bind "$HOME" "$HOME"

Overlay denied paths with empty tmpfs:

--tmpfs "$HOME/.gnupg"
--tmpfs "$HOME/.ssh"

Or use --dir for empty directory overlay:

--dir "$HOME/.gnupg"
\\\`

Defense in Depth

Even with the sandbox fix, defense-in-depth matters. Our open-source project Sentinel AI provides a complementary layer — the ToolUseScanner and SessionGuard detect when AI agents attempt to access sensitive paths like .gnupg, .ssh/id_rsa, or credential files, and can block these tool calls before the sandbox is even tested. This catches the intent regardless of sandbox enforcement:

\\\`python
from sentinel import SessionGuard
guard = SessionGuard()
result = guard.check("Bash", {"command": "gpg --list-secret-keys"})

result.blocked = True (sensitive credential access)

\\\`

This kind of layered approach (sandbox + application-level scanning) is critical for agentic AI security.

conradoplg · 6 months ago

I noticed the same issue. It's honestly shocking that this does not work.

fablefactor · 6 months ago

We are seeing the same serious security issue at our company.

PhaedrusTheGreek · 6 months ago

I'm also hitting this :

{
  "sandbox": {
    "enabled": true,
    "autoAllowBashIfSandboxed": true,
    "mode": "auto-allow",
    "filesystem": {
      "denyRead": ["**/.env", "**/.env.*"],
      "denyWrite": ["**/.env", "**/.env.*"]
    },
    "network": {
      "allowedDomains": [
        "github.com",
        "*.githubusercontent.com",
        "registry.npmjs.org",
        "*.npmjs.org",
        "golang.org",
        "proxy.golang.org",
        "sum.golang.org",
        "crates.io",
        "static.crates.io",
        "index.crates.io"
      ]
    }
  },
  "permissions": {
    "allow": ["Bash(*)", "Read(*)", "WebFetch(*)", "WebSearch(*)"]
  }
}
  • Explicit denyRead: ["/.env", "/.env.*"] is set in sandbox config
  • The Read tool successfully returned .env.prod contents
  • Bash(cat .env.prod) correctly returned "Operation not permitted"
  • Expected: both should be denied
github-actions[bot] · 5 months ago

Closing for now — inactive for too long. Please open a new issue if this is still relevant.

addshore · 5 months ago

Closing as "not planned" is a little bold for what is essentially a security bug
And reopening it in a new place also seems less than constructive

davidgm0 · 5 months ago
Closing as "not planned" is a little bold for what is essentially a security bug And reopening it in a new place also seems less than constructive

It's how it works..I don't think there's a way around to reopen it, you just have to open a new one

github-actions[bot] · 5 months ago

This issue has been automatically locked since it was closed and has not had any activity for 7 days. If you're experiencing a similar issue, please file a new issue and reference this one if it's relevant.