Claude Code sandbox
Claude Code runs inside sandbox-exec-claude, a purpose-built bwrap container with stricter isolation than the regular terminal. Every session gets its own sandboxed environment — file system, network, process tree, and resource limits are all independent.
- Filesystem — bwrap builds a read-only tmpfs root with exact per-file bind mounts. Only the specific binaries Claude Code needs are visible
- Process isolation — PID, IPC, and UTS namespaces unshared. The sandbox sees only its own 6–10 processes
- Capability drops — 27 Linux capabilities dropped, including
CAP_SYS_ADMIN,CAP_SYS_PTRACE,CAP_NET_ADMIN - Seccomp — optional BPF syscall filter blocks low-level syscalls not needed by Claude Code
- Cgroup limits — 512 MiB RAM, 80% CPU, 64 PIDs per session. Runaway processes are automatically constrained
- Env stripping — only safe variables are passed in. All server credentials are absent from the environment
Network isolation
Each Claude Code session runs in its own Linux network namespace with a dedicated veth pair. The sandbox has no default route — only a single link to the LLM proxy on port 3000 is reachable. Gitea, Postgres, SSH, and the public internet all return "Network unreachable" from inside.
- Per-session veth pair in the
10.202.x.x/30range - Host-side iptables rules allow only the sandbox's IP to reach port 3000
- The LLM proxy validates the session token and routes API calls — credentials never touch the sandbox
- Fallback: if the network namespace cannot be created, bwrap runs with
--unshare-net— the session gets a networkless container
Session & credential hygiene
API session tokens, git credentials, and internal network addresses are kept out of tenant-visible storage.
- Session tokens — each Claude Code session generates a random
cc-sess-token. Tokens expire after 4 hours even if the connection drops without cleanup - Token visibility —
ANTHROPIC_API_KEYand related vars are in Claude Code'senv.denylist. The model cannot read them via the Bash tool - Git credentials — project
.git/configfiles store only the credential-free remote URL. Credentials are injected transiently per git operation and never persisted to disk - Transcript scrubbing — internal veth IPs are replaced with
proxy.internalin conversation transcripts when the session closes
Terminal sandbox
The in-browser terminal runs inside a bubblewrap (bwrap) sandbox. Your commands execute as an unprivileged user — filesystem writes outside your project directory are blocked, and sensitive server environment variables are stripped before the shell starts.
- All sensitive env vars are removed — only safe variables like
PATH,HOME, andNODE_ENVare passed through - The npm cache is redirected to
/tmp/npm-cacheto prevent cross-project interference - Network access is allowed for package installs and API calls
Dev server isolation
Every project's dev server runs as a dedicated system user (devuser) with a separate group, isolated from other projects. Secrets are stripped from the dev server environment using the same allowlist as the terminal.
- Dev servers never have access to Gitea tokens, Supabase service keys, or Cloudflare credentials
- Each project gets its own port — dev servers cannot reach each other directly
- Preview containers use isolated nginx:alpine Docker containers per project
Database isolation
Every project gets its own PostgreSQL schema, completely isolated from other projects. Row-level access is enforced at the API layer:
ANON_KEYrequests are blocked by default — you must explicitly enable public access per table- Logged-in users can only read, update, and delete their own rows (all on by default)
- Users with
role=adminbypass row-ownership filters and can access all rows - SQL queries via the console block
COPY,GRANT/REVOKE, and system catalog access to prevent data exfiltration - Cross-schema references are blocked — queries are scoped to your project's schema only
Note: The DB_API_KEY bypasses all row-level policies and should only be used server-side (edge functions, AI commands). Never expose it in frontend code.