first commit

This commit is contained in:
Sylvain Zimmer 2026-01-28 01:15:09 +01:00
commit f865122778
No known key found for this signature in database
4 changed files with 302 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.DS_Store

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Sylvain Zimmer
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

99
README.md Normal file
View file

@ -0,0 +1,99 @@
# agent-vm
Run AI coding agents inside sandboxed Linux VMs. The agent gets full autonomy while your host system stays safe.
Uses [Lima](https://lima-vm.io/) to create lightweight Debian VMs on macOS and Linux. Ships with dev tools, Docker, and a headless Chrome browser with [Chrome DevTools MCP](https://github.com/ChromeDevTools/chrome-devtools-mcp) pre-configured.
Currently supports [Claude Code](https://claude.ai/code). Other agents (Codex, etc.) can be added in the future.
Feedbacks welcome!
## Prerequisites
- macOS or Linux
- [Lima](https://lima-vm.io/docs/installation/) (installed automatically via Homebrew if available)
- A [Claude subscription](https://claude.ai/) (Pro, Max, or Team)
## Install
```bash
git clone https://github.com/sylvinus/agent-vm.git
cd agent-vm
# Add to your shell config
echo "source $(pwd)/claude-vm.sh" >> ~/.zshrc # zsh
echo "source $(pwd)/claude-vm.sh" >> ~/.bashrc # or bash
```
## Usage
### One-time setup
```bash
claude-vm-setup
```
Creates a VM template with dev tools, Docker, Chromium, and Claude Code pre-installed. You'll be prompted to authenticate Claude during setup.
### Run Claude in a VM
```bash
cd your-project
claude-vm
```
Clones the template into a fresh VM, mounts your current directory, and runs `claude --dangerously-skip-permissions`. The VM is deleted when Claude exits.
### Debug shell
```bash
claude-vm-shell
```
Same as `claude-vm` but drops you into a bash shell instead.
## Customization
### Per-user: `~/.claude-vm.setup.sh`
Create this file in your home directory to install extra tools into the VM template. It runs once during `claude-vm-setup`, as the default VM user (with sudo available):
```bash
# ~/.claude-vm.setup.sh
sudo apt-get install -y postgresql-client
pip install pandas numpy
```
### Per-project: `.claude-vm.runtime.sh`
Create this file at the root of any project. It runs inside the cloned VM each time you call `claude-vm`, just before Claude starts. Use it for project-specific setup like installing dependencies or starting services:
```bash
# your-project/.claude-vm.runtime.sh
npm install
docker compose up -d
```
## How it works
1. **`claude-vm-setup`** creates a Debian 13 VM with Lima, installs dev tools + Chrome + Claude Code, and stops it as a reusable template
2. **`claude-vm`** clones the template, mounts your working directory read-write, runs optional `.claude-vm.runtime.sh`, then launches Claude with full permissions
3. On exit, the cloned VM is stopped and deleted. The template persists for reuse
Ports opened inside the VM (e.g. by Docker containers) are automatically forwarded to your host by Lima.
## What's in the VM
| Category | Packages |
|----------|----------|
| Core | git, curl, wget, build-essential, jq |
| Python | python3, pip, venv |
| Node.js | Node.js 22 (via NodeSource) |
| Search | ripgrep, fd-find |
| Browser | Chromium (headless), xvfb |
| Containers | docker |
| AI | Claude Code, Chrome DevTools MCP server |
## License
MIT

181
claude-vm.sh Normal file
View file

@ -0,0 +1,181 @@
#!/usr/bin/env bash
#
# agent-vm / claude-vm: Run Claude Code inside a sandboxed Lima VM
# Part of https://github.com/sylvinus/agent-vm
#
# Source this file in your shell config:
# source /path/to/agent-vm/claude-vm.sh
#
# Functions:
# claude-vm-setup - Create the VM template (run once)
# claude-vm - Run Claude in a fresh VM with cwd mounted
# claude-vm-shell - Open a debug shell in a fresh VM
CLAUDE_VM_TEMPLATE="claude-template"
claude-vm-setup() {
if ! command -v limactl &>/dev/null; then
if command -v brew &>/dev/null; then
echo "Installing Lima..."
brew install lima
else
echo "Error: Lima is required. Install from https://lima-vm.io/docs/installation/" >&2
return 1
fi
fi
limactl stop "$CLAUDE_VM_TEMPLATE" &>/dev/null
limactl delete "$CLAUDE_VM_TEMPLATE" --force &>/dev/null
echo "Creating VM template..."
limactl create --name="$CLAUDE_VM_TEMPLATE" template:debian-13 \
--set '.mounts=[]' \
--disk=20 \
--memory=8 \
--tty=false
limactl start "$CLAUDE_VM_TEMPLATE"
# Disable needrestart's interactive prompts
limactl shell "$CLAUDE_VM_TEMPLATE" sudo bash -c 'mkdir -p /etc/needrestart/conf.d && echo "\$nrconf{restart} = '"'"'a'"'"';" > /etc/needrestart/conf.d/no-prompt.conf'
echo "Installing base packages..."
limactl shell "$CLAUDE_VM_TEMPLATE" sudo DEBIAN_FRONTEND=noninteractive apt-get update
limactl shell "$CLAUDE_VM_TEMPLATE" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
git curl wget build-essential \
python3 python3-pip python3-venv \
jq ripgrep fd-find htop \
unzip zip \
docker.io
# Add user to docker group
limactl shell "$CLAUDE_VM_TEMPLATE" bash -c 'sudo usermod -aG docker $(whoami)'
# Install Node.js 22 (needed for MCP servers)
echo "Installing Node.js 22..."
limactl shell "$CLAUDE_VM_TEMPLATE" bash -c "curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -"
limactl shell "$CLAUDE_VM_TEMPLATE" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y nodejs
# Install Chromium and dependencies for headless browsing
echo "Installing Chromium..."
limactl shell "$CLAUDE_VM_TEMPLATE" sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
chromium \
fonts-liberation \
xvfb
# Symlink so tools looking for google-chrome find Chromium
limactl shell "$CLAUDE_VM_TEMPLATE" sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome
limactl shell "$CLAUDE_VM_TEMPLATE" sudo ln -sf /usr/bin/chromium /usr/bin/google-chrome-stable
limactl shell "$CLAUDE_VM_TEMPLATE" bash -c 'sudo mkdir -p /opt/google/chrome && sudo ln -sf /usr/bin/chromium /opt/google/chrome/chrome'
# Install Claude Code
echo "Installing Claude Code..."
limactl shell "$CLAUDE_VM_TEMPLATE" bash -c "curl -fsSL https://claude.ai/install.sh | bash"
limactl shell "$CLAUDE_VM_TEMPLATE" bash -c 'echo "export PATH=\$HOME/.local/bin:\$HOME/.claude/local/bin:\$PATH" >> ~/.bashrc'
# Authenticate Claude (saves token in template, inherited by clones)
echo "Setting up Claude authentication..."
limactl shell "$CLAUDE_VM_TEMPLATE" bash -lc "claude setup-token"
# Configure Chrome DevTools MCP server for Claude
echo "Configuring Chrome MCP server..."
limactl shell "$CLAUDE_VM_TEMPLATE" bash << 'VMEOF'
CONFIG="$HOME/.claude.json"
if [ -f "$CONFIG" ]; then
jq '.mcpServers["chrome-devtools"] = {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest", "--headless=true", "--isolated=true"]
}' "$CONFIG" > "$CONFIG.tmp" && mv "$CONFIG.tmp" "$CONFIG"
else
cat > "$CONFIG" << 'JSON'
{
"mcpServers": {
"chrome-devtools": {
"command": "npx",
"args": ["-y", "chrome-devtools-mcp@latest", "--headless=true", "--isolated=true"]
}
}
}
JSON
fi
VMEOF
# Run user's custom setup script if it exists
local user_setup="$HOME/.claude-vm.setup.sh"
if [ -f "$user_setup" ]; then
echo "Running custom setup from $user_setup..."
limactl shell "$CLAUDE_VM_TEMPLATE" bash < "$user_setup"
fi
limactl stop "$CLAUDE_VM_TEMPLATE"
echo "Template ready. Run 'claude-vm' in any project directory."
}
claude-vm() {
local vm_name="claude-$(basename "$(pwd)" | tr -cs 'a-zA-Z0-9-' '-')-$$"
local host_dir="$(pwd)"
if ! limactl list -q 2>/dev/null | grep -q "^${CLAUDE_VM_TEMPLATE}$"; then
echo "Error: Template VM not found. Run 'claude-vm-setup' first." >&2
return 1
fi
_claude_vm_cleanup() {
echo "Cleaning up VM..."
limactl stop "$vm_name" &>/dev/null
limactl delete "$vm_name" --force &>/dev/null
}
trap _claude_vm_cleanup EXIT INT TERM
echo "Starting VM '$vm_name'..."
limactl clone "$CLAUDE_VM_TEMPLATE" "$vm_name" \
--set ".mounts=[{\"location\":\"${host_dir}\",\"writable\":true}]" \
--tty=false &>/dev/null
limactl start "$vm_name" &>/dev/null
# Run project-specific runtime script if it exists
if [ -f "${host_dir}/.claude-vm.runtime.sh" ]; then
echo "Running project runtime setup..."
limactl shell --workdir "$host_dir" "$vm_name" bash -l < "${host_dir}/.claude-vm.runtime.sh"
fi
limactl shell --workdir "$host_dir" "$vm_name" claude --dangerously-skip-permissions
_claude_vm_cleanup
trap - EXIT INT TERM
}
claude-vm-shell() {
local vm_name="claude-debug-$$"
local host_dir="$(pwd)"
if ! limactl list -q 2>/dev/null | grep -q "^${CLAUDE_VM_TEMPLATE}$"; then
echo "Error: Template VM not found. Run 'claude-vm-setup' first." >&2
return 1
fi
_claude_vm_shell_cleanup() {
echo "Cleaning up VM..."
limactl stop "$vm_name" &>/dev/null
limactl delete "$vm_name" --force &>/dev/null
}
trap _claude_vm_shell_cleanup EXIT INT TERM
limactl clone "$CLAUDE_VM_TEMPLATE" "$vm_name" \
--set ".mounts=[{\"location\":\"${host_dir}\",\"writable\":true}]" \
--tty=false &>/dev/null
limactl start "$vm_name" &>/dev/null
# Run project-specific runtime script if it exists
if [ -f "${host_dir}/.claude-vm.runtime.sh" ]; then
limactl shell --workdir "$host_dir" "$vm_name" bash -l < "${host_dir}/.claude-vm.runtime.sh"
fi
echo "VM: $vm_name | Dir: $host_dir"
echo "Type 'exit' to stop and delete the VM"
limactl shell --workdir "$host_dir" "$vm_name" bash -l
_claude_vm_shell_cleanup
trap - EXIT INT TERM
}