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
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
Author
Sensitive files are now readonly mounted over so the LLM can't read them.
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 too if you want a safe-ish place to run arbitrary commands.