Skip to content
fredrikPublic

About

my local exe.dev clone

Resources

Stars

0 stars

Watchers

0 watching

Forks

Latest commit

 

History

41 Commits

Folders and files

Repository files navigation

shed

A clone of exe.dev, the ssh-based microVM service, that runs entirely on your own Mac. Real Linux microVMs booted from OCI images in about a second, managed over ssh, with persistent disks and an HTTP front door.

$ ssh box@shed
shed: creating vm box...
dev@box:~$

Introduction to shed

shed keeps a pile of small Linux machines behind your Mac. The idea comes from exe.dev: creating a computer should be about as cheap as creating a file. shed does the same thing but entirely on your own hardware, so there is no account and nothing metered.

Each machine is a real VM on Apple's hypervisor, with its own kernel, init, and disk — not a container. That's the point: it's where you put work you don't want running loose on your Mac. A coding agent with root, a curl | sh you don't quite trust, a service that wants port 80. Whatever happens inside stays inside; the VM can't see your Mac's disk or processes.

VMs are cheap enough that you don't have to be tidy. A stopped VM costs nothing but its disk, and ssh boots it again in about a second, so it's fine to have ten half-finished experiments sitting in ls. Everything is addressed by name: ssh box@shed opens a shell (creating the VM first if it doesn't exist), ssh shed cp box box2 clones the whole machine in a couple of seconds, and http://box.shed.localhost:8080 reaches whatever box is serving. There is no GUI and no YAML; the interface is ssh.

The default image is sheduntu: Ubuntu 26.04 with the usual tools installed (git, curl, vim, tmux, htop, ripgrep, jq), the GitHub CLI (gh), a dev user with passwordless sudo, plus mise and uv in /usr/local/bin, node 24 via mise, and python 3.14 (uv-managed) as dev's default next to the apt python3. Claude Code is preinstalled for dev, so claude works on first login — a VM is the natural place to let an agent work with less oversight than you'd give it on your Mac.

You land in zsh with a starship prompt, history suggestions and syntax highlighting as you type, ctrl-r/ctrl-t fuzzy search through history and files, and the small tools everyone installs anyway: fzf, bat, fd, zoxide (z <dir> jumps to where you usually work), tree. There is no framework behind it — cat ~/.zshrc is about forty lines and explains itself, and deleting it is a supported opinion. The prompt sticks to plain unicode so it renders in any terminal; if yours has a Nerd Font, one command upgrades it:

starship preset nerd-font-symbols -o ~/.config/starship.toml

The image is baked locally the first time you use it: a throwaway VM boots upstream ubuntu:26.04, runs the recipe, and its rootfs becomes the cached base image. Takes about a minute, rebakes whenever the recipe changes (bump sheduntuVersion to pick up upstream Ubuntu updates), and old bakes are pruned once no VM uses them. A VM keeps booting the bake it was created on; a new bake only affects VMs created after it. Any other OCI image works via --image.

How it works

One daemon (shedd) runs three things:

  • SSH gateway (127.0.0.1:2222). Routing is by username, sshpiper-style: user shed is the control plane (ssh shed ls), any other username names a VM and the session is brokered into that VM's sshd (ssh box@shed). Key-only auth against ~/.local/share/shed/authorized_keys. The same gateway also listens on a local unix socket with no client auth (the 0600 socket is the auth) — that's what bin/shed talks to.
  • VM manager over Apple's Virtualization.framework (Code-Hex/vz). An OCI image is pulled with go-containerregistry, flattened, and streamed through Microsoft's pure-Go tar2ext4 into a read-only ext4 base disk — the rootfs never touches the host filesystem, so no root is needed and ownership/setuid/device nodes survive. Each VM adds a sparse writable ext4 data disk (mke2fs), joined by overlayfs at boot. New VMs get 2 vCPUs, 4 GB of RAM and a 10 GB disk unless new says otherwise. The kernel is shed's own build of Linux (6.18 with the Kata Containers configuration, the one Apple's container direct-boots), published as a release asset on this repo's kernel-<version> tags, fetched once and cached. kernel/README.md has the recipe.
  • HTTP front door (127.0.0.1:8080). http://<vm>.shed.localhost:8080 proxies to the VM — the smallest EXPOSEd port, or share port. VMs are private by default; ssh shed share <vm> prints a signed link, share set-public opens it up.

Inside every VM, a small static Go agent (shedguest) rides the initramfs as pid 1: it assembles the overlay root, DHCPs on the NAT network, installs your keys, serves ssh (its own embedded sshd — so even distroless images are ssh-able), supervises the image's ENTRYPOINT/CMD, and talks to the daemon over vsock. When macOS's Local Network privacy blocks TCP to the guest, everything transparently falls back to vsock.

Sessions land as a proper login: the default user dev (uid 1000, passwordless sudo — baked into sheduntu) when the image has it, root otherwise, running the user's shell from /etc/passwd as a login shell in $HOME, with /etc/motd shown on interactive connects. scp/sftp run as the same user, so ownership comes out right. default_user in config.toml changes the preference; sudo -i is always one step from root.

VM state machine: creating → stopped → starting → running → stopping, plus error. Stopped VMs keep their disk and release CPU/RAM back to the pool (ls -l shows usage). ssh to a stopped VM boots it on demand (~1 s). If the daemon dies, records reconcile to stopped on restart.

Requirements

  • Apple Silicon Mac, macOS 15+
  • Go 1.25+, Homebrew (brew install e2fsprogs)
  • A 16 MB one-time kernel download (from this repo's releases) on first shedd serve, and a minute to bake sheduntu on first use

Setup

make build          # builds shedd + guest agent, codesigns (required for vz)
bin/shedd install    # ssh config (Host shed) + authorized_keys from ~/.ssh
bin/shedd            # run the daemon in the foreground (same as shedd serve)
bin/shedd doctor     # if something is off

Commands

ssh shed new [name] [--image ref] [--cpu N] [--memory MB] [--disk GB] [--no-start]
                                   # default image: sheduntu
ssh shed ls [-l] [--json]
ssh shed start|stop|restart <vm>...
ssh shed rm <vm>...
ssh shed cp <src> <dst>          # instant clone (APFS copy-on-write)
ssh shed rename <old> <new>
ssh shed share <vm>              # signed URL for a private vm
ssh shed share set-public|set-private <vm>
ssh shed share port <vm> <port>
ssh shed ssh-key ls|add          # add - reads the key from stdin
ssh shed whoami | doc | browser <vm>
ssh shed version [--json]        # daemon version

Locally, bin/shed runs the same commands without ssh: it talks to the daemon over a unix socket (~/.local/share/shed/control.sock, 0600 — the file mode is the auth), so no keys or agent are involved. shed ls, shed new mybox, cat key.pub | shed ssh-key add -. The client sends its argv line to the daemon verbatim; the command surface is one cobra tree either way. Interactive shells still go over ssh.

scp/sftp and ssh -L work through the gateway:

scp file.txt box@shed:/root/
ssh -L 8080:localhost:80 web@shed

Development

make test           # unit tests (no VMs, no signing needed)
make build          # rebuild + codesign

Versions are git tags (v0.1.0, semver, v0.x while things still move). make build bakes git describe --tags --dirty into both binaries, so a dev build reports something like v0.1.0-3-g19ad079-dirty, or dev plus the commit before the first tag. bin/shed version prints the client and daemon versions and warns when they differ, which is what a stale daemon looks like after a rebuild. bin/shedd --version works too. To release: git tag v0.1.0 && git push origin v0.1.0.

State lives in ~/.local/share/shed/ (VM records, disks, keys, optional config.toml), caches in ~/Library/Caches/shed/ (kernel, base disks by image digest). Serial console of each VM: ~/.local/share/shed/vms/<name>/serial.log.

docs/design/ describes each subsystem (ssh gateway, control plane, VM lifecycle, images and storage, guest agent, networking, HTTP front door, configuration) in more depth than this README. CLAUDE.md holds the operational notes for working on shed with a coding agent.

Caveats

  • VMs die with the daemon (Virtualization.framework VMs live in-process); records reconcile to stopped on restart.
  • *.shed.localhost resolves to loopback in Chrome, Firefox and curl, which handle .localhost themselves. Safari asks the macOS resolver, which only knows plain localhost, so it can't find the host. Add a line per VM to /etc/hosts (127.0.0.1 box.shed.localhost), or point /etc/resolver/shed.localhost at a local dnsmasq with address=/shed.localhost/127.0.0.1 for a wildcard.
  • Images run under the agent as pid 1 — systemd in the image is not executed (ubuntu works fine; systemctl does not).
  • No TLS on the front door, no ssh -R, no ssh-agent forwarding yet.

About

my local exe.dev clone

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Contributors

Languages