My sandbox profile had never run as shipped

Assumes you've seen Claude Code's sandboxed Bash tool; The model that only says RUN describes the fake model used to drive it here.

Claude Code's sandbox for its Bash tool is one of the better ideas in the coding-agent world, which is why it hurt a little to find that mine had never run. On Linux it runs each command under bubblewrap, takes away the network except through its own proxy, and fences off the filesystem with lists you control. My agent-config templates ship an opt-in profile for it, set up for Java and Go work. That profile had never been run as shipped. An audit of the templates pointed it out, and the first run found three ways it would break the builds it was set up for.

How I ran it#

I ran real Claude Code 2.1.309 with bubblewrap 0.12.0, in a throwaway HOME, driven by the fake model. The profile is a settings file my launcher passes to Claude Code with --settings; the template's permission rules, Read(...) denies included, live in user settings. The fake model only echoes the first 300 characters of a tool result back, which is too little to read a build log through, so the test project carried a Makefile that records each outcome in a file, and the file was the verdict. Each target reads or writes one path a Maven or Gradle build uses in the home directory. No real build ran; the Makefile only touches the paths.

Three ways the profile would break a build#

Maven and Gradle lost their settings. Inside the sandbox, ~/.m2/settings.xml and ~/.gradle/gradle.properties were unreadable. The template has Read(...) deny rules for both files, so the agent's Read tool can't open them, and Claude Code feeds those rules into the sandbox too. Its docs say so: "Paths and domains from both sandbox settings and permission rules are merged into the final sandbox configuration." A deny written for one tool became a deny for every command, and Maven and Gradle lost their mirror and proxy settings with it.

The caches weren't writable. ~/.m2/repository and ~/.gradle/caches were listed under allowWrite, and writes there failed anyway. In a fresh home neither directory existed yet, and in that run Claude Code ignored allowWrite entries for paths that didn't exist. A first build is exactly when they don't. A sandbox that only works on a machine that has already built something is a very loyal sandbox.

The Gradle daemon had nowhere to go. Writes to ~/.gradle/daemon were denied, because the list named only caches and wrapper under ~/.gradle.

The fix#

  • allowWrite on ~/.gradle as a whole, with denyWrite on ~/.gradle/init.d, ~/.gradle/init.gradle, ~/.gradle/init.gradle.kts and gradle.properties. Gradle runs init scripts from both places, so a build can write its caches and daemon files but not plant one. caches and wrapper, which hold code Gradle runs later, were writable before the fix too.
  • allowRead on the two settings files. From --settings it re-opens a path that user settings deny, for sandboxed commands only. The Read tool still follows the permission rule.
  • The launcher creates the cache directories before Claude Code starts, so allowWrite has something to point at.

The same Makefile afterwards: both settings files readable, caches and daemon written, and writes to ~/.gradle/init.d, to gradle.properties and to the rest of HOME still denied.

The price#

Those two settings files often hold repository passwords. After the fix, any command in the sandbox can read them, and a build script reads whatever it likes. The guard in front of the agent blocks commands that name the files, but a Gradle build script never has to name them, and the profile lets the sandbox proxy reach every host except cloud metadata addresses, so whatever a build reads can leave. I made that trade knowingly: without those files, Maven and Gradle lose their mirror and proxy settings inside the sandbox, which is the first failure above all over again.

So my templates' docs now say to keep passwords out of those files. Environment variables are the cleaner way. Maven's encrypted passwords are a middle ground. The master password lives in ~/.m2/settings-security.xml, which the profile keeps unreadable, so a sandboxed command sees only ciphertext, and a sandboxed build can't use those passwords either. Outside the sandbox they protect little: Maven's guide says the master password is encrypted with a hardcoded key and should be treated as if it were stored in plain text. Claude Code's sandbox docs also describe masking credential files. I haven't tried that with Maven yet.

The lesson I keep relearning is not about bubblewrap. A config nobody has run is a guess, however sensible each line looks. Each of the three failures came from a line that was reasonable on its own: deny reads of files with secrets, allow writes to the caches, name the directories builds use. Only running it showed how they combined.

Filed under: ai-agents, claude-code, security, java

Sources#