feat(claude): deny secrets and sandbox commands #205

Merged
claudiomaradonna merged 6 commits from sandbox-settings into main 2026-11-09 12:09:39 +01:00
Collaborator

Why

The Claude Code settings file the template seeds into adopters held
hooks and switches but no rule on files and no sandbox: Claude's file
tools and shell commands could read .env files, private keys, and the
credential stores of the home directory, and a prompt injection in an
issue or pull request the workflows read could have them sent out.
Claude Code's Read deny rules and its sandboxed Bash tool close that
gap, but each adopter had to work the rules out alone, though they hold
for most projects. Set aside: shipping the sandbox lists without turning
the sandbox on, which leaves OS-level protection opt-in and unused; the
built-in environment scrub, which turns auto-allow off and refuses
excluded commands on Linux, so run-sandboxed.sh could not run.

What changed

.claude/settings.json now refuses reads of the files that hold secrets
in most web, mobile, and infrastructure projects and on most
workstations, asks before git fetch, git pull, and git push, and
turns Claude Code's Bash sandbox on: credential variables are unset
inside it, the policy's own files are write-denied, and git's network
commands and .agents/scripts/run-sandboxed.sh run outside it, since
the script builds a stronger bubblewrap sandbox of its own and cannot
nest inside Claude Code's. The script takes -C <dir>, so an umbrella
runs a member's commit without a cd, which would keep the call inside
the sandbox, and the run-sandboxed skill names the call form. The
evals' agent runs with Claude Code's sandbox off, the runner's
bubblewrap being its boundary. The README's Hooks and settings describes
the rules, which commands fail inside the sandbox and retry outside it,
and how a repository or a developer drops or adds to them; UPGRADING.md
gains the Sandbox settings migration; docs/MAINTENANCE.md says how to
run the bubblewrap tests and the evals under the source's own sandbox.

How to verify

From a fresh checkout of the branch on Linux with bubblewrap and socat,
start Claude Code at the root, add a fictional .env, export
AWS_SECRET_ACCESS_KEY=fictional, and ask Claude to run cat .env,
sh -c 'cat .env', and env | grep -c AWS_SECRET_ACCESS_KEY, each in
its own command: the two reads fail with Permission denied and the
count is 0, where main prints the file and counts 1. Then ask it to
run .agents/scripts/run-sandboxed.sh HEAD -- sh -c 'echo ok': it
prints ok from outside Claude Code's sandbox, where main has no
exclusion and the call fails with the sandbox cannot start.

On the tip, bash tests/verify-agent-config.sh (231 tests),
bash tests/check-work-folder.sh, .agents/hooks/shellcheck.sh repo,
prettier --check '**/*.md', scripts/verify-agent-config.sh, and
git diff --check main pass; the four regression assertions of
tests/agent-config/claude-settings.sh, the two -C tests of
sandbox-run.sh, and the extended evals-runner.sh test fail on
main. Not tried: the behavioral evals, billed and the maintainer's
before a merge that changes a skill and AGENTS.md; a sandboxed session
on a host whose bubblewrap mounts a fresh /proc, since the container
this ran in needed a copy of bwrap first in PATH and
enableWeakerNestedSandbox for the session.

Considerations

Every developer meets a prompt on each fetch and pull: an ask rule
whose pattern equals the exclusion's is the one guard against a quoted
or escaped --upload-pack, which runs a program on the host and which a
text rule alone cannot stop. Inside the sandbox an app that loads .env
itself, a release build signed from the repository, the cloud CLIs, and
a shell command on .env.example fail and retry outside it, the last
accepted since the Read tool still reads the example; a container that
refuses bubblewrap a fresh /proc or a nested user namespace fails every
sandboxed command until the developer's own settings allow for it, as
the README says. On Linux a write-denied file is protected once it
exists, so a sandboxed command can still create an absent CLAUDE.md.

## Why The Claude Code settings file the template seeds into adopters held hooks and switches but no rule on files and no sandbox: Claude's file tools and shell commands could read `.env` files, private keys, and the credential stores of the home directory, and a prompt injection in an issue or pull request the workflows read could have them sent out. Claude Code's `Read` deny rules and its sandboxed Bash tool close that gap, but each adopter had to work the rules out alone, though they hold for most projects. Set aside: shipping the sandbox lists without turning the sandbox on, which leaves OS-level protection opt-in and unused; the built-in environment scrub, which turns auto-allow off and refuses excluded commands on Linux, so `run-sandboxed.sh` could not run. ## What changed `.claude/settings.json` now refuses reads of the files that hold secrets in most web, mobile, and infrastructure projects and on most workstations, asks before `git fetch`, `git pull`, and `git push`, and turns Claude Code's Bash sandbox on: credential variables are unset inside it, the policy's own files are write-denied, and git's network commands and `.agents/scripts/run-sandboxed.sh` run outside it, since the script builds a stronger bubblewrap sandbox of its own and cannot nest inside Claude Code's. The script takes `-C <dir>`, so an umbrella runs a member's commit without a `cd`, which would keep the call inside the sandbox, and the `run-sandboxed` skill names the call form. The evals' agent runs with Claude Code's sandbox off, the runner's bubblewrap being its boundary. The README's Hooks and settings describes the rules, which commands fail inside the sandbox and retry outside it, and how a repository or a developer drops or adds to them; `UPGRADING.md` gains the Sandbox settings migration; `docs/MAINTENANCE.md` says how to run the bubblewrap tests and the evals under the source's own sandbox. ## How to verify From a fresh checkout of the branch on Linux with bubblewrap and socat, start Claude Code at the root, add a fictional `.env`, export `AWS_SECRET_ACCESS_KEY=fictional`, and ask Claude to run `cat .env`, `sh -c 'cat .env'`, and `env | grep -c AWS_SECRET_ACCESS_KEY`, each in its own command: the two reads fail with `Permission denied` and the count is `0`, where `main` prints the file and counts `1`. Then ask it to run `.agents/scripts/run-sandboxed.sh HEAD -- sh -c 'echo ok'`: it prints `ok` from outside Claude Code's sandbox, where `main` has no exclusion and the call fails with `the sandbox cannot start`. On the tip, `bash tests/verify-agent-config.sh` (231 tests), `bash tests/check-work-folder.sh`, `.agents/hooks/shellcheck.sh repo`, `prettier --check '**/*.md'`, `scripts/verify-agent-config.sh`, and `git diff --check main` pass; the four regression assertions of `tests/agent-config/claude-settings.sh`, the two `-C` tests of `sandbox-run.sh`, and the extended `evals-runner.sh` test fail on `main`. Not tried: the behavioral evals, billed and the maintainer's before a merge that changes a skill and `AGENTS.md`; a sandboxed session on a host whose bubblewrap mounts a fresh `/proc`, since the container this ran in needed a copy of `bwrap` first in `PATH` and `enableWeakerNestedSandbox` for the session. ## Considerations Every developer meets a prompt on each fetch and pull: an `ask` rule whose pattern equals the exclusion's is the one guard against a quoted or escaped `--upload-pack`, which runs a program on the host and which a text rule alone cannot stop. Inside the sandbox an app that loads `.env` itself, a release build signed from the repository, the cloud CLIs, and a shell command on `.env.example` fail and retry outside it, the last accepted since the Read tool still reads the example; a container that refuses bubblewrap a fresh `/proc` or a nested user namespace fails every sandboxed command until the developer's own settings allow for it, as the README says. On Linux a write-denied file is protected once it exists, so a sandboxed command can still create an absent `CLAUDE.md`.
A new test area asserts what .claude/settings.json already does, so
the rules the next commit adds change nothing the README describes
today: the four denied tools, auto memory, attribution, telemetry,
and the built-in skills it turns off.
The template's settings now refuse reads of the files that hold
secrets in most projects and workstations, ask before a git push, and
turn Claude Code's Bash sandbox on: credential variables are unset
inside it, the policy's own files are write-denied, and git's network
commands and run-sandboxed.sh, which builds a stronger bubblewrap
sandbox of its own, run outside it. The sandbox falls back to
unsandboxed commands where it cannot start, and a project deletes a
rule it cannot live with.
run-sandboxed.sh takes -C <dir>, the repository the commit and the
--tests-from paths come from, so an umbrella's session runs a member's
commit from the umbrella's root without a cd, which Claude Code's
sandbox would keep inside it, where bubblewrap cannot start.
Step 2 of run-sandboxed names the one call Claude Code's sandbox runs
outside itself: the script's relative path, alone in its shell command,
with the shell at the root of the checkout that holds it, since a cd, a
pipe, a redirection, or a substitution keeps the call inside the
sandbox, where bubblewrap cannot start. Step 3 sends a refusal of any
other form back to that form, and the Members section has an
umbrella's run name the member's root with -C.
The README's Hooks and settings section says what the settings deny
and sandbox, which commands fail first inside the sandbox and retry
outside it, what the sandbox needs, and how a repository or a
developer drops a rule; its Dependencies name socat beside bubblewrap.
UPGRADING.md gains the Sandbox settings migration, which tells an
upgrading user before the merge and keeps out what they reject.
MAINTENANCE.md tells the maintainer how to run the bubblewrap tests
and the evals from a sandboxed agent session, and the internal skills
guide points at the README for the run-sandboxed call.
fix(evals): run the agent with Claude's sandbox off
All checks were successful
Maintenance / checks (pull_request) Successful in 4m21s
Maintenance / checks (push) Successful in 3m48s
414f8225e9
The template's settings turn Claude Code's sandbox on, and the evals'
agent runs inside the runner's bubblewrap with a fresh home that sets
nothing, where that sandbox cannot nest and would fail every command.
The runner's bubblewrap is the boundary, so the agent starts with
--settings turning the sandbox off, and the runner test expects it.
claudiomaradonna deleted branch sandbox-settings 2026-11-09 12:09:44 +01:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
claudiomaradonna/agent-foundry!205
No description provided.