Sandboxing AI Coding Agents with FreeBSD Jails
A two-machine architecture for pragmatic agent isolation

Table of Contents

Impressionist painting depicting two buildings connected by a one-way beam of light, representing the restricted host and sandbox host architecture

1. Abstract

AI coding agents need broad system access to be useful – filesystem, network, git, build tools – but granting that access on a shared machine creates security risks ranging from credential exposure to lateral movement. This paper presents a practical, implemented architecture using two commodity FreeBSD machines and three layers of isolation: a restricted host for operator sessions, a sandbox host with FreeBSD thin jails per agent identity, and a one-way SSH bulkhead between them. Total hardware cost is under $400. We describe the trust model, demonstrate verified isolation properties, and provide a step-by-step deployment guide.

2. The Problem

Modern AI coding agents (Claude Code, Copilot Workspace, Cursor, Aider, etc.) require:

  • Read/write access to source trees
  • Build and test execution (compilers, interpreters, test runners)
  • Git operations including push to remote forges
  • API access to forge APIs, LLM APIs, and package registries
  • Sometimes: package installation, service management, system configuration

Running these agents on a developer's primary machine means an agent error, hallucination, or adversarial prompt injection could read SSH keys, API tokens, browser sessions, or credentials for unrelated services. On a shared machine, one agent session can observe or interfere with another.

Existing container solutions (Docker, Podman) provide process isolation but typically run on the developer's primary machine, leaving the fundamental trust boundary problem unsolved. Cloud VMs add latency and recurring cost.

3. Architecture

3.1. Two Machines, One Direction

graph TD
    dev["Operator Laptop"]

    subgraph LAN["Local Network"]
        restricted["Restricted Host<br/>(no root, scoped creds)<br/>Operator sessions"]
        sandbox["Sandbox Host<br/>(root, jails)<br/>Agent workloads"]
    end

    subgraph Forges["External Forges"]
        forge1["Forge A"]
        forge2["Forge B"]
    end

    dev -->|"SSH / TRAMP"| restricted
    restricted -->|"SSH (one-way)"| sandbox
    sandbox -.-x|"BLOCKED"| restricted
    restricted --> forge1
    sandbox --> forge2

    style restricted fill:#2d5016,color:#fff
    style sandbox fill:#8b1a1a,color:#fff
  • Restricted host: Where the operator works. Runs editor, git, agent CLI. Has forge credentials but no root access. Cannot install packages or modify system files.
  • Sandbox host: Where agent workloads execute. Has root (for package installation, jail management). Runs one thin jail per agent identity, each with scoped credentials and resource limits.
  • Bulkhead: The sandbox host cannot SSH back to the restricted host. Enforced by firewall rules and absence of authorized keys. If the sandbox is compromised, the restricted host (where real credentials live) is unreachable.

3.2. Identity Layering

Each agent identity is isolated at multiple levels:

Level Mechanism What It Provides
1. Unix user adduser agent-N File permission boundaries, own home dir
2. Credentials Scoped tokens per user Agent can only access its own forge org
3. Thin jail Bastille/jail(8) Kernel-enforced filesystem, process, network isolation
4. ZFS quota zfs set quota=5G Disk usage limits per jail
5. rctl rctl -a jail:...:memoryuse:deny=2G CPU, memory, process count limits (requires reboot)
6. pf firewall Egress allowlist Jails can only reach forge hosts and pkg mirrors

3.2.1. What Are Thin Jails?

Thin jails share the base FreeBSD system read-only via NullFS mounts. Each jail only stores its delta – packages, configuration, and user data.

Component Storage
Base system (shared) ~459MB (mounted read-only)
Per-jail delta ~380MB (packages + config)
Total for 5 jails + base ~2.8GB

This is why jail creation takes ~3 seconds instead of minutes – there's no full OS copy, just a ZFS dataset and NullFS mount. On a host with 379GB free, you can run dozens of agents without storage concerns.

3.2.2. Credential Scoping

Each identity has its own:

~agent-N/
├── .config/gh/hosts.yml    # Forge CLI auth (scoped token)
├── .gitconfig              # Identity (name, email)
├── .ssh/id_ed25519         # SSH key (registered on specific forge org)
├── .claude/                # AI agent config and auth
│   └── .credentials.json
└── ~/ghq/                  # Repository tree

The operator's credentials are never copied into agent jails. Each agent gets the minimum credentials needed for its forge org.

3.3. The --dangerously-skip-permissions Trust Model

Claude Code's --dangerously-skip-permissions flag removes the interactive confirmation prompts that normally gate file writes, command execution, and network requests. On an unprotected machine, this flag is genuinely dangerous – an agent error could rm -rf your home directory, exfiltrate credentials, or install malware.

Inside a Bastille jail, the calculus changes:

Risk Without Jail With Jail
Delete files Entire home dir Only jail's ZFS dataset (5G quota)
Read credentials All ~/.ssh, ~/.config Only scoped jail credentials
Install packages System-wide Jail-local only
Network exfiltration Unrestricted pf egress rules (ports 22/80/443 only)
Lateral movement SSH to other machines Cannot reach restricted host
Persistence Survives reboot zfs rollback reverts everything

The flag goes from "dangerous" to "appropriate" once the jail provides the containment that the flag removes. The jail is the permission boundary; the flag just removes the ceremony.

Inside jails, we alias:

alias claude='claude --dangerously-skip-permissions'

This is arguably the paper's most practical insight: the same flag that's reckless on a developer laptop becomes reasonable when the jail constrains its blast radius to a ZFS dataset with a 5G quota and pf egress rules.

3.4. Command Hierarchy

Three levels of access, each with different capabilities:

3.4.1. Level 0: Operator (restricted host)

# Direct access to all forge credentials
ssh -T git@forge-a.com          # Authenticated
gh api user                     # Full token scope

# Can shell into sandbox host
ssh sandbox-host

# Cannot install packages or modify system
sudo pkg install foo            # DENIED (no sudo)

3.4.2. Level 1: Operator on Sandbox (host user, sudo)

# Manage jails
sudo bastille list              # See all jails
sudo bastille console agent-N   # Enter a jail
sudo bastille cmd agent-N CMD   # Run command in jail

# Switch to agent Unix user (non-jailed, for testing)
sudo su - agent-N               # Full host access as that user

# Install packages, manage services
sudo pkg install foo            # Works (has sudo)

3.4.3. Level 2: Agent (inside jail)

# Scoped forge access
gh api user                     # Only sees agent's forge org
git clone https://...           # Works (HTTPS allowed by pf)

# AI agent sessions
claude                          # Runs with jailed credentials

# Cannot escape
ps aux                          # Only sees jail's own processes (~5)
ls /home                        # Empty (host /home not mounted)
ping anything                   # DENIED (raw sockets disabled)
mount anything                  # DENIED (jail restriction)

4. Isolation Properties

Verified on a running implementation (FreeBSD 14.3, Bastille 1.3.2):

4.1. What Jails Prevent

Attack Vector Blocked? Mechanism
Read host /home Yes Jail root is isolated filesystem
See host processes Yes Kernel process namespace
Read other jail credentials Yes Other jail paths don't exist
Raw sockets (network scan) Yes Kernel blocks raw sockets in jails
Mount filesystems Yes Kernel blocks mount in jails
Access non-allowed ports Yes pf egress rules
Consume unlimited disk Yes ZFS quotas (5G default)
Consume unlimited CPU/RAM Yes rctl limits (after RACCT enable)

4.2. What Jails Allow (by design)

Capability How
Git clone/push (HTTPS) pf allows port 443 outbound
Forge API calls pf allows port 443 outbound
SSH to forges pf allows port 22 outbound
DNS resolution pf allows port 53 outbound
Package installation bastille pkg from host
AI agent sessions Claude Code runs inside jail

4.3. Known Limitations

  • A 2025 security audit found ~50 kernel vulnerabilities enabling jail escapes. Jails defend against accidents and casual attacks, not sophisticated adversaries targeting the jail boundary.
  • The network bulkhead (sandbox cannot reach restricted host) is the primary security boundary, not the jail itself.
  • Credential copying is manual. No automated sync or rotation.

4.4. FreeBSD Jail Gotchas for AI Agents

Running AI coding agents (Claude Code, etc.) inside FreeBSD jails requires specific configuration that isn't obvious from standard jail documentation:

4.4.1. bash is Required

FreeBSD jails default to /bin/sh as root shell. Claude Code requires bash and checks the SHELL environment variable. Without it:

Error: No suitable shell found. Claude CLI requires a Posix shell
environment. Please ensure you have a valid shell installed and the
SHELL environment variable set.

Fix:

sudo bastille pkg <jail> install -y bash
sudo bastille cmd <jail> chsh -s /usr/local/bin/bash root
echo 'export SHELL=/usr/local/bin/bash' >> /root/.profile
echo 'export SHELL=/usr/local/bin/bash' >> /root/.bashrc

4.4.2. gmake vs make

FreeBSD's system make is BSD make, not GNU make. Most project Makefiles (especially those from GitHub) require GNU make. Install gmake and use it everywhere, or alias it:

sudo bastille pkg <jail> install -y gmake
# In jail: gmake check, gmake build, etc.
# Or: echo 'alias make=gmake' >> /root/.bashrc

4.4.3. npm Global Installs

npm install -g works inside jails but the global prefix must be writable. Since jail root is actual root (scoped to the jail), this works by default. If running as a non-root jail user, set:

mkdir -p ~/.npm-global
npm config set prefix '~/.npm-global'
echo 'export PATH=~/.npm-global/bin:$PATH' >> ~/.bashrc

4.4.4. ghq Clone Sync

Jails start with empty home directories. Use a sync script to clone repos from the agent's forge account:

#!/bin/sh
# ghq-sync.sh - run inside jail
repos=$(gh repo list <org> --json name --jq '.[].name')
for repo in $repos; do
    ghq get https://github.com/<org>/$repo 2>/dev/null || true
done

4.4.5. DNS Resolution

Jails inherit /etc/resolv.conf from the host at creation time. If the host uses Tailscale DNS (100.100.100.100), the jail will too. If DNS stops working after jail creation, regenerate:

sudo bastille cmd <jail> sh -c 'echo "nameserver 8.8.8.8" > /etc/resolv.conf'

4.4.6. Concurrent Agent Sessions

Two agent sessions using the same forge identity (same GitHub token) from different jails will work – forge APIs are stateless. However, if both push to the same repo branch simultaneously, you'll get conflicts. Scope each agent to different repos or branches.

5. Deployment

5.1. Hardware

Any two x86-64 machines with 16GB+ RAM and NVMe storage. Mini PCs work well. Total cost ~$300-400.

Role Example Hardware Cost
Restricted host AZW/Beelink mini PC ~$170
Sandbox host AZW/Beelink mini PC ~$170

5.2. Software Stack

Component Purpose
FreeBSD 14.3+ Operating system (both hosts)
Bastille Jail management
ZFS Filesystem with quotas and snapshots
pf Firewall with NAT and egress filtering
rctl/RACCT Resource limits per jail
gh, git, ghq Forge CLI tools
bd (beads) Git-backed issue tracker for agent task coordination
node, npm For Claude Code
go Build bd and other Go tools (host only)
guile, emacs, python Agent toolchain (varies by identity)

5.3. Step-by-Step

5.3.1. 1. Sandbox Host Setup

# Install Bastille
sudo pkg install -y bastille

# Configure ZFS-backed jails
sudo sysrc bastille_enable=YES
sudo sysrc -f /usr/local/etc/bastille/bastille.conf \
    bastille_zfs_enable=YES bastille_zfs_zpool=zroot

# Create loopback interface for jails
sudo sysrc cloned_interfaces='lo1' ifconfig_lo1_name='bastille0'
sudo service netif cloneup

# Enable pf and IP forwarding
sudo sysrc pf_enable=YES gateway_enable=YES
sudo kldload pf

# Bootstrap base system (~200MB, ~23 seconds)
sudo bastille bootstrap 14.3-RELEASE

5.3.2. 2. Create pf Rules

# /etc/pf.conf
ext_if="re0"                     # Adjust to your interface
jail_net="10.0.0.0/24"
restricted_host="192.168.x.x"   # Restricted host IP

set skip on lo0

nat on $ext_if from $jail_net to any -> ($ext_if)

block in all
block out all

pass in on $ext_if proto tcp from $restricted_host to any port 22
pass out on $ext_if proto { tcp udp } from ($ext_if) to any
pass out on $ext_if proto udp from $jail_net to any port 53
pass out on $ext_if proto tcp from $jail_net to any port { 22 80 443 }
pass on bastille0 all
sudo pfctl -e -f /etc/pf.conf
sudo sysctl net.inet.ip.forwarding=1

5.3.3. 3. Create an Agent

# Create Unix user
sudo pw useradd agent-alpha -m -s /bin/sh -c 'Agent Alpha'

# Generate SSH key
sudo -u agent-alpha ssh-keygen -t ed25519 \
    -f /home/agent-alpha/.ssh/id_ed25519 -N '' -C 'agent-alpha@sandbox'

# Create gitconfig
cat <<'EOF' | sudo tee /home/agent-alpha/.gitconfig
[user]
    name = Agent Alpha
    email = agent-alpha@example.com
[init]
    defaultBranch = main
EOF

# Create thin jail (~3 seconds)
sudo bastille create agent-alpha 14.3-RELEASE 10.0.0.20

# Install toolchain -- bash and gmake are REQUIRED
sudo bastille pkg agent-alpha install -y git gh ghq node npm-node24 \
    bash gmake guile3 emacs-nox python311 curl

# Install Go on the sandbox HOST (not inside jails)
sudo pkg install -y go

# Build bd (beads issue tracker) on the host, copy static binary into jails
# Go builds a single static binary -- no runtime deps needed inside the jail
go install github.com/steveyegge/beads/cmd/bd@latest
sudo cp ~/go/bin/bd /usr/local/bastille/jails/agent-alpha/root/usr/local/bin/bd

# CRITICAL: Set bash as root shell and export SHELL
# Claude Code requires bash -- FreeBSD jails default to /bin/sh
# which causes: "No suitable shell found. Claude CLI requires a
# Posix shell environment."
sudo bastille cmd agent-alpha chsh -s /usr/local/bin/bash root
echo 'export SHELL=/usr/local/bin/bash' | \
    sudo tee -a /usr/local/bastille/jails/agent-alpha/root/root/.profile
echo 'export SHELL=/usr/local/bin/bash' | \
    sudo tee -a /usr/local/bastille/jails/agent-alpha/root/root/.bashrc

# Install AI agent CLI
sudo bastille cmd agent-alpha npm install -g @anthropic-ai/claude-code

# Copy credentials into jail
sudo mkdir -p /usr/local/bastille/jails/agent-alpha/root/root/.config/gh
sudo cp /home/agent-alpha/.config/gh/* \
    /usr/local/bastille/jails/agent-alpha/root/root/.config/gh/
sudo cp /home/agent-alpha/.gitconfig \
    /usr/local/bastille/jails/agent-alpha/root/root/.gitconfig

# Copy Claude Code credentials (if agent uses Claude)
sudo mkdir -p /usr/local/bastille/jails/agent-alpha/root/root/.claude
sudo cp /home/agent-alpha/.claude/.credentials.json \
    /usr/local/bastille/jails/agent-alpha/root/root/.claude/
sudo cp /home/agent-alpha/.claude.json \
    /usr/local/bastille/jails/agent-alpha/root/root/.claude.json

# Set ZFS quota
sudo zfs set quota=5G zroot/bastille/jails/agent-alpha/root

5.3.4. 4. Run Agent in Jail

# Interactive shell
sudo bastille console agent-alpha

# Or run a single command
sudo bastille cmd agent-alpha claude --print "explain this codebase"

# Or clone and work
sudo bastille cmd agent-alpha git clone https://forge.example/org/repo /tmp/work
sudo bastille cmd agent-alpha sh -c 'cd /tmp/work && claude'

5.3.5. 5. Verify Isolation

# From host: agent can't see host processes
sudo bastille cmd agent-alpha ps aux | wc -l   # Should be ~5

# From host: agent can't see host filesystem
sudo bastille cmd agent-alpha ls /home          # Should be empty

# From host: agent can't see other jails
sudo bastille cmd agent-alpha ls /usr/local/bastille  # No such file

# From host: agent identity is correct
sudo bastille cmd agent-alpha gh api user --jq '.login'

5.3.6. 6. ZFS Snapshots for Rollback

Before risky agent operations, snapshot the jail:

# Pre-task snapshot
sudo zfs snapshot zroot/bastille/jails/agent-alpha/root@pre-task-42

# ... agent does work ...

# Review what changed
sudo zfs diff zroot/bastille/jails/agent-alpha/root@pre-task-42

# If bad, rollback (destroys all changes since snapshot)
sudo bastille stop agent-alpha
sudo zfs rollback zroot/bastille/jails/agent-alpha/root@pre-task-42
sudo bastille start agent-alpha

# If good, destroy snapshot
sudo zfs destroy zroot/bastille/jails/agent-alpha/root@pre-task-42

For production use, automate daily snapshots with retention:

# Cron: 0 3 * * * /usr/local/bin/jail-snap-daily.sh
for jail in $(bastille list | tail -n +2 | awk '{print $2}'); do
    zfs snapshot "zroot/bastille/jails/${jail}/root@daily-$(date +%Y-%m-%d)"
done

6. Test Protocol

A formal verification checklist for new jail deployments:

#!/bin/sh
# jail-isolation-test.sh <jail-name>
JAIL=$1
PASS=0
FAIL=0

test_blocked() {
    desc="$1"; expected="$2"; shift 2
    result=$(sudo bastille cmd "$JAIL" "$@" 2>&1)
    if echo "$result" | grep -qi "$expected"; then
        echo "PASS: $desc"
        PASS=$((PASS + 1))
    else
        echo "FAIL: $desc (expected '$expected', got: $result)"
        FAIL=$((FAIL + 1))
    fi
}

test_works() {
    desc="$1"; shift
    if sudo bastille cmd "$JAIL" "$@" >/dev/null 2>&1; then
        echo "PASS: $desc"
        PASS=$((PASS + 1))
    else
        echo "FAIL: $desc (exit code $?)"
        FAIL=$((FAIL + 1))
    fi
}

echo "=== Isolation Tests for jail: $JAIL ==="
echo ""

echo "--- Should be BLOCKED ---"
count=$(sudo bastille cmd "$JAIL" ps aux 2>&1 | wc -l)
if [ "$count" -lt 10 ]; then
    echo "PASS: Host processes hidden ($count lines, jail only)"
    PASS=$((PASS + 1))
else
    echo "FAIL: Host processes visible ($count lines)"
    FAIL=$((FAIL + 1))
fi
test_blocked "Host /home inaccessible" "No such file" ls /home/
test_blocked "Bastille dir hidden" "No such file" ls /usr/local/bastille
test_blocked "Raw sockets blocked" "not permitted\|denied" ping -c1 127.0.0.1

echo ""
echo "--- Should WORK ---"
test_works "Git available" git --version
test_works "Forge CLI available" gh --version
test_works "Beads tracker available" bd --version
test_works "Identity correct" gh api user --jq '.login'
test_works "DNS works" host github.com
test_works "HTTPS works" fetch -qo /dev/null https://github.com

echo ""
echo "Results: $PASS passed, $FAIL failed"

7. Cost Comparison

Approach Hardware Monthly Isolation Latency
Agent on laptop $0 $0 None Lowest
Cloud VM (e2-medium) $0 ~$25 Per-VM High
Two mini PCs + jails ~$340 $0 Kernel-level LAN
Rack server + bhyve $800+ $0 Hypervisor LAN

The two-box approach breaks even vs cloud VMs in ~14 months, provides stronger isolation (jails > containers), lower latency (LAN), and complete control over the environment.

8. Emacs Development Workflow with TRAMP

TRAMP (Transparent Remote Access, Multiple Protocols) supports multi-hop connections and custom methods, making it well-suited for editing files inside FreeBSD jails from a remote Emacs session.

8.1. TRAMP Configuration: Custom Bastille Method

TRAMP does not natively support FreeBSD jails. Define a custom bastille TRAMP method that calls sudo bastille console <jail>:

(with-eval-after-load 'tramp
  ;; FreeBSD TRAMP performance fix (known process-send-string bug)
  (setq tramp-chunksize 500)

  ;; Define bastille method
  (add-to-list 'tramp-methods
    '("bastille"
      (tramp-login-program "sudo")
      (tramp-login-args (("bastille") ("console") ("%h")))
      (tramp-remote-shell "/bin/sh")
      (tramp-remote-shell-args ("-i" "-c"))
      (tramp-completion-use-cache nil)))

  ;; Auto-proxy: jail names matching "agent-.*" route through sandbox
  ;; This lets you type /bastille:agent-N:/path instead of full multi-hop
  (add-to-list 'tramp-default-proxies-alist
               '("agent-.*" nil "/ssh:operator@sandbox:")))

Passwordless sudo is required on the sandbox host. Add to /usr/local/etc/sudoers.d/bastille:

operator ALL=(root) NOPASSWD: /usr/local/bin/bastille console *
operator ALL=(root) NOPASSWD: /usr/local/bin/bastille cmd *
operator ALL=(root) NOPASSWD: /usr/local/bin/bastille list
operator ALL=(root) NOPASSWD: /usr/sbin/jexec
operator ALL=(root) NOPASSWD: /usr/sbin/jls

8.2. TRAMP Path Syntax

With tramp-default-proxies-alist configured, jail names auto-proxy through the sandbox host:

;; Short form (auto-proxied through sandbox)
C-x C-f /bastille:agent-alpha:/root/project/main.py

;; Explicit multi-hop (equivalent)
C-x C-f /ssh:operator@sandbox|bastille:agent-alpha:/root/project/main.py

;; Dired inside jail
C-x d /bastille:agent-alpha:/root/ghq/github.com/

;; Edit host files (not jailed)
C-x C-f /ssh:operator@sandbox:/etc/pf.conf

8.3. Helper Functions

(defun jail-find-file (jail path)
  "Open PATH inside Bastille JAIL on the sandbox host."
  (interactive
   (list (read-string "Jail: " "agent-alpha")
         (read-string "Path: " "/root/")))
  (find-file (format "/bastille:%s:%s" jail path)))

(defun jail-shell (jail)
  "Open a shell inside JAIL."
  (interactive (list (read-string "Jail: " "agent-alpha")))
  (let ((default-directory (format "/bastille:%s:/root/" jail)))
    (shell (format "*jail:%s*" jail))))

(defun jail-compile (jail command)
  "Run COMMAND inside JAIL via compile."
  (interactive
   (list (read-string "Jail: " "agent-alpha")
         (read-string "Command: " "gmake check")))
  (let ((default-directory (format "/bastille:%s:/root/" jail)))
    (compile command)))

8.4. Development Workflow

A typical session editing code inside a sandboxed jail:

  1. Open project. M-x jail-find-file with jail agent-alpha, path /root/ghq/forge.example/org/project/. TRAMP handles the multi-hop transparently.
  2. Edit files. Syntax highlighting, completion, and LSP all work (provided the language server is installed inside the jail).
  3. Run tests. M-x jail-compile with gmake check runs the test suite inside the jail. Errors appear in *compilation* with clickable file references that resolve through the TRAMP path.
  4. Issue tracking. M-x jail-shell then bd ready to see available work. bd update <id> --status in_progress to claim tasks.
  5. Git operations. M-x vc-diff on TRAMP files works when git is available inside the jail. bd sync + git push from the shell.
  6. Copy results out. Use dired to copy build artifacts from the jail to the restricted host — TRAMP handles cross-hop copies.

8.5. Performance Notes

  • tramp-chunksize must be 500 on FreeBSD (known bug).
  • Multi-hop adds latency. Keep restricted and sandbox hosts on the same switch (sub-1ms LAN).
  • ControlMaster in SSH config reduces connection overhead:
# ~/.ssh/config on restricted host
# Use Tailscale IP, not .lan -- the .lan name may resolve differently
# depending on your network, causing intermittent TRAMP failures.
Host sandbox sandbox.example.ts.net
    HostName 100.x.y.z              # Tailscale IP of sandbox host
    User operator
    IdentityFile ~/.ssh/id_ed25519
    ControlMaster auto
    ControlPath ~/.ssh/cm-%r@%h:%p
    ControlPersist 10m
    ForwardAgent no

9. Conclusion

You don't need a cloud account or Kubernetes cluster to sandbox AI coding agents. Two commodity mini PCs, FreeBSD, and Bastille thin jails provide kernel-level isolation per agent identity with sub-second jail creation, ZFS snapshots for rollback, and pf firewall for egress control.

The key insight: the network is the real isolation boundary. Separating the machine where credentials live (restricted host) from the machine where agents execute (sandbox host with jails) limits blast radius in a way that no single-machine containerization can match.

The practical insight: --dangerously-skip-permissions becomes safe – even appropriate – when the jail provides the containment that the flag removes.

10. References

Author: Jason Walsh

jwalsh@nexus

Last Updated: 2026-02-02 00:22:42

build: 2026-02-02 00:49 | sha: 7414ae7