Configuration¶
Every knob the sandbox honours, what it does, what it defaults to, whether an admin can lock it down, and a runnable example. Edit ~/.config/agent-sandbox/sandbox.conf (deployed automatically on first run); changes take effect the next time you start a sandbox — no reinstall.
How config is loaded¶
1. Defaults (sandbox-lib.sh, hard-coded)
2. Admin baseline /app/lib/agent-sandbox/sandbox.conf (if present, root-owned)
3. User config ~/.config/agent-sandbox/sandbox.conf (or user.conf when admin baseline exists)
4. Per-project ~/.config/agent-sandbox/conf.d/*.conf (sourced in lexical order)
Each layer adds to the previous. The admin layer (when present) is the security baseline: certain values land in the layer-2 snapshot and the user's layer-¾ entries are merged on top such that the user can append but not remove. See admin enforcement below for the exact rules.
User config is loaded in an isolated subprocess (no eval in the parent), then variable values are extracted via a three-layer-validated declare -p round-trip. Configs cannot mutate the sandbox via shell-level side effects — set, trap DEBUG, IFS=, exports, eval overrides, background jobs, etc. all stay inside the subprocess and are dropped. Unknown variable names produce a one-line warning at startup so stale config from older versions surfaces instead of silently breaking.
Admin enforcement model¶
When /app/lib/agent-sandbox/sandbox.conf exists, it is sourced first as a trusted file, then snapshotted. The user's layer-¾ config runs in the isolated subprocess described above, and after extraction the policy enforcer (_enforce_admin_policy in sandbox-lib.sh) merges per these rules:
Enforced arrays — admin entries are restored, user entries are appended on top. User cannot remove an admin entry; attempting it produces a WARNING: removed admin-enforced X entry '…' — restored. line on stderr.
| Array | Why locked down |
|---|---|
BLOCKED_FILES |
Admin can pin specific files (e.g. site-wide instruction files) hidden from agents. |
BLOCKED_ENV_VARS |
Site-wide env-var blocklist. |
BLOCKED_ENV_PATTERNS |
Site-wide credential-pattern globs. |
EXTRA_BLOCKED_PATHS |
Site-wide path blocklist (e.g. clinical data, regulated datasets). |
DEVICES_BLACKLIST |
Site-wide device-node blocklist (e.g. /dev/pts to refuse the kernel-<6.2 TIOCSTI workaround). |
Enforced scalars (security-critical booleans) — admin can set to true and the user cannot weaken to false. Attempting it produces WARNING: weakened admin-enforced X=true → restored. and the value is restored.
| Scalar | Effect when admin sets true |
|---|---|
PRIVATE_TMP |
/tmp isolated per sandbox (locked on). |
PRIVATE_IPC |
IPC namespace isolated per sandbox (locked on). |
FILTER_PASSWD |
LDAP/AD user enumeration filtered (locked on). |
HOME_READONLY → HOME_WRITABLE escalation prevented — if the admin lists an entry in HOME_READONLY, the user cannot move it to HOME_WRITABLE. The sandbox warns and reverts.
DENIED_WRITABLE_PATHS — admin-only deny-list (no user-side equivalent). Any user EXTRA_WRITABLE_PATHS or HOME_WRITABLE entry that resolves under a denied path is stripped with a warning. Symlinks are resolved on both sides so a writable path can't bypass the blocklist by pointing at a denied target.
ALLOWED_PROJECT_PARENTS (narrowing-only) — the user can only narrow admin's list, never expand it. A user-supplied path is admissible iff its canonical resolution (via realpath) is identical to or a path-component subdir of the canonical resolution of one of admin's allowed parents. Symlinks are followed: a user path that resolves outside admin's tree is rejected, even if its literal string appears under an admin entry. Inadmissible entries are stripped with a WARNING. If the merge yields an empty effective list (e.g., user requested only paths outside admin's tree), the sandbox refuses to start.
If admin doesn't set ALLOWED_PROJECT_PARENTS, the narrowing baseline is / (the user's list passes through unchanged). If admin sets a malformed value (non-array, non-absolute path, command substitution), the sandbox refuses to start. Missing admin file vs. malformed admin file is an explicit security boundary: missing → default to /; malformed → fail-closed (no fall-through).
Without an admin baseline, ~/.config/agent-sandbox/sandbox.conf is the only config and there is no enforcement layer — the user's configuration is the effective policy in full. See Admin Install for setting up an admin baseline.
Variables index¶
| Variable | Type | Admin-enforced? | Default |
|---|---|---|---|
ALLOWED_PROJECT_PARENTS |
array | narrowing-only (user can only restrict admin's list) | /fh/fast, /fh/scratch, $HOME |
READONLY_MOUNTS |
array | additive merge | system paths + /app |
HOME_ACCESS |
scalar | no | tmpwrite |
HOME_READONLY |
array | RO→WR escalation blocked | shell + tool defaults |
HOME_WRITABLE |
array | additive; admin RO entries can't move here; respects DENIED_WRITABLE_PATHS |
.cache/uv + agent profile additions |
HOME_SEEDED_FILES |
array | additive | .gitconfig |
EXTRA_BLOCKED_PATHS |
array | yes (admin entries restored) | () |
EXTRA_WRITABLE_PATHS |
array | additive; respects DENIED_WRITABLE_PATHS |
() |
DENIED_WRITABLE_PATHS |
array | admin-only | () |
BLOCKED_FILES |
array | yes | () + per-agent additions |
DEVICES |
array | additive; vetoed by DEVICES_BLACKLIST |
NVIDIA driver nodes |
DEVICES_BLACKLIST |
array | yes | /dev/{mem,kmem,port,pts,sd*,nvme*,loop*} |
BIND_DEV_PTS |
scalar | no (deprecated, kernel-aware shim) | false |
BLOCKED_ENV_VARS |
array | yes | service-credential names |
BLOCKED_ENV_PATTERNS |
array | yes | SSH_*, *_TOKEN, *_SECRET, … |
ALLOWED_ENV_VARS |
array | additive | agent API-key names |
SANDBOX_ENV |
array | additive | () |
SANDBOX_BACKEND |
scalar | no | auto (bwrap → firejail → landlock) |
SANDBOX_PREFERRED_BACKENDS |
(set inline via SANDBOX_BACKEND only) |
— | — |
SANDBOX_MODULES |
array | additive | () |
PRIVATE_TMP |
scalar | harden-only (admin true is sticky) |
true |
PRIVATE_IPC |
scalar | harden-only | true |
FILTER_PASSWD |
scalar | harden-only | true |
SANDBOX_NPROC_LIMIT |
scalar | no | "" (unlimited) |
SANDBOX_QUIET |
scalar | no | false |
NETWORK_FILTER_MODE |
scalar | harden-only (user can only request equal or stricter) | filtered |
NETWORK_FILTER_FALLBACK |
scalar | harden-only | open |
NETWORK_BLOCKLIST |
array | yes (admin entries floor) | built-in floor (SMTP submission, webhook/paste/DoH exfil surfaces, legacy r-services) |
NETWORK_BLOCKLIST_EXCEPT |
array | additive; user entries covered by an admin BLOCKLIST pattern are stripped | () |
NETWORK_MAIL_BLOCK |
scalar | harden-only (user can only request equal or stricter; off < auto < on) | auto |
NETWORK_FILTER_SKIP_HELPER_PROBE |
env override | no | 0 |
SLURM_SCOPE |
scalar | no | project |
CHAPERON_LOG_LEVEL |
scalar | no | info |
CHAPERON_LOG_RETAIN_DAYS |
scalar | no | 7 |
ENABLED_AGENTS |
array | additive | claude, codex, gemini |
SUPPRESS_AGENT_WARNINGS |
array | additive | () |
Project & home¶
ALLOWED_PROJECT_PARENTS¶
Type array · Admin-enforced narrowing-only (user can only restrict admin's list) · Default ("/fh/fast" "/fh/scratch" "$HOME")
The sandbox grants write access to exactly one directory — the project dir, set with --project-dir or defaulting to $PWD. For safety, that path must resolve under one of the entries listed here. A project dir outside this set is rejected at sandbox start.
Set this to the parents under which projects live on your system. On non-Fred-Hutch hosts the defaults can be replaced (+= to append, plain = to replace).
Admin/user merge — narrowing-only. When an admin baseline is present, the user can only restrict the admin's allow-list. A user-supplied entry is admissible iff its canonical path (via realpath, with all symlinks followed) is identical to or a path-component subdir of the canonical path of one of admin's entries. The check is canonical-on-canonical, so a user-supplied path whose canonical form escapes admin's tree (via a symlink that looks admissible) is rejected.
The path-component boundary is enforced: /foo is not a parent of /foobar. Each non-admissible entry is stripped from the effective list with a WARNING: line on stderr. If every user-requested entry is rejected (or the user's array is empty), the sandbox refuses to start — there is no fall-through to a permissive default.
Missing vs. malformed admin config. If the admin file is missing entirely, the narrowing baseline defaults to / (no narrowing — the user's list passes through unchanged). If the admin file is present but malformed — syntax error, runtime error during source, ALLOWED_PROJECT_PARENTS is not an indexed array, an entry is not absolute, or an entry contains command substitution — the sandbox refuses to start with a clear error. The boundary is explicit and security-relevant: missing admin → default; malformed admin → fail-closed.
HOME_ACCESS¶
Type scalar · Admin-enforced no · Default tmpwrite
Controls how much of $HOME the agent sees and whether unlisted writes persist. Override per-session via HOME_ACCESS=read agent-sandbox bash.
| Mode | Real files visible? | Agent can write? | Writes persist? | Use case |
|---|---|---|---|---|
tmpwrite (default) |
Only listed paths | Anywhere in $HOME |
No — lost on exit | Recommended. Agents create lock files, caches, temp dirs without errors; nothing leaks. |
restricted |
Only listed paths | Only HOME_WRITABLE + project |
Yes | Maximum lockdown. Unlisted writes return EROFS. |
read |
Everything | Only HOME_WRITABLE + project |
Yes | Agent needs to read arbitrary dotfiles or configs. |
write |
Everything | Everything | Yes | Full access — use with caution. |
Credential dirs (.ssh, .aws, .gnupg) are always blocked, regardless of mode.
HOME_READONLY¶
Type array · Admin-enforced RO→WR escalation blocked · Default shell + tool config dotfiles (.bashrc, .zshrc, .vimrc, .tmux.conf, .linuxbrew, .local/bin, micromamba, .condarc, …)
Subdirectories and files of $HOME to mount read-only inside the sandbox. Each entry is $HOME-relative (no leading /). Missing entries are silently skipped, so listing dotfiles you might not have is safe.
Per-agent read-only entries (e.g. .aider.conf.yml) are folded in automatically from each enabled agent profile — see ENABLED_AGENTS.
HOME_READONLY+=(
".config/gh" # GitHub CLI config (also add GITHUB_TOKEN to ALLOWED_ENV_VARS)
".config/nvim" # neovim config
)
HOME_WRITABLE¶
Type array · Admin-enforced additive; admin HOME_READONLY entries cannot move here; respects DENIED_WRITABLE_PATHS · Default (.cache/uv) + per-agent additions from enabled profiles
Subdirectories and files of $HOME with read+write access. Missing entries are auto-created as empty dirs before the sandbox launches, so first-time in-sandbox auth works for agents.
If the admin baseline lists an entry in HOME_READONLY, the user cannot promote it to HOME_WRITABLE — the sandbox warns and reverts.
HOME_SEEDED_FILES¶
Type array · Admin-enforced additive · Default (.gitconfig)
Files whose content is read from the host but materialised into the per-session tmpfs $HOME as a writable copy. The agent can edit them without touching the real host file — writes land in the tmpfs and are discarded on sandbox exit. Use this for dotfiles tools want to write but you don't want the agent to mutate persistently (gh auth setup-git rewriting .gitconfig, IDE git plugins, package-manager telemetry).
Conflict rule: an entry in HOME_SEEDED_FILES wins over the same entry in HOME_READONLY (the read-only mount is skipped).
Backend support:
- bwrap — full support via
--file FD DEST(writable tmpfs copy). - firejail — degrades to read-only with a startup warning.
- Landlock — degrades to read-only with a startup warning (no mount namespace).
Mounts¶
READONLY_MOUNTS¶
Type array · Admin-enforced additive merge · Default ("/usr" "/lib" "/lib64" "/bin" "/sbin" "/etc" "/app")
Directories the agent can read but never write. The system paths are required for basic functionality (the sandbox warns if they are missing on the host). Add data directories the agent needs to read.
The agent cannot access anything not listed here, so apply the principle of least privilege: mount only what the task needs. Mounting an entire lab share is convenient but exposes everything under it.
READONLY_MOUNTS+=(
"/fh/fast/mylab/user/me" # just your user dir — recommended
"/fh/fast/shared/reference_genomes" # site-wide reference data
)
EXTRA_WRITABLE_PATHS¶
Type array · Admin-enforced additive; entries under DENIED_WRITABLE_PATHS are stripped with a warning · Default ()
Directories the agent can read and write in addition to the project directory. Use for shared output directories, pipeline scratch space, or other locations the agent must modify but that aren't the project dir. Each entry expands the agent's write surface — only add directories the agent genuinely needs.
DENIED_WRITABLE_PATHS¶
Type array · Admin-enforced admin-only (no user-side counterpart; the variable is editable in user config but only the admin snapshot is honoured) · Default ()
Paths that must never be writable, regardless of user config. After the user/project layers are merged, any EXTRA_WRITABLE_PATHS or HOME_WRITABLE entry that resolves under a denied path is stripped with a warning. Both literal strings and resolved symlink targets are checked, so a writable entry pointing a symlink at a denied target is rejected.
Set this in the admin baseline only; user-set values do not survive admin enforcement.
# /app/lib/agent-sandbox/sandbox.conf (admin)
DENIED_WRITABLE_PATHS=(
"/etc"
"/usr"
"/fh/fast/restricted_clinical"
)
EXTRA_BLOCKED_PATHS¶
Type array · Admin-enforced yes · Default ()
Paths outside $HOME that should be hidden inside the sandbox. Each path is overlaid with an empty tmpfs (bwrap/firejail) — the path appears to exist but resolves to an empty directory. Use to carve sensitive subdirectories out of otherwise-visible mounts (e.g. clinical data under a lab storage path).
BLOCKED_FILES¶
Type array · Admin-enforced yes · Default () (per-agent instruction files added automatically)
Specific files inside readable or writable directories that should be hidden. Each file is overlaid with /dev/null — it appears to exist but is empty. Useful when a parent directory must be accessible but a specific file within it should be protected.
Per-agent instruction files (~/.claude/CLAUDE.md, ~/.codex/AGENTS.md, etc.) are added automatically by _apply_agent_profiles from each enabled agent's config.conf. The agent's overlay then exports a *_CONFIG_DIR env var so the agent reads the sandbox-merged copy instead — see agents/<name>/config.conf for the schema.
Backend limitation: only respected by bwrap and firejail. Landlock cannot block individual files under directories it has already granted access to (no mount namespace, no overlays).
bwrap masking needs a real file on host. When the bwrap backend implements BLOCKED_FILES via --ro-bind /dev/null <path>, the host path must already exist as a regular file: bwrap's ensure_file → create_file → creat() (see utils.c in v0.11.0) opens the target with O_CREAT during mount setup. If the target's parent is a writable host path, bwrap creates a zero-byte stub on host before binding /dev/null over it; if intermediate parent directories are missing, bwrap creates those too. The stub persists after sandbox exit unless explicitly removed. Firejail's --blacklist does not have this requirement (the path may be absent at launch). Landlock does not enforce BLOCKED_FILES at all — file-level masking needs a mount namespace, which Landlock doesn't have.
To make masking visible and controllable, sandbox-exec.sh materializes every missing BLOCKED_FILES entry itself at config-load (after _apply_agent_profiles, before backend_prepare):
mkdir -p "$(dirname X)"for any missing parents,touch Xto create a zero-byte placeholder under user-controlled permissions,- one
WARNING:line per materialized path on stderr — and one per parent directory it had to create — so you can see exactly what host-side state the launcher touched.
If the materialization fails (non-writable parent like /etc, read-only mount), sandbox-exec.sh refuses to start and prints the full list of failing entries with a remediation hint. Silent skip would leave the path unenforced — the failure case is unrecoverable without user action, so we surface it.
Pass --cleanup-materialized (or set CLEANUP_MATERIALIZED_BLOCKED_FILES=1 in the environment or config) to have the launcher remove the placeholders post-exit. Cleanup is conservative: a file is removed only if it's still 0 bytes; a directory is removed only if it's still empty. Anything that grew (a real edit between launch and exit) is retained with a kept note on stderr.
To opt out of the protection for a specific entry, remove it from BLOCKED_FILES. To make an entry permanently exist (no warning on each launch), run mkdir -p "$(dirname X)" && touch X once outside the sandbox.
Devices¶
DEVICES¶
Type array · Admin-enforced additive; vetoed by DEVICES_BLACKLIST · Default
Device nodes to expose inside the sandbox. bwrap only — firejail and Landlock have their own device-handling models. Each entry is bind-mounted via bwrap --dev-bind PATH PATH after bwrap --dev /dev has set up the minimal devtmpfs.
Glob patterns are expanded against the host /dev at sandbox spawn time; entries that match nothing are silently dropped (so the NVIDIA defaults are a safe no-op on CPU-only nodes). After expansion, DEVICES_BLACKLIST is enforced — any resolved path matching a blacklist glob is dropped with a stderr notice.
The defaults expose the recurring HPC use case (NVIDIA driver nodes). Extend for AMD/Intel/sound/DRI/etc. as your workload requires:
To replace defaults entirely (uncommon):
See Device Passthrough for the full design rationale and per-backend behaviour.
DEVICES_BLACKLIST¶
Type array · Admin-enforced yes · Default
DEVICES_BLACKLIST=(
/dev/mem # direct kernel-memory access
/dev/kmem
/dev/port
/dev/pts # TIOCSTI keystroke injection on kernel < 6.2; also
# shadows bwrap's auto-mounted user-ns devpts on >= 5.4
/dev/sd* # raw block devices — filesystem bypass
/dev/nvme*
/dev/loop*
)
Devices that must not be bind-mounted, regardless of DEVICES. Admin baselines lock this in: users add but cannot remove admin-set entries. Without an admin install these defaults are the safety baseline.
To extend:
BIND_DEV_PTS (deprecated)¶
Type scalar · Admin-enforced no (deprecated, kernel-aware shim) · Default false
Historical knob: when true, bound the entire host /dev into the sandbox. Replaced by DEVICES. For backward compatibility BIND_DEV_PTS=true is shimmed at config-load time:
- On kernel < 5.4 it appends
/dev/ptstoDEVICES(the historical pty workaround —tmuxneeds the host devpts because bwrap's user-ns devpts on those kernels reportsptmxmode=000). - On kernel ≥ 5.4 it is a logged no-op. bwrap auto-mounts a working user-ns devpts on those kernels, and binding the host
/dev/ptson top would shadow it withptmxmode=000and silently break pty allocation (tmuxexits "create session failed";script(1)reports "Permission denied").
Migration: drop the line. On kernel ≥ 5.4 you do not need it; on kernel < 5.4 bwrap's auto-devpts is fine for pty in most cases. Only add DEVICES+=(/dev/pts) if you're on a pre-5.4 kernel and tmux fails inside the sandbox without it; expect the TIOCSTI security caveat (kernel < 6.2) in return.
Environment¶
BLOCKED_ENV_VARS¶
Type array · Admin-enforced yes · Default service-credential names not caught by BLOCKED_ENV_PATTERNS (e.g. GITHUB_PAT, AWS_ACCESS_KEY_ID, DATABASE_URL, PGPASSWORD, KRB5CCNAME, TMUX, OLDPWD, …)
Explicit env-var names to strip from the sandbox environment. Names like GITHUB_TOKEN, OPENAI_API_KEY, AWS_SESSION_TOKEN, SSH_* etc. are already matched by BLOCKED_ENV_PATTERNS globs and don't need duplicating here. Use this for credentials with non-standard names (no _TOKEN / _SECRET / _KEY suffix).
To audit your environment for entries that might slip through both sets:
BLOCKED_ENV_PATTERNS¶
Type array · Admin-enforced yes · Default
BLOCKED_ENV_PATTERNS=(
"SSH_*"
"*_TOKEN" "*_SECRET" "*_PASSWORD" "*_CREDENTIAL"
"*_API_KEY" "*_SECRET_KEY" "*_PRIVATE_KEY"
"AZURE_*" "GCP_*" "GCLOUD_*" "GOOGLE_CLOUD_*"
"DOCKER_*" "REGISTRY_*"
"CI_*" "GITLAB_*" "JENKINS_*" "BUILDKITE_*" "CIRCLECI_*"
)
Glob patterns that block any matching env var. Patterns catch the common credential conventions automatically. Use ALLOWED_ENV_VARS to exempt a specific variable matched by these patterns.
Case sensitivity. Patterns are case-sensitive by default. Append the trailing /i flag — the industry-standard regex convention from sed (s/.../.../i), Perl (qr/.../i), and JavaScript (/.../i) — to opt one entry into case-insensitive matching. Env-var names are restricted to [A-Za-z0-9_] (POSIX IEEE Std 1003.1 §8.1) and cannot legitimately contain /, so the suffix is unambiguous. Default remains case-sensitive so admin baselines upgrading across versions keep their existing semantics.
BLOCKED_ENV_PATTERNS+=(
"APP_SECRET_*" # only blocks APP_SECRET_X (uppercase exact)
"app_secret_*/i" # blocks APP_SECRET_X, app_secret_x, App_Secret_X, …
)
The startup banner (unless SANDBOX_QUIET=true) reports how many env vars were blocked by pattern, so missing vars are diagnosable without leaking the names of credentials.
ALLOWED_ENV_VARS¶
Type array · Admin-enforced additive · Default ("ANTHROPIC_API_KEY" "OPENAI_API_KEY" "CODEX_API_KEY" "GOOGLE_API_KEY")
Variables listed here are never blocked, even if they appear in BLOCKED_ENV_VARS or match a BLOCKED_ENV_PATTERNS glob. The agent-API-key defaults are enabled so agents that use env-var auth (Codex, Aider, OpenCode, Gemini) work on first launch. Comment out any line to block that variable instead.
ALLOWED_ENV_VARS+=(
"GITHUB_TOKEN" # for `gh` CLI inside the sandbox
"MY_APP_API_KEY" # site-specific
)
SANDBOX_ENV¶
Type array of KEY=VALUE strings · Admin-enforced additive · Default ()
Per-project environment variables applied to the host environment before the backend runs, so backend PATH prepends (chaperon stubs, sandbox bin) layer on top naturally. Set in conf.d/*.conf files guarded by _PROJECT_DIR so they only fire for the matching project.
# conf.d/genomics.conf
[[ "$_PROJECT_DIR" == /fh/fast/mylab/genomics/* ]] || return 0
SANDBOX_ENV+=(
"PATH=/fh/fast/mylab/genomics/bin:${PATH}"
"MY_PIPELINE_REF=/fh/fast/shared/reference_genomes/hg38"
)
Backend & isolation¶
SANDBOX_BACKEND¶
Type scalar · Admin-enforced no · Default auto (priority bwrap → firejail → landlock)
Which isolation backend to use. Overridable by --backend bwrap on the command line or by exporting SANDBOX_BACKEND in the environment — both override config-file values, since explicit selection should win over config defaults.
| Value | Backend | Notes |
|---|---|---|
auto (or empty) |
best available | bwrap → firejail → landlock |
bwrap |
Bubblewrap | primary, recommended dependency |
firejail |
Firejail | fallback (setuid root) |
landlock |
Landlock LSM | fallback (kernel ≥ 5.13, no mount/PID namespaces; documented gaps) |
SANDBOX_MODULES¶
Type array · Admin-enforced additive · Default ()
Lmod modules to load before backend detection. Use this on HPC systems where sandbox dependencies (e.g. a newer bubblewrap) are only available via module load. The sandbox sources lmod init from common locations (/etc/profile.d/lmod.sh, /usr/share/lmod/lmod/init/sh, /app/lmod/lmod/init/sh) if module isn't already on PATH.
PRIVATE_TMP¶
Type scalar · Admin-enforced harden-only (admin true is sticky) · Default true
Isolate /tmp with a private tmpfs. Each sandbox gets its own /tmp. Set to false if the sandboxed process needs shared /tmp access — MPI shared-memory transport (OpenMPI, MVAPICH) and NCCL inter-GPU sockets put files there.
Backend support: bwrap (--tmpfs /tmp) and firejail (--private-tmp). Landlock has no mount namespace — the value is honoured at config level but /tmp is not actually isolated; the docs site's Known Limitations table lists this as a Landlock gap.
PRIVATE_IPC¶
Type scalar · Admin-enforced harden-only · Default true
Isolate the SysV IPC namespace and /dev/shm. Each sandbox gets its own IPC namespace, preventing the agent from reading or corrupting shared memory of processes outside the sandbox. MPI/NCCL within a single Slurm job are unaffected — all ranks share one sandbox.
Backend support: bwrap (--unshare-ipc + private /dev/shm tmpfs) and firejail (--ipc-namespace). Landlock cannot isolate IPC.
FILTER_PASSWD¶
Type scalar · Admin-enforced harden-only · Default true
Generate a minimal /etc/passwd (system UIDs < 1000 + the current user) and override /etc/nsswitch.conf to use files only (no ldap/sss). Prevents LDAP/AD user enumeration via getent passwd, finger, etc. — getent passwd returns ~35 entries instead of every user on the directory.
Backend support:
- bwrap — overlays
/etc/passwd+/etc/nsswitch.confvia--ro-bind. - firejail — blocks NSS daemon sockets (
nscd,nslcd,sssd). - Landlock — not supported (no mount namespace; user enumeration succeeds).
Munge, Slurm, and normal user/group resolution are unaffected. Set false if the sandboxed process needs LDAP user lookups (rare).
On bwrap,
idshows supplementary groups asnogroup(65534) regardless of this setting. Cosmetic only — the kernel uses host credentials for filesystem access, so file permissions still work correctly.
SANDBOX_NPROC_LIMIT¶
Type scalar (integer or empty) · Admin-enforced no · Default "" (no limit)
Defense-in-depth against fork bombs. Caps the total processes the sandbox user can run via RLIMIT_NPROC (ulimit -u for bwrap/Landlock, firejail --rlimit-nproc for firejail). Note that RLIMIT_NPROC counts per-UID system-wide, not per-sandbox — a fork bomb inside the sandbox can fill the per-UID limit and kill the user's shells/editors outside the sandbox. Admin cgroups with pids.max are the primary defense; this is supplemental.
SANDBOX_QUIET¶
Type scalar · Admin-enforced no · Default false
Suppress the one-line startup banner that shows backend, project dir, and home-access mode, plus the count of env vars blocked by pattern. Useful inside scripts and CI where the banner is noise.
Network filter¶
Restricts the agent's outbound network access. Closes the canonical identity-hijack threat (local-MTA abuse via SMTP submission) and the adjacent lateral-movement / exfiltration surface (webhook-as-mail APIs, paste sites, DoH endpoints, legacy r-services). See the Network filter reference for the threat model, helper sourcing (pasta probe), per-backend support, precedence rules, worked examples, and a verification recipe.
NETWORK_FILTER_MODE¶
Type scalar · Admin-enforced harden-only (user can only request equal or stricter) · Default filtered
How the agent's outbound network is isolated.
| Value | Mechanism | Network reach |
|---|---|---|
open |
share the host network namespace; no isolation | full host network (legacy behaviour) |
filtered (default) |
new Linux network namespace + pasta helper; default-deny port floor plus user/admin NETWORK_BLOCKLIST |
general outbound TCP/UDP/DNS minus the threat-class ports |
proxied (new in 0.10.1) |
new netns with no native outbound; agent traffic is mediated by a host-side HTTP CONNECT + SOCKS5 daemon (tools/proxy/agent-sandbox-proxy.py) reached via bind-mounted Unix sockets + an in-sandbox TCP↔Unix bridge. HTTP_PROXY/HTTPS_PROXY/ALL_PROXY pre-set |
only what the proxy admits — proxy-aware tools (curl, pip, git, gh, conda, Claude SDK) work; raw TCP/UDP/ICMP, ssh direct, dig/nslookup, ping, bash /dev/tcp/* all break |
isolated |
new netns with no network at all | none (DNS / pip / git break inside the sandbox) |
filtered requires pasta on the host. agent-sandbox ships a static pasta binary at tools/pasta/<arch>/pasta (x86_64 in v1.1); install passt from your distro (apt install passt, dnf install passt, brew install passt) for a newer system pasta on PATH. When filtered cannot be delivered (no pasta, kernel < 5.7 without setcap cap_net_raw+ep, landlock backend), the resolver falls back per NETWORK_FILTER_FALLBACK.
proxied requires python3 on PATH and the in-tree helper tools/proxy/agent-sandbox-proxy.py (always present in a make install-ed deployment; no per-arch build). Bwrap only in v0.10.1; firejail/landlock unsupported. See Proxied mode for the enforcement surface and breakage list.
Strictness ordering: open < filtered < proxied < isolated. If the admin baseline pins a value, the user's effective value cannot be weaker; an attempted weakening is restored at config-load with a WARNING.
See the Modes section of the reference page for what pasta -T/-U actually enforces in v1.1 (port-level entries) versus what is tracked for v1.2 L7-proxy work (hostname-level entries).
NETWORK_FILTER_FALLBACK¶
Type scalar · Admin-enforced harden-only · Default open
What happens when the requested NETWORK_FILTER_MODE is not deliverable on the resolved backend.
| Value | Behaviour |
|---|---|
strict |
Refuse to launch. Loud error enumerating the fix paths. |
stricter |
Fall back ONLY to a STRICTER mode. Loud warning. Walks the strictness chain LEAST-strict-step-up first (smallest weakening), so a degraded-pasta host pinning MODE=filtered lands on proxied (v0.10.1+) before isolated. If no stricter mode is possible (landlock has no netns), refuse to launch. |
open (default) |
Fall back ONLY to a LESS restrictive mode, preferring the most-strict less-strict option first. NEVER strengthens — default-config users (MODE=filtered FALLBACK=open) on a degraded host fall to open, never silently to proxied. Loud warning. |
Strictness ordering: open < stricter < strict. Admin-pinned values cannot be weakened by the user.
The open default keeps the sandbox usable on legacy-kernel (< 5.7) deployments where pasta's SO_BINDTODEVICE forwarding probe trips and filtered becomes unavailable — without it, every such host would refuse to launch. Sites where the stronger posture is mandatory should pin stricter (or strict) in the admin baseline.
See the fallback decision matrix for the per-combination outcome under each policy, and the per-backend support matrix for which modes each backend can deliver.
NETWORK_BLOCKLIST¶
Type array · Admin-enforced yes (admin entries become a floor the user cannot remove) · Default built-in floor in sandbox-lib.sh — SMTP submission ports (24/25/465/587/2525) on loopback and 0.0.0.0/0, transactional-email HTTPS APIs (api.mailgun.net, api.sendgrid.com, …), webhook-as-mail surfaces (hooks.slack.com, *.webhook.office.com, …), anonymous file-drop hosts (transfer.sh, 0x0.st, …), public paste services, DoH endpoints (cloudflare-dns.com, dns.google, …), DoT port 853, and legacy r-services ports 23/79/113/512/513/514. See sandbox.conf for the commented, site-tunable entries (LDAP/Kerberos, Slurm, SMB/RDP/VNC, campus mail-relay CIDR).
Destinations to deny when NETWORK_FILTER_MODE is filtered. Entries are bash-glob-aware patterns over hostnames, IPs, CIDRs, and ports. The default floor is always applied; user += entries are additive; admin NETWORK_BLOCKLIST entries become a floor that user config cannot remove.
NETWORK_BLOCKLIST+=(
"*.untrusted-vendor.com" # block a DNS suffix
"10.0.0.0/8:25" # site-specific CIDR + port
"203.0.113.5" # single host, all ports
)
Pattern syntax (host / host:port / CIDR / CIDR:port / bare port / *.suffix / *), the runtime apply order, the most-specific-rule-wins precedence model, and worked examples live in the reference page: Pattern syntax and Precedence model.
v1.1 enforcement scope. pasta's -T/-U port-exclusion enforces port-level entries (bare port 25, loopback 127.0.0.1:25, universal 0.0.0.0/0:25). Hostname and wildcard-hostname entries (e.g. hooks.slack.com, *.s3.amazonaws.com) are tracked for v1.2 L7-proxy work and are silently skipped at the pasta boundary today (set NETWORK_FILTER_VERBOSE=1 to see the skip notes). The default port-class closure already shuts the motivating identity-hijack threat. See Modes — enforcement scope for the full list.
NETWORK_BLOCKLIST_EXCEPT¶
Type array · Admin-enforced additive; user entries covered by an admin-set NETWORK_BLOCKLIST pattern are stripped at config-load with a WARNING · Default ()
Exceptions that carve holes in NETWORK_BLOCKLIST under the most-specific-rule-wins precedence model. Use to allow specific destinations that would otherwise be caught by a broader blocklist pattern, or to express deny-by-default via the implicit-allowlist idiom.
# Block all of S3 except one bucket
NETWORK_BLOCKLIST+=("*.s3.amazonaws.com")
NETWORK_BLOCKLIST_EXCEPT+=("mybucket.s3.amazonaws.com")
# Implicit-allowlist idiom (deny-by-default)
NETWORK_BLOCKLIST+=("*")
NETWORK_BLOCKLIST_EXCEPT+=(
"github.com" "api.github.com" "codeload.github.com"
"api.anthropic.com" "api.openai.com"
"pypi.org" "files.pythonhosted.org"
)
Admin policy is absolute: a user exception covered by an admin-set NETWORK_BLOCKLIST pattern is stripped at config-load. See Admin precedence and Implicit-allowlist idiom on the reference page.
NETWORK_MAIL_BLOCK¶
Type scalar auto | on | off · Admin-enforced harden-only (user can only request equal or stricter; off < auto < on) · Default auto
Defense-in-depth above NETWORK_FILTER_MODE's port-level SMTP block. When active, the launcher replaces every canonical mailer binary inside the sandbox — sendmail, mail, mailx, mutt, msmtp, ssmtp, s-nail, swaks, the postfix admin tools, exim, dma, qmail clients — with a stub (tools/mail-block/mail-block-stub.sh) that prints a deterrent message to stderr and exits 77 (sysexits EX_NOPERM). The stub catches the execve syscall, so the agent learns the policy in human-readable terms before the network filter drops a connection.
| Value | Behaviour |
|---|---|
auto (default) |
on whenever the configured NETWORK_FILTER_MODE OR the resolved one is anything other than open. Tracks the user's intent, not only the post-fallback realised state: if NETWORK_FILTER_FALLBACK=open degrades a filtered request to open because pasta is missing, the configured intent ("constrain egress") still keeps the stub layer on — defense-in-depth earns its name precisely when the primary layer collapsed. Only when BOTH configured and resolved are open does the layer step aside. |
on |
always on, regardless of NETWORK_FILTER_MODE. |
off |
never on. Escape hatch for sites that legitimately need the canonical mailer binaries visible. Rare — the v0.10.0 port filter already breaks them at the socket layer. |
The two layers compose. The stub catches every UNIX tool that respects the sendmail interface or has a canonical binary name on PATH; the network filter catches the application-level remainder (python -c 'import smtplib...', curl smtp://, nc <host> 25). Evaluated CONFIG > NETWORK so the agent sees the policy text before the kernel drops the connection.
Mechanism: bind-mount the stub --ro-bind over every canonical absolute path that exists on the host (/usr/{bin,sbin}/<name>, /usr/lib/sendmail, /var/qmail/bin/qmail-*), plus a per-launch symlink farm of the same names under $TMPDIR that is --ro-bind'd at the same path on both sides of the sandbox boundary (mirroring the chaperon FIFO and proxy socket-dir pattern) and prepended to PATH so name-resolution finds the stub before any unstubbed copies (e.g. /usr/local/bin/<name>, Lmod-injected /app/software/.../bin/<name>).
Argv echo is sanitized: the stub strips bytes outside [:graph:] from basename "$0" and reports the argv count, never the args themselves — a hostile argv[0] containing ANSI / OSC-8 / CR / NUL bytes cannot rewrite the agent's terminal or smuggle clickable URLs into log scrapers.
# Default — auto picks the right thing for the active network mode
NETWORK_MAIL_BLOCK="auto"
# Always on, even when MODE=open
NETWORK_MAIL_BLOCK="on"
# Off (rare — document the use case)
NETWORK_MAIL_BLOCK="off"
Backend support: bwrap only in v0.10.1 (firejail and landlock fall back to no stub; the network filter still applies on the supported backends per their own matrix). See Outbound mail policy for the threat-model / mechanism / failure-mode write-up.
NETWORK_FILTER_SKIP_HELPER_PROBE¶
Type env override (scalar 0 / 1) · Admin-enforced no · Default 0
Skip the ~50 ms pasta-forwarding probe that runs at session-start to detect kernels where SO_BINDTODEVICE is gated behind CAP_NET_RAW (most kernels < 5.7 without the 2020 relaxation backported). Set only after verifying pasta's host-side forwarding actually works in your environment — typically after an admin has run setcap cap_net_raw+ep on a system-wide pasta binary, or on kernel ≥ 5.7.
Do not set this as a workaround for the
pasta degraded to loopback-onlyfallback warning. The probe exists precisely to catch that silent-loopback-only failure mode; skipping it re-introduces the gap wherefilteredmode would launch with the agent silently unable to reach anything except127.0.0.1. See Probe escape hatch for the full reasoning.
Slurm¶
SLURM_SCOPE¶
Type scalar · Admin-enforced no · Default project
Which jobs the chaperon-proxied squeue, scancel, scontrol, and sstat can see and operate on. The proxy filters output and validates targets against the configured scope.
| Value | What's visible / cancellable |
|---|---|
session |
Only jobs submitted by this sandbox session (one shell). |
project (default) |
Jobs from any sandbox session with the same project dir — survives reconnects, multi-window workflows. |
user |
All of the calling user's jobs, including non-sandbox ones. |
none |
No restriction (full access to your own jobs — squeue --me semantics). |
sacct is always scoped to the current user (--user=$(whoami) injected by the chaperon); --allusers and cross-user --user=... are denied with an actionable hint. sacctmgr user/account enumeration is denied entirely.
Override per-session via env: SLURM_SCOPE=session agent-sandbox claude.
CHAPERON_LOG_LEVEL¶
Type scalar · Admin-enforced no · Default info
Verbosity of the chaperon's per-session log file (one file per sandbox session at ~/.local/state/agent-sandbox/chaperon/<hostname>_<PID>_<timestamp>.log).
| Level | What lands in the log |
|---|---|
debug |
All requests, full handler exit codes, protocol details (script content captured). |
info (default) |
Startup, shutdown, each request, non-zero handler exits. |
warn |
Timeouts, validation failures, non-zero handler exits. |
error |
Only security rejections and hard failures. |
CHAPERON_LOG_RETAIN_DAYS¶
Type scalar (integer) · Admin-enforced no · Default 7
How many days of chaperon logs to keep. Older logs are pruned at each chaperon startup. A total size cap of 50 MiB is also enforced (oldest-first deletion when exceeded). Filenames include the hostname for NFS-safe uniqueness across machines.
Agent profiles¶
ENABLED_AGENTS¶
Type array · Admin-enforced additive · Default ("claude" "codex" "gemini")
Names of agent profiles in agents/<name>/ to enable. Each enabled agent contributes:
- Writable paths (e.g.
~/.claude,~/.codex) folded intoHOME_WRITABLE. - Read-only paths folded into
HOME_READONLY. - Per-agent instruction files folded into
BLOCKED_FILES(the overlay then exports a*_CONFIG_DIRenv var pointing at the sandbox-merged copy).
Disabled agents contribute nothing — their config dirs stay invisible inside the sandbox. Enable only the agents you actually use, so ~/.pi or ~/.config/opencode (which could be unrelated user data) doesn't become writable for users who don't run those agents.
Built-in profiles: claude, codex, gemini (default-enabled), aider, opencode, pi (opt-in).
Adding support for a new tool: drop in agents/<name>/{config.conf,overlay.sh,agent.md}, then add "<name>" to this list. See agents/claude/config.conf for the schema.
SUPPRESS_AGENT_WARNINGS¶
Type array · Admin-enforced additive · Default ()
Silence per-agent credential/path warnings emitted at startup. The sandbox checks each enabled agent profile and warns if the declared credentials/paths look unreachable (missing env vars and no writable auth directory). Useful when you intentionally isolate an agent (e.g. dropped ~/.claude from HOME_WRITABLE to force a fresh login each session).
SUPPRESS_AGENT_WARNINGS=("claude") # silence Claude only
SUPPRESS_AGENT_WARNINGS=("all") # silence every agent
Per-project overrides (conf.d/)¶
Different projects often need different data access. Drop files in ~/.config/agent-sandbox/conf.d/*.conf to add mounts only when the project directory matches. Each file is sourced in lexical order after sandbox.conf, so use += to append to the global arrays.
# conf.d/genomics.conf
[[ "$_PROJECT_DIR" == /fh/fast/mylab/genomics/* ]] || return 0
READONLY_MOUNTS+=(
"/fh/fast/shared/reference_genomes"
)
EXTRA_WRITABLE_PATHS+=(
"/fh/scratch/delete30/mylab/pipeline-output"
)
SANDBOX_ENV+=(
"MY_PIPELINE_REF=/fh/fast/shared/reference_genomes/hg38"
)
The _PROJECT_DIR variable is set by the sandbox before sourcing conf.d files. Returning early when the project doesn't match keeps the file a no-op for unrelated projects.
See conf.d/example.conf in the install for a template.
Common customizations¶
# Add a read-only data directory
READONLY_MOUNTS+=("/shared/other_lab/data")
# Add a writable output directory beyond the project dir
EXTRA_WRITABLE_PATHS=("/shared/scratch/agent-output")
# Block a sensitive subdirectory inside an otherwise-visible mount
EXTRA_BLOCKED_PATHS=("/shared/lab/clinical_restricted")
# Allow GitHub CLI inside the sandbox
HOME_READONLY+=(".config/gh")
ALLOWED_ENV_VARS+=("GITHUB_TOKEN" "GH_TOKEN")
# Open an extra device (audio + DRI render nodes)
DEVICES+=(/dev/snd /dev/dri/*)
SSH keys.
~/.sshis excluded fromHOME_READONLYby default — the agent cannot see it. Do not add it. On HPC clusters with passwordless SSH between nodes, an agent with access to~/.sshcan SSH to localhost for an unsandboxed shell. If the agent needs git access, prefer deploy keys scoped to a single repo, or HTTPS with a fine-grained token (add the token var toALLOWED_ENV_VARS).
Claude Code permissions (settings.json)¶
For Claude Code, the sandbox overlays ~/.claude/settings.json to auto-allow tools (Bash, Read, Edit, Write, Glob, Grep, NotebookEdit) that are already restricted by the kernel-enforced filesystem isolation. Your existing rules (including deny) are preserved. Customise via ~/.config/agent-sandbox/agents/claude/settings.json.
Cross-references¶
- Admin install layout & config hierarchy — where the admin baseline lives, who can write it, how it's protected.
- Device passthrough rationale — full design of
DEVICES/DEVICES_BLACKLISTand theBIND_DEV_PTSdeprecation. - Chaperon protocol — exactly what each Slurm stub allows and denies, with examples.
- Architecture reference — per-resource isolation matrix across backends.
- Sandbox config guide for in-sandbox agents — the same content from the agent's perspective (the file an agent reads from inside the sandbox to know how to ask the user for permission grants).