Last active
January 13, 2026 09:40
-
-
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")" | |
| # 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="${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 | |
| ) | |
| # 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:$PWD/$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 | |
| 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
Updated the script to make
.hg,.git,.jjreadonly. I had an LLM randomly start committing stuff before and it royally messed up my repo. Now as long as you commit before starting codex, it shouldn't be able to wreak too much havoc.