From f865122778c5d068d502b0dfe4622f9a8c9f0b40 Mon Sep 17 00:00:00 2001 From: Sylvain Zimmer Date: Wed, 28 Jan 2026 01:15:09 +0100 Subject: [PATCH] first commit --- .gitignore | 1 + LICENSE | 21 ++++++ README.md | 99 ++++++++++++++++++++++++++++ claude-vm.sh | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 302 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 claude-vm.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e43b0f9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..e8c3ec9 --- /dev/null +++ b/LICENSE @@ -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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e1d157 --- /dev/null +++ b/README.md @@ -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 diff --git a/claude-vm.sh b/claude-vm.sh new file mode 100644 index 0000000..df72689 --- /dev/null +++ b/claude-vm.sh @@ -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 +}