Skip to content

Instantly share code, notes, and snippets.

@akhenakh
Last active December 22, 2025 19:04
Show Gist options
  • Select an option

  • Save akhenakh/c9f33ee5fa3b75d7b599aa9047aa0447 to your computer and use it in GitHub Desktop.

Select an option

Save akhenakh/c9f33ee5fa3b75d7b599aa9047aa0447 to your computer and use it in GitHub Desktop.
create small spawned containers for Arch family Linuxes

AI Agent Container Manager

A lightweight Bash script to create and manage secure systemd-nspawn containers.
Designed for isolating AI Agents and LLM scripts while giving them controlled access to specific project directories on your host.

Prerequisites

This script is designed for Arch Linux (or derivatives).

sudo pacman -S arch-install-scripts systemd-container

Setup

  1. Save the script as spawn.sh.
  2. Make it executable:
    chmod +x spawn.sh

Usage

1. Create a Container

Containers are minimal Arch Linux environments.

Option A: Read-Only Access (Default - Secure)
The agent can read your code but cannot modify or delete files on your host.

# Usage: ./spawn.sh create <name> <host-path> <container-path>
./spawn.sh create ai-sandbox ~/my-projects/app /work

Option B: Read-Write Access
Use this if you want the agent to write code or modify files.

# Add the --rw flag at the end
./spawn.sh create ai-dev ~/my-projects/app /work --rw

2. Enter the Container

This starts the container (if stopped) and drops you into a shell as the user agent.

./spawn.sh shell ai-sandbox

Inside the container:

  • User: agent (UID mapped automatically)
  • Sudo: Enabled without password (sudo pacman -S ... works)
  • Files: Your host directory is mounted at /work

3. Other Commands

Command Description
./spawn.sh start <name> Boot the container in the background.
./spawn.sh stop <name> Power off the container.
./spawn.sh list List created and running containers.
./spawn.sh delete <name> Delete a container and its config.

Pre-installed Software

Modify the script to suit your needs

Security Note

This script uses systemd-nspawn with PrivateUsers=pick.

  • Files on Host: Appear owned by your user (UID 1000).
  • Files in Container: Appear owned by root/agent.
  • Isolation: Processes are isolated from the host system.
```bash bin/spawn.sh
#!/bin/bash
# spawn.sh - Create and manage systemd-nspawn containers in userspace
# Usage: ./spawn.sh create <container-name>
# ./spawn.sh start <container-name>
# ./spawn.sh shell <container-name>
# ./spawn.sh stop <container-name>
# ./spawn.sh list
set -e
# Configuration
CONTAINER_BASE="$HOME/.local/containers"
CONTAINER_CONFIG="$HOME/.config/systemd-nspawn"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
check_dependencies() {
local missing=()
for cmd in pacstrap systemd-nspawn machinectl; do
if ! command -v $cmd &> /dev/null; then
missing+=($cmd)
fi
done
if [ ${#missing[@]} -gt 0 ]; then
log_error "Missing dependencies: ${missing[*]}"
log_info "Install with: sudo pacman -S arch-install-scripts systemd-container"
exit 1
fi
}
setup_directories() {
mkdir -p "$CONTAINER_BASE"
mkdir -p "$CONTAINER_CONFIG"
log_info "Container base: $CONTAINER_BASE"
}
create_container() {
local name=$1
local mount_src="${2:-}"
local mount_dst="${3:-/mnt/shared}"
local permission_arg="${4:-}"
local container_path="$CONTAINER_BASE/$name"
if [ -d "$container_path" ]; then
log_error "Container '$name' already exists"
exit 1
fi
# Determine Mount Mode
local mount_mode="ro"
if [ "$permission_arg" == "--rw" ]; then
mount_mode="rw"
fi
# Validate mount source if provided
if [ -n "$mount_src" ]; then
# Expand tilde to home directory
mount_src="${mount_src/#\~/$HOME}"
if [ ! -d "$mount_src" ]; then
log_error "Mount source directory does not exist: $mount_src"
exit 1
fi
if [ "$mount_mode" == "rw" ]; then
log_warn "Will mount (READ-WRITE): $mount_src -> $mount_dst"
else
log_info "Will mount (READ-ONLY): $mount_src -> $mount_dst"
fi
fi
log_info "Creating container '$name' at $container_path"
# Create container directory
mkdir -p "$container_path"
# Create mount point inside container
if [ -n "$mount_src" ]; then
mkdir -p "$container_path$mount_dst"
fi
# Bootstrap minimal Arch Linux
log_info "Bootstrapping Arch Linux (requires sudo for pacstrap)..."
local max_attempts=3
local attempt=1
while [ $attempt -le $max_attempts ]; do
log_info "Attempt $attempt of $max_attempts..."
if sudo pacstrap -c "$container_path" base base-devel python python-pip git helix go zig --noconfirm; then
log_info "Bootstrap completed successfully!"
break
else
if [ $attempt -eq $max_attempts ]; then
log_error "Failed to bootstrap. Try: sudo pacman-mirrors --fasttrack"
sudo rm -rf "$container_path"
exit 1
fi
sleep 3
attempt=$((attempt + 1))
fi
done
# Create configuration with specific mount mode
create_nspawn_config "$name" "$mount_src" "$mount_dst" "$mount_mode"
# Set hostname
echo "$name" | sudo tee "$container_path/etc/hostname" > /dev/null
# Setup networking
sudo tee "$container_path/etc/systemd/network/80-container-host0.network" > /dev/null <<EOF
[Match]
Name=host0
[Network]
DHCP=yes
EOF
# Enable networkd
sudo ln -sf /usr/lib/systemd/system/systemd-networkd.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
sudo ln -sf /usr/lib/systemd/system/systemd-resolved.service \
"$container_path/etc/systemd/system/multi-user.target.wants/systemd-resolved.service"
# Create user
log_info "Setting up 'agent' user..."
sudo systemd-nspawn -D "$container_path" --pipe useradd -m -G wheel -s /bin/bash agent
echo "agent:agent" | sudo systemd-nspawn -D "$container_path" --pipe chpasswd
echo "%wheel ALL=(ALL:ALL) NOPASSWD: ALL" | sudo tee "$container_path/etc/sudoers.d/wheel" > /dev/null
# Install Python packages
log_info "Installing Python packages..."
sudo systemd-nspawn -D "$container_path" --pipe \
bash -c "python -m pip install --break-system-packages mistral-vibe" || \
log_warn "Failed to install packages."
log_info "Container '$name' created!"
}
create_nspawn_config() {
local name=$1
local mount_src="${2:-}"
local mount_dst="${3:-}"
local mode="${4:-ro}"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
cat > "$config_file" <<EOF
[Exec]
Boot=yes
PrivateUsers=pick
[Network]
VirtualEthernet=yes
Private=yes
[Files]
PrivateUsersOwnership=auto
EOF
# Add bind mount based on mode
if [ -n "$mount_src" ]; then
if [ "$mode" == "rw" ]; then
echo "Bind=$mount_src:$mount_dst" >> "$config_file"
log_warn "Configured READ-WRITE bind: $mount_src -> $mount_dst"
else
echo "BindReadOnly=$mount_src:$mount_dst" >> "$config_file"
log_info "Configured READ-ONLY bind: $mount_src -> $mount_dst"
fi
fi
log_info "Created nspawn config: $config_file"
}
start_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container '$name' does not exist"
exit 1
fi
log_info "Starting container '$name'..."
local bind_args=""
if [ -f "$config_file" ]; then
# Extract Bind= lines (Read-Write) -> convert to --bind=
local bind_rw=$(grep "^Bind=" "$config_file" | sed 's/^Bind=/--bind=/' | tr '\n' ' ')
# Extract BindReadOnly= lines (Read-Only) -> convert to --bind-ro=
local bind_ro=$(grep "^BindReadOnly=" "$config_file" | sed 's/^BindReadOnly=/--bind-ro=/' | tr '\n' ' ')
bind_args="$bind_rw $bind_ro"
fi
# Start container
sudo systemd-nspawn -D "$container_path" \
--machine="$name" \
--network-veth \
--private-users=pick \
--private-users-ownership=auto \
$bind_args \
--boot &
sleep 2
log_info "Container '$name' started"
}
shell_container() {
local name=$1
if ! machinectl list | grep -q "$name"; then
log_warn "Container '$name' is not running. Starting..."
start_container "$name"
sleep 3
fi
log_info "Opening shell in container '$name' as user 'agent'..."
sudo machinectl shell agent@$name /bin/bash
}
stop_container() {
local name=$1
log_info "Stopping container '$name'..."
sudo machinectl poweroff "$name" 2>/dev/null || true
sleep 2
log_info "Container '$name' stopped"
}
delete_container() {
local name=$1
local container_path="$CONTAINER_BASE/$name"
local config_file="$CONTAINER_CONFIG/$name.nspawn"
if [ ! -d "$container_path" ]; then
log_error "Container '$name' does not exist"
exit 1
fi
# Check if container is running
if machinectl list 2>/dev/null | grep -q "$name"; then
log_warn "Container '$name' is running. Stopping it first..."
stop_container "$name"
fi
log_warn "About to delete container '$name'"
log_warn "Path: $container_path"
read -p "Are you sure? (yes/no): " confirm
if [ "$confirm" != "yes" ]; then
log_info "Deletion cancelled"
exit 0
fi
log_info "Deleting container '$name' (requires sudo for root-owned files)..."
# Remove container directory (use sudo for root-owned files)
sudo rm -rf "$container_path"
# Remove config file (user-owned, no sudo needed)
if [ -f "$config_file" ]; then
rm -f "$config_file"
fi
log_info "Container '$name' deleted successfully"
}
list_containers() {
log_info "Available containers:"
if [ -d "$CONTAINER_BASE" ]; then
ls -1 "$CONTAINER_BASE" 2>/dev/null || echo " (none)"
else
echo " (none)"
fi
echo ""
log_info "Running containers:"
machinectl list --no-pager 2>/dev/null || echo " (none)"
}
case "${1:-}" in
create)
check_dependencies
setup_directories
if [ -z "${2:-}" ]; then
log_error "Usage: $0 create <container-name> [mount-source] [mount-dest]"
log_error "Example: $0 create ai-agent ~/projects /mnt/projects"
exit 1
fi
create_container "$2" "${3:-}" "${4:-/mnt/shared}"
;;
start)
if [ -z "${2:-}" ]; then
log_error "Usage: $0 start <container-name>"
exit 1
fi
start_container "$2"
;;
shell)
if [ -z "${2:-}" ]; then
log_error "Usage: $0 shell <container-name>"
exit 1
fi
shell_container "$2"
;;
stop)
if [ -z "${2:-}" ]; then
log_error "Usage: $0 stop <container-name>"
exit 1
fi
stop_container "$2"
;;
delete)
if [ -z "${2:-}" ]; then
log_error "Usage: $0 delete <container-name>"
exit 1
fi
delete_container "$2"
;;
list)
list_containers
;;
*)
echo "Usage: $0 {create|start|shell|stop|list} [container-name]"
echo ""
echo "Commands:"
echo " create <name> - Create a new container"
echo " start <name> - Start a container"
echo " shell <name> - Open shell in container"
echo " stop <name> - Stop a container"
echo " list - List all containers"
exit 1
;;
esac
```
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment