Machine: Never run NPM install on your computer

katspaugh1 pts0 comments

machine — one isolated VM per project<br>away." />

// never run npm install on your machine again

Run code and devtools in a machine.

Reproducible, sandboxed Lima VMs for the Claude Code / Codex era.<br>No host filesystem mount. No cross-project bleed.<br>One machine per project.

brew install katspaugh/machine/machine

copy

lima · blog

$ machine up blog<br>→ profile python · cypress<br>→ image ubuntu-24.04-arm64<br>→ disk 20G<br>→ cpu 4 · mem 8G<br>✓ ssh ready · machine ssh blog

scroll

// section 01 — architecture

The machine room.

Your shell talks to the CLI. The CLI drives Lima. Lima boots a VM per project.<br>Nothing crosses a line that isn't explicitly drawn.

// host (your mac)<br>// hypervisor<br>// guest vms

host<br>your machine<br>macOS · arm64

$ shell<br>↓ machine cli

~/.ssh/agent<br>forwarded, never copied

control · ssh

hypervisor<br>Lima<br>Apple Virtualization · vz

• no host fs mount<br>• tmpfs secrets

boot · provision

vm · wallet

vm · scraper

vm · blog<br>ubuntu-24.04-arm64

running

— isolation barrier —

agent-forwarded SSH

Your ssh-agent is forwarded into the guest at exec time. The key file never leaves the host.

tmpfs-only secrets

Secrets from 1Password land in $XDG_RUNTIME_DIR/dev-secrets, a tmpfs that's wiped at shutdown.

no host fs mount

Lima's default mountType is disabled. The VM can't read your ~/ or anything else.

// section 02 — why bother

Three things, in order of importance.

01

Isolated by default.

One VM per project. The blast radius of a bad postinstall script is one VM, not your laptop.

machine up blog

02

Reproducible from TOML.

Check projects.toml and provision.toml into the repo. Your teammate runs up, gets the same box.

machine rebuild blog

03

Agent-ready, on day one.

Claude Code and Codex ship in the base image, with the official Claude marketplace and a curated plugin set wired up. Point your editor at the SSH host and let them rip.

machine ssh blog

// section 03 — threat model

Ghost in the machine.

The point of a VM is the boundary. Here's exactly what crosses it and what doesn't.<br>No vibes — the file path on the right is real.

✓ what the VM can reach

→Your project's git repo, cloned at bootgit@github.com:you/blog → ~/code/blog (inside the VM)

→Your forwarded SSH agent socket$SSH_AUTH_SOCK · used, never copied

→1Password env(s) you declared in the repo's .envrc$XDG_RUNTIME_DIR/dev-secrets (tmpfs)

→Outbound network, and a few forwarded localhost ports3000-3010 · 4200 · 5173-5180 · 8080-8099

→Its own filesystem, fullydisposable. `machine rebuild` is cheap.

✕ what the VM cannot reach

×Your dotfiles, shells, history~/.bash_history · ~/.config · ~/.aws

×Anything else in your home directory~/Documents · ~/Desktop · ~/Library

×Other projects' VMs, files, or socketsno shared volumes, no cross-VM ssh

×Your 1Password vault directlyonly items you allow-list

×The host disk, the keychain, Touch IDif it's on macOS, it stays on macOS

// section 04 — profiles

The machine shop.

Profiles stack. Pick a base, layer extras in provision.toml, or write your own from scratch in 30 lines.

cypress

Chrome (or Chromium on arm64), Xvfb, and the lib deps Cypress needs. Node lands from the base image.

chromechromiumxvfbgtk · nss

supabase-fly

The two CLIs you actually use when shipping a backend. Docker comes from the base image.

supabaseflyctl

python

Modern Python tooling on top of system python3: uv for envs and installs, ruff for lint/format, pyright for types.

uvruffpyright

rust

rustup with the stable toolchain, installed system-wide so cargo is on PATH for every user.

rustupcargostable

go

go

A pinned Go toolchain from go.dev, dropped into /usr/local/go with go and gofmt on PATH.

go 1.23gofmt

your own

A profile is a TOML file and a shell script. Copy any of the above, change three lines, commit it.

provision.tomlsetup.sh

// section 05 — demo

What it looks like.

A VHS replay of the CLI surface — list, up, ssh. The first up is the slow one; subsequent boots are warm restarts.

~/code/blog

// section 06 — lifecycle

Tend the machine.

Ten commands, no manifesto. Most days you'll use the first two.

machine up

Boot the VM. First time provisions; after that it's a warm restart.

machine ssh

Open an interactive shell. With your agent forwarded.

machine claude

Skip the shell, jump straight into claude at the primary repo.

machine update

Re-run provisioning on a running box. Tools change; the VM doesn't.

machine rebuild

Tear the VM down to the image and provision from scratch.

machine destroy

Rage-quit a machine. Disk, state, secrets, all gone.

machine doctor

Check Lima, network, agent, and disk space. Tells you what's drifted.

machine ps

List your VMs, their states, and where the SSH ports landed.

machine secrets

Render 1Password Environments into $XDG_RUNTIME_DIR/dev-secrets (tmpfs) for this project.

machine init

First run only. Writes a starter projects.toml to ~/.config/machine/ (or to the repo root in dev mode).

// section 07 — editors

Your editor doesn't...

machine blog host secrets project lima

Related Articles