Skip to content

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_READONLYHOME_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).

ALLOWED_PROJECT_PARENTS=(
    "$HOME"
    "/data/myorg"
    "/scratch/myorg"
)

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_WRITABLE+=(
    ".cache"
    ".my_tool_state"
)

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).
HOME_SEEDED_FILES=(
    ".gitconfig"
    ".npmrc"
    ".yarnrc"
)

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.

EXTRA_WRITABLE_PATHS=(
    "/fh/scratch/delete30/mylab/agent-output"
)

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).

EXTRA_BLOCKED_PATHS=(
    "/fh/fast/setty_m/restricted_clinical_data"
)

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 X to 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.

BLOCKED_FILES+=(
    "$HOME/notes/secret.md"
)

Devices

DEVICES

Type array · Admin-enforced additive; vetoed by DEVICES_BLACKLIST · Default

DEVICES=(
    /dev/nvidia*
    /dev/nvidia-uvm
    /dev/nvidia-uvm-tools
    /dev/nvidia-modeset
    /dev/nvidiactl
)

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:

DEVICES+=(/dev/snd /dev/dri/* /dev/kvm)

To replace defaults entirely (uncommon):

DEVICES=(/dev/something-specific)

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:

DEVICES_BLACKLIST+=(/dev/fuse)

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/pts to DEVICES (the historical pty workaround — tmux needs the host devpts because bwrap's user-ns devpts on those kernels reports ptmxmode=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/pts on top would shadow it with ptmxmode=000 and silently break pty allocation (tmux exits "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:

env | grep -iE 'token|key|secret|pat|auth'

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_BACKEND="bwrap"          # force bwrap; fail if unavailable

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.

SANDBOX_MODULES=("bubblewrap/0.11.1-GCCcore-12.3.0")

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.conf via --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, id shows supplementary groups as nogroup (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_NPROC_LIMIT="4096"

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.

NETWORK_FILTER_MODE="filtered"          # default — port-level enforcement

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.

NETWORK_FILTER_SKIP_HELPER_PROBE=1 agent-sandbox bash

Do not set this as a workaround for the pasta degraded to loopback-only fallback warning. The probe exists precisely to catch that silent-loopback-only failure mode; skipping it re-introduces the gap where filtered mode would launch with the agent silently unable to reach anything except 127.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 into HOME_WRITABLE.
  • Read-only paths folded into HOME_READONLY.
  • Per-agent instruction files folded into BLOCKED_FILES (the overlay then exports a *_CONFIG_DIR env 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).

ENABLED_AGENTS+=("aider")              # add to defaults
ENABLED_AGENTS=("claude")              # solo-claude profile

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. ~/.ssh is excluded from HOME_READONLY by default — the agent cannot see it. Do not add it. On HPC clusters with passwordless SSH between nodes, an agent with access to ~/.ssh can 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 to ALLOWED_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