Last active
December 15, 2025 13:00
-
-
Save mnpenner/137ccdccf7619dfc403c53ddd1b626e6 to your computer and use it in GitHub Desktop.
A jail for LLMs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| { | |
| set -euo pipefail | |
| # --- Verify HOME (we bindmount a bunch of $HOME paths) --- | |
| if [[ -z "${HOME-}" || "$HOME" == "/" ]]; then | |
| echo "[error] HOME is empty/invalid: '${HOME-}'" >&2 | |
| exit 1 | |
| fi | |
| # --- USER fallback --- | |
| USERNAME="${USER-}" | |
| if [[ -z "${USERNAME}" ]]; then | |
| USERNAME="$(whoami)" | |
| fi | |
| # 1. Setup | |
| exec_file="$(command -v "${1:-$SHELL}")" | |
| real_path="$(readlink -f "$exec_file")" | |
| bin_name="$(basename "$exec_file")" | |
| # Setup JAIL Root | |
| JAIL="$(mktemp -d "${TMPDIR:-/tmp}/aijail.XXXXXX")" | |
| CHROOT="$JAIL/mnt" | |
| mkdir -p "$CHROOT" | |
| echo "[debug] JAIL: $JAIL" >&2 | |
| # --- SETUP JAIL /etc --- | |
| mkdir -p "$CHROOT/etc" | |
| # --- FAKE /etc/passwd and /etc/group --- | |
| uid="$(id -u)" | |
| gid="$(id -g)" | |
| cat > "$CHROOT/etc/passwd" <<EOF | |
| root:x:0:0:root:/root:/usr/sbin/nologin | |
| $USERNAME:x:$uid:$gid::$HOME:/bin/bash | |
| nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin | |
| EOF | |
| cat > "$CHROOT/etc/group" <<EOF | |
| root:x:0: | |
| $USERNAME:x:$gid: | |
| nogroup:x:65534: | |
| EOF | |
| # --- SETUP JAIL HOME --- | |
| JAIL_HOME="$CHROOT$HOME" | |
| mkdir -p "$JAIL_HOME" | |
| # 2. Construct Explicit PATH | |
| # Start with standard system paths (excluding sbin as requested) | |
| JAIL_PATH="/usr/local/bin:/usr/bin:/bin" | |
| # Prepend User Local Bin | |
| if [[ -d "$HOME/.local/bin" ]]; then JAIL_PATH="$HOME/.local/bin:$JAIL_PATH"; fi | |
| # Prepend Common Runtimes | |
| if [[ -d "$HOME/.cargo/bin" ]]; then JAIL_PATH="$HOME/.cargo/bin:$JAIL_PATH"; fi | |
| if [[ -d "$HOME/.bun/bin" ]]; then JAIL_PATH="$HOME/.bun/bin:$JAIL_PATH"; fi | |
| # Prepend Active Node/NVM Path | |
| HOST_NODE="$(command -v node 2>/dev/null || true)" | |
| if [[ -n "$HOST_NODE" ]]; then | |
| NODE_DIR="$(dirname "$HOST_NODE")" | |
| if [[ "$NODE_DIR" != "/usr/bin" && "$NODE_DIR" != "/bin" ]]; then | |
| JAIL_PATH="$NODE_DIR:$JAIL_PATH" | |
| fi | |
| fi | |
| # 3. Base Arguments | |
| ns_args=( | |
| --mode o | |
| --time_limit 0 | |
| --skip_setsid | |
| --forward_signals | |
| # --- ENVIRONMENT --- | |
| --env TERM="${TERM:-xterm-256color}" | |
| --env HOME="$HOME" | |
| --env USER="$USERNAME" | |
| --env LANG=C.UTF-8 | |
| --env 'PS1=\u:\w\$ ' | |
| # Explicit PATH | |
| --env PATH="$JAIL_PATH" | |
| # NVM Support vars | |
| --env NVM_DIR | |
| --env NVM_INC | |
| --env NVM_BIN | |
| # --- FILESYSTEM STRATEGY: STRICT CHROOT --- | |
| --chroot "$CHROOT" | |
| # --- SYSTEM MOUNTS (Read-Only) --- | |
| --bindmount_ro /usr:/usr | |
| --bindmount_ro /bin:/bin | |
| --bindmount_ro /lib:/lib | |
| --bindmount_ro /lib64:/lib64 | |
| --bindmount_ro /boot:/boot | |
| # DNS + NSS (host truth) | |
| --bindmount_ro /etc/resolv.conf:/etc/resolv.conf | |
| --bindmount_ro /etc/hosts:/etc/hosts | |
| --bindmount_ro /etc/nsswitch.conf:/etc/nsswitch.conf | |
| # Time / locale (optional but sane) | |
| --bindmount_ro /etc/localtime:/etc/localtime | |
| --bindmount_ro /etc/timezone:/etc/timezone | |
| --bindmount_ro /etc/os-release:/etc/os-release | |
| # /etc extras | |
| --bindmount_ro /etc/ssl:/etc/ssl | |
| #--bindmount_ro /etc/alternatives:/etc/alternatives | |
| #--bindmount_ro /etc/fonts:/etc/fonts | |
| # --- RUNTIME MOUNTS (Read-Write) --- | |
| --mount 'none:/dev:tmpfs' | |
| --mount 'devpts:/dev/pts:devpts' | |
| --bindmount /dev/null:/dev/null | |
| --bindmount /dev/zero:/dev/zero | |
| --bindmount /dev/random:/dev/random | |
| --bindmount /dev/urandom:/dev/urandom | |
| --bindmount /proc:/proc | |
| --mount 'none:/tmp:tmpfs:size=512m' | |
| --bindmount /var:/var | |
| --bindmount /run:/run | |
| --mount 'none:/dev/shm:tmpfs:size=256m' | |
| # --- WORKSPACE MOUNT (Read-Write) --- | |
| --bindmount "$PWD:$PWD" | |
| --cwd "$PWD" | |
| # --- NAMESPACES --- | |
| --disable_clone_newnet | |
| --disable_clone_newpid | |
| --disable_clone_newipc | |
| --disable_clone_newuts | |
| --disable_clone_newcgroup | |
| --disable_proc | |
| # --- RELAXATION --- | |
| --disable_rlimits | |
| ) | |
| # 4. Mount User Configs (Split RO vs RW) | |
| # List A: READ-ONLY (Tools & Global Configs) | |
| ro_items=( | |
| ".nvm" | |
| ".bun" | |
| ".cargo" | |
| ".rustup" | |
| ".local" | |
| ".config" | |
| ".gitconfig" | |
| ) | |
| for item in "${ro_items[@]}"; do | |
| if [[ -e "$HOME/$item" ]]; then | |
| ns_args+=( --bindmount_ro "$HOME/$item:$HOME/$item" ) | |
| fi | |
| done | |
| # List B: READ-WRITE (State, Logs, Caches) | |
| rw_items=( | |
| ".codex" | |
| ".npm" | |
| ".cache" | |
| ".local/state" | |
| ".local/share" | |
| ".config/github-copilot" | |
| ) | |
| for item in "${rw_items[@]}"; do | |
| if [[ -e "$HOME/$item" ]]; then | |
| ns_args+=( --bindmount "$HOME/$item:$HOME/$item" ) | |
| fi | |
| done | |
| # Codex API Key | |
| if [[ "$bin_name" == "codex" ]]; then | |
| if [[ -n "${OPENAI_API_KEY-}" ]]; then | |
| ns_args+=( --env OPENAI_API_KEY ) | |
| fi | |
| fi | |
| # 5. Run | |
| nsjail "${ns_args[@]}" -- "$real_path" "${@:2}" | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This depends on nsjail: https://github.com/google/nsjail?tab=readme-ov-file#installation
Usage:
Or whatever your favorite LLM CLI is. Works with bash to if you want a safe-ish place to run arbitrary commands.