Skip to content

Instantly share code, notes, and snippets.

@mnpenner
Last active January 13, 2026 09:40
Show Gist options
  • Select an option

  • Save mnpenner/137ccdccf7619dfc403c53ddd1b626e6 to your computer and use it in GitHub Desktop.

Select an option

Save mnpenner/137ccdccf7619dfc403c53ddd1b626e6 to your computer and use it in GitHub Desktop.
A jail for LLMs
#!/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}"
}
@mnpenner
Copy link
Author

mnpenner commented Dec 15, 2025

This depends on nsjail: https://github.com/google/nsjail?tab=readme-ov-file#installation

Usage:

aijail.sh codex

Or whatever your favorite LLM CLI is. Works with bash to if you want a safe-ish place to run arbitrary commands.

@mnpenner
Copy link
Author

Updated the script to make .hg, .git, .jj readonly. 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.

mpen:/mnt/c/Users/Mark/PhpstormProjects/nsjail$ echo hello > foo
mpen:/mnt/c/Users/Mark/PhpstormProjects/nsjail$ cat foo
hello
mpen:/mnt/c/Users/Mark/PhpstormProjects/nsjail$ hg status
? foo
mpen:/mnt/c/Users/Mark/PhpstormProjects/nsjail$ hg ci -m "commit"
abort: could not lock working directory of /mnt/c/Users/Mark/PhpstormProjects/nsjail: Read-only file system

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment