Paul's Programming Notes     Archive     Feed     Github

Claude Code - Experimenting With Dev Containers and Permission Allowlists

I run Claude Code, and its VS Code extension, inside dev containers. The reason is isolation. Editor extensions and the toolchains a project pulls in have been a real supply-chain vector lately, and a dev container keeps a compromised one off my host: the extension, the agent, and everything npm or uv installs all live inside the container, not on my machine.

That isolation changes how I think about permissions. Because the blast radius is the container and not my laptop, I’m comfortable letting the agent do far more inside it than I would on bare metal.

The container is a small file. The Claude Code dev container feature installs the CLI and the extension into the container, and I run as a non-root user:

{
  "name": "project",
  "image": "mcr.microsoft.com/devcontainers/base:ubuntu",
  "features": {
    "ghcr.io/anthropics/devcontainer-features/claude-code:1.0": {},
    "ghcr.io/devcontainers/features/github-cli:1": {}
  },
  "remoteUser": "vscode",
  "postCreateCommand": "bash .devcontainer/post-create.sh"
}

With that boundary in place I stop fighting permission prompts. I commit a wide allowlist to .claude/settings.json and turn on acceptEdits so file writes don’t prompt either:

{
  "permissions": {
    "defaultMode": "acceptEdits",
    "allow": [
      "Bash(make test:*)",
      "Bash(make lint:*)",
      "Bash(uv run pytest:*)",
      "Bash(npm run build:*)",
      "Bash(git add:*)",
      "Bash(git commit:*)"
    ],
    "deny": ["Read(.env)", "Read(**/.env)"]
  }
}

It goes in .claude/settings.json, not .claude/settings.local.json, on purpose. When you click “yes, don’t ask again”, Claude Code writes the rule to the local file, and that file gets wiped every time the container rebuilds. Commit the allowlist and it survives rebuilds and travels with the repo.

The container isn’t a free-for-all, so I keep two guardrails inside it. It still holds my code and a token, so I don’t blanket-allow uv run or npm install: uv run runs whatever follows it, and npm install <pkg> runs package install scripts, which is how a lot of npm supply-chain attacks land. I pin the inner command (uv run pytest) and leave the runners prompting. I also keep the container’s GitHub token read-only and push from the host, so a bad suggestion can’t push or open a PR on its own.

If you don’t want a full dev container, Claude Code added a built-in Bash sandbox recently. Run /sandbox and it isolates shell commands at the OS level, Seatbelt on macOS and bubblewrap on Linux, restricting what each command can read, write, and reach on the network. In auto-allow mode it runs sandboxed commands without a prompt while git push and friends still ask. It’s a lighter boundary than a container, and you can run it inside one as a second layer (set enableWeakerNestedSandbox when nested, since the container is already the outer boundary).

None of this is a hard sandbox on its own. Permission rules are enforced by Claude Code, not the OS, and the sandbox proxy doesn’t inspect TLS, so a broad domain allowlist can still leak. I’ve always been more worried about approving the one thing I shouldn’t than about the prompts themselves, and I’m still working out how to shrink the blast radius further. This is early, and I expect parts of it to look risky in hindsight.