Last active
February 7, 2026 02:24
-
-
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 | |
| # --- Re-run inside WSL when invoked from Windows shells --- | |
| if [[ -z "${WSL_INTEROP-}" && -z "${WSL_DISTRO_NAME-}" ]]; then | |
| exec wsl -d Ubuntu-24.04 -- /bin/bash -lc "aijail $*" | |
| fi | |
| # --- 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") | |
| # workdir="/work/$(basename "$PWD")" | |
| workdir="$PWD" | |
| # Setup JAIL Root | |
| JAIL="$(mktemp -d "${TMPDIR:-/tmp}/aijail.XXXXXX")" | |
| CHROOT="$JAIL/chroot" | |
| mkdir -p "$CHROOT" | |
| echo "[debug] JAIL: $JAIL" >&2 | |
| # --- REDACTION SETUP --- | |
| redacted_file="$JAIL/redacted_file.txt" | |
| redacted_dir="$JAIL/redacted_dir" | |
| cat > "$redacted_file" <<'EOF' | |
| This file is read-only and its contents have been intentionally redacted to prevent exposure of potentially sensitive information. | |
| EOF | |
| mkdir -p "$redacted_dir" | |
| cat > "$redacted_dir/REDACTED.txt" <<'EOF' | |
| This directory is intentionally presented as empty and made read-only to prevent exposure of potentially sensitive information. | |
| The original contents exist but are not available in this environment. | |
| EOF | |
| chmod 444 "$redacted_file" "$redacted_dir/REDACTED.txt" | |
| chmod 555 "$redacted_dir" | |
| # --- 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" | |
| ns_args=( | |
| --mode o | |
| --time_limit 0 | |
| --skip_setsid | |
| --forward_signals | |
| # --- 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 | |
| --bindmount_ro /sys/fs/cgroup:/sys/fs/cgroup | |
| ) | |
| # 2. Construct Explicit PATH | |
| # Start with standard system paths (excluding sbin as requested) | |
| JAIL_PATH="/usr/local/bin:/usr/bin:/bin" | |
| if [[ -d /snap/bin ]]; then | |
| JAIL_PATH="$JAIL_PATH:/snap/bin"; | |
| ns_args+=( --bindmount_ro "/snap:/snap" ) | |
| 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 | |
| # Prepend User Local Bin | |
| if [[ -d "$HOME/.local/bin" ]]; then JAIL_PATH="$HOME/.local/bin:$JAIL_PATH"; fi | |
| # 3. Base Arguments | |
| ns_args+=( | |
| # --- ENVIRONMENT --- | |
| --env TERM="${TERM:-xterm-256color}" | |
| --env HOME="$HOME" | |
| --env USER="$USERNAME" | |
| --env LANG="${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 | |
| # 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' | |
| --mount "none:$HOME:tmpfs:size=256m" | |
| # --- WORKSPACE MOUNT (Read-Write) --- | |
| --bindmount "$PWD:$workdir" | |
| --cwd "$workdir" | |
| # --- NAMESPACES --- | |
| --disable_clone_newnet | |
| --disable_clone_newpid | |
| --disable_clone_newipc | |
| --disable_clone_newuts | |
| --disable_clone_newcgroup | |
| --disable_proc | |
| # --- RELAXATION --- | |
| --disable_rlimits | |
| ) | |
| echo "[debug] Searching for sensitive files..." >&2 | |
| # Redact potentially sensitive files and directories in the workspace. | |
| while IFS= read -r rel_path; do | |
| [[ -z "$rel_path" ]] && continue | |
| if [[ "$rel_path" == */ ]]; then | |
| rel_path="${rel_path%/}" | |
| if [[ -d "$PWD/$rel_path" ]]; then | |
| ns_args+=( --bindmount_ro "$redacted_dir:$workdir/$rel_path" ) | |
| fi | |
| else | |
| if [[ -f "$PWD/$rel_path" ]]; then | |
| ns_args+=( --bindmount_ro "$redacted_file:$workdir/$rel_path" ) | |
| fi | |
| fi | |
| done < <( | |
| cd "$PWD" && find . \ | |
| \( -type d \( \ | |
| -name node_modules -o -name vendor -o -name .hg -o -name .git -o -name .yarn \ | |
| \) -prune \) -o \ | |
| \( -type d \( \ | |
| -iname '*hidden*' -o -iname '*secret*' -o -iname '*password*' -o -iname '*production*' \ | |
| -o -iname '*apikey*' -o -iname '*api-key*' -o -iname '*accesskey*' -o -iname '*access-key*' \ | |
| -o -iname '*token*' -o -iname '*bearer*' -o -iname '*oauth*' -o -iname '*jwt*' \ | |
| -o -iname '*credential*' -o -iname '*creds*' -o -iname '*passphrase*' -o -iname '*private*' \ | |
| -o -iname '*ssh*' -o -iname '*kubeconfig*' \ | |
| -o -iname '*.bak' -o -name '.idea' \ | |
| \) -printf '%P/\n' -prune \) -o \ | |
| \( -type f ! \( \ | |
| -iname '*.jsx' -o -iname '*.tsx' -o -iname '*.css' -o -iname '*.svg' -o -iname '*.twig' -o -iname '*.blade.php' -o -iname '*.html' -o -iname '*.md' \ | |
| -o -iname '*.example' -o -iname '*.example.*' -o -iname '*.sample' -o -iname '*.sample.*' -o -iname '*schema*.sql' \ | |
| -o -iname '.local' -o -iname '.development' \ | |
| \) \ | |
| \( \ | |
| -iname '*hidden*' -o -iname '*secret*' -o -iname '*password*' -o -iname '*production*' \ | |
| -o -iname '*apikey*' -o -iname '*api-key*' -o -iname '*accesskey*' -o -iname '*access-key*' \ | |
| -o -iname '*token*' -o -iname '*bearer*' -o -iname '*oauth*' -o -iname '*jwt*' \ | |
| -o -iname '*credential*' -o -iname '*creds*' -o -iname '*passphrase*' -o -iname '*private*' \ | |
| -o -iname '*ssh*' -o -iname '*kubeconfig*' \ | |
| -o -iname '*.env' -o -iname '*.env.*' \ | |
| -o -iname '*.pem' -o -iname '*.key' -o -iname '*.p12' -o -iname '*.pfx' -o -iname '*.jks' -o -iname '*.keystore' -o -iname '*.truststore' \ | |
| -o -iname '*dump*.sql' -o -iname '*data*.sql' \ | |
| -o -iname '*.tar' -o -iname '*.tgz' -o -iname '*.zip' -o -iname '*.7z' -o -iname '*.gz' \ | |
| \) \ | |
| -printf '%P\n' \ | |
| \) | tee >(cat >&2) | |
| ) | |
| # echo "[debug] Done" >&2 | |
| # If the current directory is a VCS repo, make metadata read-only. | |
| vcs_dirs=( | |
| ".git" | |
| ".hg" | |
| ".jj" | |
| ) | |
| for vcs_dir in "${vcs_dirs[@]}"; do | |
| if [[ -d "$PWD/$vcs_dir" ]]; then | |
| ns_args+=( --bindmount_ro "$PWD/$vcs_dir:$workdir/$vcs_dir" ) | |
| fi | |
| done | |
| # 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 | |
| echo "[debug] Starting nsjail" >&2 | |
| 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
Sensitive files are now readonly mounted over so the LLM can't read them.