feat(claude): narrow the sandbox to secret files #209

Merged
claudiomaradonna merged 5 commits from feat/secret-rules-only into main 2026-11-10 14:27:15 +01:00
Collaborator

Why

The sandbox the template turned on on 2026-11-09 made most git commands fail or prompt. It blocked the signing agent's socket, hid ~/.ssh, filtered the network, and write-denied the files a landing merge replaces, so each commit, fetch, and push of a workflow cost a failed first attempt or a prompt. Two ways out were tried and set aside: carrying git inside that sandbox while keeping its defences against an injected instruction, which took six trade-offs and still drew two high security findings; and no sandbox at all, where a recursive search or a glob was measured to print a nested .env. The sandbox is wanted for one thing: .env and secret files, wherever they sit in the project, do not reach Claude by mistake.

What changed

The sandbox now only holds every command to the Read deny rules on the project's secret files, and is open to everything else: every host, every Unix socket, and writes in the home directory. A signed commit, a fetch, and a push run inside it at their first call, unasked, in Manual and acceptEdits mode too. Inside it the sixty credential variables of providers and services the settings name are still unset; those of databases and frameworks, such as DATABASE_URL, now stay set for the tests and servers that read them.

It is no longer a defence against an injected instruction. The write denials on the policy files, the ask rules on git pull and git push, the --upload-pack deny rules, and the Read rules on the home directory's credential stores are gone, and the README says what that gives up. This breaks nothing silently for an adopter of the earlier sandbox: the Narrowed sandbox entry of UPGRADING.md lists each change, to leave out of the merge when rejected.

git merge --ff-only now leaves the sandbox, since the landing merge replaces files Claude Code keeps read-only inside it, and land.md gives the merge the form that exclusion needs. The README's account is shorter, without Your own sandbox settings.

How to verify

With Claude Code 2.1.322 on Linux, commits signed through ssh-agent, in a clone of the branch:

  1. Create a/b/c/.env holding a line with a marker word. Ask Claude to run grep -rn <marker> a, cat a/b/c/.e*, and sh -c "cat a/b/c/.env". Each answers Permission denied and the marker is never printed.
  2. In the default permission mode, ask Claude to run git commit --allow-empty -m "chore: probe" and git -C . push --dry-run origin HEAD:refs/heads/probe. Each succeeds at its first call, without a prompt; on main the commit fails with Couldn't get agent socket?.
  3. Start Claude with NPM_TOKEN and DATABASE_URL exported and ask it to run printenv NPM_TOKEN DATABASE_URL. It prints the second alone.
  4. On a branch that changes a file under .claude/agents/, git -C . merge --ff-only <branch> fails with unable to unlink old, and git merge --ff-only <branch> alone in its call fast-forwards.

On 3d160da the checks of docs/MAINTENANCE.md pass, 243 tests among them, and the three rewritten or changed assertions of tests/agent-config/claude-settings.sh fail on main. Not tried: macOS, Docker, a tool that ignores the sandbox's proxy, and the excluded merge's prompt in Manual mode, for want of the machines; whether a variable a shell profile exports returns after the unset in an interactive session; and the behavioral evals, which bill, though land.md changes.

Considerations

A push now rests on the rule of AGENTS.md that one happens only when the user names it, with no prompt behind it. A project's own command that reads a secret file, such as a test suite loading its .env, or that takes a login from an unset variable, fails inside the sandbox and falls to the retry. The unset covers only the sixty names, so a forge token such as GH_TOKEN stays set; the README says where a repository or a developer adds a name, and a run showed the lists add together. The merge in a git -C or cd spelling still fails inside; land.md gives it one retry in the right form.

## Why The sandbox the template turned on on 2026-11-09 made most git commands fail or prompt. It blocked the signing agent's socket, hid `~/.ssh`, filtered the network, and write-denied the files a landing merge replaces, so each commit, fetch, and push of a workflow cost a failed first attempt or a prompt. Two ways out were tried and set aside: carrying git inside that sandbox while keeping its defences against an injected instruction, which took six trade-offs and still drew two high security findings; and no sandbox at all, where a recursive search or a glob was measured to print a nested `.env`. The sandbox is wanted for one thing: `.env` and secret files, wherever they sit in the project, do not reach Claude by mistake. ## What changed The sandbox now only holds every command to the `Read` deny rules on the project's secret files, and is open to everything else: every host, every Unix socket, and writes in the home directory. A signed commit, a fetch, and a push run inside it at their first call, unasked, in Manual and `acceptEdits` mode too. Inside it the sixty credential variables of providers and services the settings name are still unset; those of databases and frameworks, such as `DATABASE_URL`, now stay set for the tests and servers that read them. It is no longer a defence against an injected instruction. The write denials on the policy files, the `ask` rules on `git pull` and `git push`, the `--upload-pack` deny rules, and the `Read` rules on the home directory's credential stores are gone, and the README says what that gives up. This breaks nothing silently for an adopter of the earlier sandbox: the Narrowed sandbox entry of `UPGRADING.md` lists each change, to leave out of the merge when rejected. `git merge --ff-only` now leaves the sandbox, since the landing merge replaces files Claude Code keeps read-only inside it, and `land.md` gives the merge the form that exclusion needs. The README's account is shorter, without Your own sandbox settings. ## How to verify With Claude Code 2.1.322 on Linux, commits signed through ssh-agent, in a clone of the branch: 1. Create `a/b/c/.env` holding a line with a marker word. Ask Claude to run `grep -rn <marker> a`, `cat a/b/c/.e*`, and `sh -c "cat a/b/c/.env"`. Each answers `Permission denied` and the marker is never printed. 2. In the default permission mode, ask Claude to run `git commit --allow-empty -m "chore: probe"` and `git -C . push --dry-run origin HEAD:refs/heads/probe`. Each succeeds at its first call, without a prompt; on `main` the commit fails with `Couldn't get agent socket?`. 3. Start Claude with `NPM_TOKEN` and `DATABASE_URL` exported and ask it to run `printenv NPM_TOKEN DATABASE_URL`. It prints the second alone. 4. On a branch that changes a file under `.claude/agents/`, `git -C . merge --ff-only <branch>` fails with `unable to unlink old`, and `git merge --ff-only <branch>` alone in its call fast-forwards. On 3d160da the checks of `docs/MAINTENANCE.md` pass, 243 tests among them, and the three rewritten or changed assertions of `tests/agent-config/claude-settings.sh` fail on `main`. Not tried: macOS, Docker, a tool that ignores the sandbox's proxy, and the excluded merge's prompt in Manual mode, for want of the machines; whether a variable a shell profile exports returns after the unset in an interactive session; and the behavioral evals, which bill, though `land.md` changes. ## Considerations A push now rests on the rule of `AGENTS.md` that one happens only when the user names it, with no prompt behind it. A project's own command that reads a secret file, such as a test suite loading its `.env`, or that takes a login from an unset variable, fails inside the sandbox and falls to the retry. The unset covers only the sixty names, so a forge token such as `GH_TOKEN` stays set; the README says where a repository or a developer adds a name, and a run showed the lists add together. The merge in a `git -C` or `cd` spelling still fails inside; `land.md` gives it one retry in the right form.
The sandbox made most git commands fail or prompt, and its defences
against an injected instruction outgrew what it is for: keeping .env
and secret files, wherever they sit in the project, from Claude by
mistake. The Read rules alone stop only the file tools and a command
that names the file, so the sandbox stays to enforce them on every
process and opens to all else: every host and Unix socket, writes in
the home directory, and the home's credential stores. The write
denials, credential variables, ask rules, upload-pack deny rules, and
the exclusions of fetch, pull, and push go; the landing merge leaves
the sandbox, which keeps Claude Code's own protected paths read-only.
The sandbox now only holds every command to the Read rules on the
project's secret files, so the README states that one purpose, what the
sandbox is opened to and gives up against an injected instruction, the
two commands that leave it, and what still falls to a retry, in place
of its longer account and of the settings it asked each developer to
add. UPGRADING.md tells an adopter of the earlier sandbox what the
merge drops and opens.
feat(skills): land with the merge alone in its call
All checks were successful
Maintenance / checks (pull_request) Successful in 4m25s
a9b33f9557
The settings take the landing merge out of Claude Code's sandbox, which
keeps files such as .claude/skills read-only, but only when the merge
is alone in its call with the shell in the main checkout. An agent in
a worktree spells it with git -C or after a cd, which stays inside and
fails, so land.md gives the form and the one retry that cures a call
made in another.
feat(claude): unset provider credentials again
All checks were successful
Maintenance / checks (pull_request) Successful in 4m22s
28cc912fbd
Narrowing the sandbox let every credential variable through, on the
view that unsetting them only defended against an injected instruction.
It also keeps a secret from Claude by mistake, through env as the Read
rules do through a file, which is what the sandbox is for. The
variables of cloud providers, registries, AI providers, mobile signing,
and third-party services are unset again; those of databases and
frameworks stay set, since a project's own tests and servers read them.
docs: say where to unset more variables
All checks were successful
Maintenance / checks (pull_request) Successful in 4m20s
Maintenance / checks (push) Successful in 3m55s
3d160da9f8
The sandbox unsets only the sixty credential variables the settings
name, so a project's own key or a developer's token stays set. The
README now says where each names another to unset, and that Claude
Code adds the lists together, which a headless run confirmed.
claudiomaradonna deleted branch feat/secret-rules-only 2026-11-10 14:27:20 +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!209
No description provided.