{"slug": "show-hn-safe-and-lightweight-vms-for-coding-agents", "title": "Show HN: Safe and lightweight VMs for Coding agents", "summary": "Skerr ysoft released safeagent, an early-development tool that gives each git worktree its own lightweight Linux VM so parallel coding-agent sessions cannot collide on shared ports, databases or the host machine. safeagent reads lockfiles, compose files, monorepo packages and env-file addresses to configure most projects without setup, and installs into Claude Code via three hooks (WorktreeCreate, WorktreeRemove, SessionStart) or a plugin; it currently runs only on macOS on Apple Silicon, with Linux hosts still being built.", "body_md": "**A lightweight Linux VM for every checkout, so parallel coding sessions\ncan't collide, and the code they run can't reach your machine.**\n\nCoding agents like Claude Code work best in parallel: several sessions, each in its own git worktree, each on its own task. Worktrees separate the files, but everything else is shared. Two sessions both want port 3000, one branch's migration breaks the other's database, and every command an agent runs has your whole machine within reach.\n\nsafeagent gives each worktree its own small Linux VM, with its own processes,\nports, Postgres, Valkey (Redis) and Temporal, and its own copy of their data.\nIt reads what it needs from the repository (lockfiles, compose files,\nmonorepo packages, the local addresses its env files name), so most\nprojects need no configuration. It plugs\ninto Claude Code, so every worktree session gets one automatically and the\nVM is ready in the background while the session gets on with the work; with\nany other tool, `safeagent create` makes a workspace by hand.\n\n*Two real Claude Code sessions, recorded live with\n[asciinema](https://github.com/skerrysoft/safeagent/blob/main/docs/demo/demo.cast) (idle time trimmed to 2 s; nothing else\nedited). As each session opens, safeagent prepares its workspace in the\nbackground (the VM, Postgres, Temporal, `npm install`, migrations), shown\nin the status line, while Claude gets on with the code. Each has its own\ndatabase: both place order #1, and only the second, which merged a branch\nwith a new migration, has a `gift_note` column.*\n\n**Status: early development.** macOS on Apple Silicon today; Linux hosts\nare being built. See [What works today](#what-works-today).\n\n**1. Install safeagent** on a Mac with Apple Silicon:\n\n```\ncurl -fsSLO https://github.com/skerrysoft/safeagent/releases/latest/download/safeagent-darwin-arm64.tar.gz\nmkdir -p ~/.local/bin && tar -xzf safeagent-darwin-arm64.tar.gz -C ~/.local/bin   # or anywhere on your PATH\n```\n\nEach release is built by [GitHub Actions](https://github.com/skerrysoft/safeagent/blob/main/.github/workflows/release.yml)\nfrom this repository, with checksums (`SHA256SUMS`) and a signed build\nprovenance attestation. To check a download was built here, before\ninstalling it:\n\n```\ngh attestation verify safeagent-darwin-arm64.tar.gz --repo skerrysoft/safeagent\n```\n\nDownloaded it with a browser instead? macOS then won't run it (\"cannot be\nopened\"), as it isn't notarized yet; clear the download flag with\n`xattr -d com.apple.quarantine ~/.local/bin/safeagent`.\n\n## Or build it from source (Go 1.27+)\n\n```\ngit clone https://github.com/skerrysoft/safeagent && cd safeagent\nmake build                      # bin/safeagent, signed to run VMs\ncp bin/safeagent ~/.local/bin/     # or anywhere on your PATH\n```\n\n**2. Connect it to Claude Code.**\n\n```\nsafeagent claude install\n```\n\nThis shows the three hooks it will add to `~/.claude/settings.json`\n(`WorktreeCreate`, `WorktreeRemove`, `SessionStart`), keeps a backup, and\nasks which folders to turn safeagent on for. safeagent only ever acts in those\nfolders; everywhere else, worktrees stay plain worktrees.\n\n## Or use the Claude Code plugin instead\n\nThe plugin carries the same three hooks (and a small script that finds the\n`safeagent` command), and nothing else. In any Claude Code session, from any\nfolder:\n\n```\n/plugin marketplace add skerrysoft/safeagent\n/plugin install safeagent@safeagent\n```\n\nThe first command adds this GitHub repository as a plugin marketplace; the\nsecond installs the plugin for your user, in every project. safeagent itself\nmust still be installed (step 1), and it still only acts in the folders you\nenable (`safeagent enable DIR`). Use the plugin or `safeagent claude install`, not\nboth: two `WorktreeCreate` hooks would both try to make the worktree.\n\n**3. Start sessions in worktrees, as you already do.**\n\n```\nclaude --worktree fix-login\n```\n\nor turn on **Worktree** when starting a session in the Claude desktop app.\nSubagents and background sessions that use worktrees work the same way.\n\nThat's all. Optionally, show each workspace in Claude Code's status line, so\nyou can see it being prepared (add to `~/.claude/settings.json`):\n\n```\n\"statusLine\": { \"type\": \"command\", \"command\": \"safeagent statusline\" }\n```\n\n**Without Claude Code** (or with another agent), make a workspace yourself\nand work in it:\n\n```\nsafeagent enable ~/code              # once: safeagent acts only in folders you enable\ncd ~/code/shop\nsafeagent plan                       # what safeagent found, before anything runs\ncd \"$(safeagent create fix-login)\"   # a worktree that gets its own VM\nsafeagent exec -- npm test           # the first command boots and prepares it, then runs\nsafeagent start                      # run the project's apps, forwarded to your machine\nsafeagent status                     # services, apps, and where each is on your machine\n```\n\n1. **Claude creates the worktree through safeagent.** In an enabled folder it\nbecomes a*workspace* ; elsewhere, a plain worktree, made the way Claude\nwould make it.\n2. **The session opens right away** , and safeagent prepares the workspace in\nthe background: builds or reuses the VM image, boots the VM (about half a\nsecond), starts the services (Postgres, Valkey, Temporal), creates the\ndatabases, installs dependencies, builds, and runs the setup you\nconfigured. The first time for a project this takes a minute or two;\nafter that, seconds. So the database is there when you run migrations\nor tests.\n3. **Nothing waits on that but the commands that need the VM.** Claude can\nread and edit code and use git meanwhile. A command that needs the VM\n(`npm test` , say) waits for it, printing each step, and the status line\nshows where preparation has got to.\n4. **Commands go to the right place automatically.**`node` ,`npm` ,`npx` ,`yarn` ,`pnpm` and`corepack` run in the VM, as do the commands of any\nother runtime the project lists (`python` ,`go` , …). Everything else,\ngit included, runs on your machine with your keys and settings. Claude is\ntold how the workspace works when the session starts.\n5. **Your apps start when you start them, not before.** Preparing a\nworkspace never runs the project's own apps (its API, web app,\nworkers). When your change is ready to try, run`safeagent start` (all of\nthem) or`safeagent start web` (one, with the library watchers it needs),\nor ask Claude to. They run in the VM, restart if they crash, and are\nforwarded to your machine;`safeagent stop` stops them, and so does the\nVM stopping (`safeagent down` , or 30 minutes unused). Running your own\ndev command (`npm run dev` ) works too: it runs in the VM like any\nother.\n6. **Removing the worktree removes the workspace** : its VM and its data.\nUncommitted work and branches with commits are never deleted.\n\n- **One VM per workspace** , on Apple's Virtualization framework: a small\nLinux kernel (Alpine's) and a userland built from signature-verified[Wolfi](https://wolfi.dev) packages, shared read-only by every workspace\nthat needs the same runtimes and services. It boots in about half a\nsecond.\n- **Your checkout, live.** The worktree is shared into the VM at the same\npath (virtio-fs), so your editor, git and the VM all see the same files.\nDependencies install into it as Linux builds.\n- **Its own disks.** Service data (Postgres, Valkey) and caches (the home\ndirectory,`/tmp` ) live on sparse disk images of the workspace's own;\nthe rest of the system is in memory.\n- **A guest agent** , the VM's first process, runs the services and apps\nand takes commands from safeagent over SSH on vsock, with keys pinned per\nworkspace.\n- **A user-mode network.** The VM's network card connects to a network\nstack inside safeagent ([gVisor's](https://gvisor.dev) ), which lets it reach\nthe internet and nothing local. Ports are forwarded to your machine's`127.0.0.1` .\n- **Sized to the project** : 4 CPUs and 3 GB by default, more memory for\neach service and app, within half your machine.\n\nMost projects need no configuration. safeagent reads the files a project already has:\n\n| Service | Started when the project has… | Commands in the VM get | \n|---|---|---|\n| `postgres` : PostgreSQL 17 with pgvector and the contrib extensions | `pg` ,`postgres` ,`pg-promise` (npm);`pgx` ,`lib/pq` (Go);`psycopg` ,`asyncpg` (Python);`pg` (Ruby); a`postgres` /`pgvector` image in a compose file | `DATABASE_URL` ,`PG*` ; database`app` , with`vector` installed | \n| `valkey` : Valkey 8 (Redis-compatible) | `redis` ,`ioredis` ,`bullmq` ,`iovalkey` (npm);`go-redis` ,`valkey-go` ;`redis` (Python, Ruby); a`redis` /`valkey` image | `REDIS_URL` ,`VALKEY_URL` | \n| `temporal` : Temporal dev server | `@temporalio/client` or`/worker` ;`go.temporal.io/sdk` ;`temporalio` (Python); a`temporalio/*` image | `TEMPORAL_ADDRESS` | \n\nIt also installs dependencies from the lockfile (`npm ci`,\n`pnpm install --frozen-lockfile`, `uv sync --frozen`, …), reads every\npackage of a monorepo, gives each new workspace copies of your checkout's\nlocal `.env` files (so apps run as they do in your checkout), creates the\nlocal databases they point to, and builds what the apps import. safeagent\nitself reads env files only for local addresses, ports and variable\nnames: every other value, and any password in a URL, is dropped as the\nfile is read, and nothing from them is stored, printed or sent anywhere.\nIt also finds the project's apps (packages with a `dev` script and a\nport, Procfile processes, Django's and Rails' dev servers), which\n`safeagent start` runs, with their ports forwarded to your machine; they\nnever start on their own. `safeagent plan` shows all of it before anything runs,\nincluding how each app is wired: what in the VM answers at every local\naddress its env files name, and what nothing does. To say exactly what a\nworkspace needs, add a `safeagent.yaml` at the repository root:\n\n```\nservices: [postgres, temporal]\nruntimes: [python@3.12]        # Node is always there\npackages: [ffmpeg]             # extra packages, by name\nsetup:                         # in the VM, in order, when the data is new\n  - npm ci\n  - npm run migrate\napps: [api, web]               # what `safeagent start` runs\nports: [3000]                  # forward another port to your machine\n```\n\nSetup runs once for a workspace's data, and again when its inputs change\n(dependencies, migrations, `safeagent.yaml`). **[The `safeagent.yaml`\nreference](https://github.com/skerrysoft/safeagent/blob/main/docs/safeagent-yaml.md)** explains every key and exactly what runs\nwhen, in which order.\n\nThe services' variables are set for every command in the VM, so they win\nover the same names in a `.env` file (dotenv loaders don't override\nvariables that are already set). `safeagent status` shows the services and the\nports they are forwarded to on your machine; `safeagent url postgres` prints a\nURL, password included, for tools such as `psql` or a database GUI.\n\nThe checkout is never copied into the VM. Your worktree stays an ordinary folder on your machine; the VM sees the same folder, live, at the same path, through a shared file system. Git runs on your machine, not in the VM, so it works as it always does:\n\n- **Rebase, merge, pull, switch branches** in the worktree as usual\n(`git pull --rebase origin dev` ,`git merge dev` ). The VM sees the new\nfiles the moment git writes them.\n- **New dependencies or migrations arrive with a merge?** The next command\nthat runs in the VM notices (safeagent watches lockfiles, migration folders\nand`safeagent.yaml` ) and runs setup again first, so the workspace installs\nthem and migrates its own database.\n- **A rebase rewrote migrations the database already ran?** Start the\ndata over:`safeagent reset` (back to the snapshot, if the repository has\none, else empty), and setup migrates it from scratch.\n- **Your editor, git GUI and Claude's file tools** all work on the same\nfiles on your machine; paths in errors and stack traces match.\n\nCode in the VM can't redirect your git: the worktree's `.git` pointer is\nlocked, and repositories or Claude Code settings planted in the checkout\nare quarantined (see [Security](#security)).\n\nA workspace can start with real data, migrated for its own branch. A dump in\nthe repository loads in `safeagent.yaml` (`seed: {postgres: db/seed.sql.gz}`);\na dump that isn't (an anonymized copy of production, say) loads from your\nmachine:\n\n```\nsafeagent reset --empty && safeagent seed ~/dumps/prod.dump   # in one workspace\nsafeagent snapshot save                                    # make it the default\n```\n\nEvery new workspace of the repository then starts from that snapshot (on\nmacOS, an APFS clone: instant, and taking no space until it changes) and\nruns its own branch's setup on top. `safeagent reset` goes back to the\nsnapshot; `safeagent snapshot ls` and `safeagent snapshot rm NAME` manage them.\n\nSnapshots and `safeagent reset` cover service data only. What commands keep\nin the VM's home directory and `/tmp` (package caches, downloaded\nbrowsers, build output) lives on a second disk of the workspace's own, so\nit doesn't take the VM's memory; the home directory keeps it across\nrestarts, and `/tmp` starts empty. It is never in a snapshot,\n`safeagent reset` leaves it, and removing the workspace deletes it.\n\nWorkspaces run untrusted code: dependencies' install scripts, repository\nscripts, and whatever an agent decides to run. safeagent is built to contain\nit. Found a way out? Please report it privately, as\n[SECURITY.md](https://github.com/skerrysoft/safeagent/blob/main/SECURITY.md) describes.\n\n**What protects you**\n\n- **VM boundary.** Each workspace is a Linux VM (on macOS, Apple's\nVirtualization framework, which runs each VM in its own sandboxed\nprocess). It sees only its checkout, the read-only system image and its\nown configuration, never your machine's other files.\n- **A network that only goes out.** A VM's network card connects to a\nuser-mode network stack inside safeagent, not to a bridge or your machine's\nnetwork. Every connection is checked after DNS resolution: public\naddresses are allowed; loopback, private and link-local ranges (cloud\nmetadata included), carrier-grade NAT, multicast, your machine's own\naddresses and every network it is directly attached to (VPNs included)\nare refused, as is outgoing mail (port 25). A VM can hold at most 512 TCP\nand 128 UDP flows, so it can't exhaust your machine's ports. DNS goes\nthrough your machine's resolver. Refused connections are logged\n(`safeagent logs network` ).\n- **Services reachable only from your machine.** Forwarded ports listen on`127.0.0.1` only, at ports safeagent picks (never ones the checkout names,\nwhich could take over ports your own servers use). From your machine,\nPostgres and Valkey each require their own password, which other local\nusers can't read; services without a password aren't forwarded unless\nyou ask. The apps' web ports answer only to`localhost` names, so a web\npage can't reach them by DNS rebinding.\n- **Trust inside the VM only.** Inside the VM, where everything runs as\nroot anyway, Postgres trusts the user a command names, so a project's`.env` URLs work unchanged (on other ports too, which relay to the\nservice inside the VM). Connections forwarded from your machine never\nget that trust.\n- **Clean output.** What safeagent prints from the VM (logs, errors) is\nstripped of terminal control sequences.\n- **Locked git metadata.** The worktree's`.git` pointer is made immutable,\nand safeagent's own git commands name the repository explicitly, so a VM\ncan't redirect them. After every command, at session start and every\n15 seconds while a VM runs, safeagent scans the checkout for planted\nrepositories and Claude Code settings and quarantines them.\n- **Opt-in.** Nothing happens outside folders you enable. Creating a\nworktree never runs the repository's filter drivers, or its hooks unless\nyou opt in.\n- **Pinned keys.** Host and VM authenticate each other with per-workspace\nSSH keys that safeagent generates. Nothing is trusted on first use.\n- **Verified images.** Every package is checked against a signed index\n(Wolfi's and Alpine's pinned keys) down to each file, extraction can't\nwrite outside the image directory, and install scripts run in a build VM.\n- **Careful deletion and bounded logs.** safeagent only deletes worktrees it\ncreated, never a branch that holds commits, and caps the logs a VM can\nwrite on your disk.\n\nEach of these is tested by attacking a real VM from the inside\n([scripts/escape-test.sh](https://github.com/skerrysoft/safeagent/blob/main/scripts/escape-test.sh)), and the parsers that read\nVM or repository input are fuzzed.\n\n**Known gaps**\n\n- The checkout is shared read-write: that is how your edits reach the VM and\nits output reaches you. Code in the VM can change your source files, and\nfiles git tracks under `.claude/` . Review changes as you would any\ndependency's.\n- Something running in the background in a VM could plant a file between scans (up to 15 seconds).\n- No disk quota yet: a VM can fill the disk through its checkout, and its service data disk and its home disk (caches) can each grow to 64 GB. Space the VM frees is handed back to your machine.\n- The Temporal dev server and its UI have no authentication. If you forward them, any process of any user on your machine can reach them.\n- A web page served from a VM and opened in your browser is a local page: it\ncan send requests to other services on your machine's loopback interface,\nas any local web app can. Open workspace apps through their `*.localhost` URLs.\n- A snapshot's data reaches every new workspace of its repository, whatever branch it is on, and code in those VMs can reach the internet. Don't save data in a snapshot that you wouldn't give to every branch you check out (an outside contributor's included).\n- Setup commands from `safeagent.yaml` , dependency installs and builds run\nin the VM without asking, as`npm install` would; the project's apps run\nwhen you (or Claude) run`safeagent start` .\n- Your local `.env` files (secrets included) are copied into each new\nworkspace, as Claude Code's`.worktreeinclude` copies what it lists, so\ncode in the VM can read them, and the VM can reach the internet. Add a`.worktreeinclude` to choose exactly which files a workspace gets.\n- A VM can reach anything public, from your machine's public address: code in it can send what it can read (the checkout) to the internet, and reach services that trust your IP address or a public range routed over your VPN. An allowlist mode is planned.\n- No external security review yet.\n\nThe full threat model, and the rules every change is tested against, are in\n[docs/design.md](https://github.com/skerrysoft/safeagent/blob/main/docs/design.md#7-security).\n\nYou rarely need these with Claude Code; these are the ones for working by hand:\n\n| Command | What it does | \n|---|---|\n| `safeagent plan [--env] [DIR]` | Show what safeagent would set up for a checkout, and how its apps are wired, without starting anything | \n| `safeagent create NAME [--base BRANCH]` | Make a workspace (worktree on branch `worktree-NAME` ) and print its path | \n| `safeagent start [APP…]` /`stop [APP…]` | Run or stop the project's apps in the VM (all, or the ones named); they never start on their own | \n| `safeagent status [NAME]` | Preparation, services, setup, whether each app answers, and where everything is on your machine | \n| `safeagent exec -- CMD…` | Run a command in the workspace's VM | \n| `safeagent logs [-f] WHAT` | A service's or an app's output, or `setup` ,`console` ,`network` | \n| `safeagent rm NAME [--force]` | Remove a workspace; `--force` discards uncommitted changes but never a branch with commits | \n\nCommands that act on a workspace use the one you're in, unless you name one.\n\n## All commands and settings\n\n| Command | What it does | \n|---|---|\n| `safeagent claude install` /`uninstall` | Add or remove the Claude Code hooks | \n| `safeagent enable [DIR]` /`disable [DIR]` ,`enable --list` | Turn safeagent on or off for a folder and everything under it | \n| `safeagent ls` | List workspaces | \n| `safeagent up [NAME]` /`down [NAME]` | Start (and prepare) or stop a workspace's VM | \n| `safeagent url [-w NAME] SERVICE` | Print a URL for a service, password included, for tools on your machine | \n| `safeagent setup [NAME]` | Run the project's seed and setup again | \n| `safeagent seed [-w NAME] FILE` | Load a `.sql` ,`.sql.gz` or`.dump` file into the workspace's empty Postgres | \n| `safeagent snapshot save [S]` /`ls` /`rm S` | Save the workspace's data for new workspaces ( `default` ); list or delete snapshots | \n| `safeagent reset [NAME] [--snapshot S \\| --empty]` | Start the workspace's data over | \n| `safeagent gc` | Delete images and downloads no workspace needs | \n| `safeagent statusline` | One line for Claude Code's status line | \n\nSettings, through environment variables for now:\n\n| Variable | Effect | \n|---|---|\n| `SAFEAGENT_IDLE_STOP` | Stop a workspace's VM after it has been unused (no commands, no connections to its ports) this long (default `30m` ;`off` never), apps included. Its data stays; the next command starts it again, and`safeagent start` the apps | \n| `SAFEAGENT_CPUS` ,`SAFEAGENT_MEMORY` | CPUs and memory per VM (default 4 and `3g` , more memory for each service and app; at most all CPUs and half the machine's memory) | \n| `SAFEAGENT_BASE=head` | Start new worktrees at your current commit instead of the remote's default branch | \n| `SAFEAGENT_GIT_HOOKS=1` | Let the repository's git hooks run when creating worktrees (off by default) | \n| `SAFEAGENT_BRANCH_PREFIX` | Prefix for new branches (default `worktree-` ) | \n| `SAFEAGENT_FILE_EVENTS=off` | Don't forward file changes to watchers in the VM (by default, watchers in a VM see edits made on your machine) | \n| `SAFEAGENT_SHIMS=off` | Run your machine's own `node` ,`python` … even in workspaces | \n| `SAFEAGENT_HOME` | Keep all of safeagent's state and caches under one directory | \n\n| ✅ Claude Code hooks: CLI, desktop app, subagents, background sessions; plugin | verified | \n| ✅ One VM per workspace on macOS (Apple Silicon) | ~0.5 s boot | \n| ✅ Preparation in the background, with progress in the status line and `safeagent status` |  | \n| ✅ Networking: internet yes; your machine, local network and cloud metadata no |  | \n| ✅ Services: Postgres 17 + pgvector, Valkey, Temporal; data kept per workspace |  | \n| ✅ Runtimes: Node, Python, Go, Ruby, Java, Bun, Deno; extra packages by name |  | \n| ✅ Dependencies installed from the lockfile; setup re-run when a merge changes them |  | \n| ✅ Monorepos: services, apps and library watchers detected across packages; `safeagent start` runs apps with forwarded ports |  | \n| ✅ Procfiles, Django and Rails dev servers; Python, Ruby and Go runtimes detected |  | \n| ✅ Local `.env` files copied into workspaces; the databases they name created; their secrets never read |  | \n| ✅ Seeds, setup commands (migrations), snapshots cloned per workspace |  | \n| ✅ Hot reload; idle stop; CPU and memory limits |  | \n| ✅ Images built only from signature-verified packages |  | \n| 🚧 Linux hosts | in progress | \n\n- macOS on Apple Silicon (built and tested on macOS 27); Go 1.27+ and git\n- Network access the first time an image is built: packages come from Wolfi (packages.wolfi.dev) and the kernel from Alpine. About 100 MB, more with services and runtimes.\n\n`make build` builds the guest agent for Linux, embeds it in `bin/safeagent`, and\nsigns `bin/safeagent` with the `com.apple.security.virtualization` entitlement\nso it can start VMs. A plain `go build` produces a binary without the agent\nor the signature.\n\nsafeagent is early. If it gets something wrong on your project (a service it\nmissed, an app it didn't find, setup that failed), `safeagent plan` output in\nan [issue](https://github.com/skerrysoft/safeagent/issues) helps most.\nSecurity problems go to [SECURITY.md](https://github.com/skerrysoft/safeagent/blob/main/SECURITY.md) instead.\n\nMIT. See [LICENSE](https://github.com/skerrysoft/safeagent/blob/main/LICENSE).", "url": "https://wpnews.pro/news/show-hn-safe-and-lightweight-vms-for-coding-agents", "canonical_source": "https://github.com/skerrysoft/safeagent", "published_at": "2026-11-04 18:34:50+00:00", "updated_at": "2026-11-04 18:50:38.019386+00:00", "lang": "en", "topics": ["ai-agents", "developer-tools", "ai-tools", "ai-safety"], "entities": ["safeagent", "Skerr ysoft", "Claude Code", "GitHub Actions", "Postgres", "Valkey", "Temporal", "Go"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/show-hn-safe-and-lightweight-vms-for-coding-agents", "markdown": "https://wpnews.pro/news/show-hn-safe-and-lightweight-vms-for-coding-agents.md", "text": "https://wpnews.pro/news/show-hn-safe-and-lightweight-vms-for-coding-agents.txt", "jsonld": "https://wpnews.pro/news/show-hn-safe-and-lightweight-vms-for-coding-agents.jsonld"}}