skiff
Health Warn
- License — License: Apache-2.0
- Description — Repository has a description
- Active repo — Last push 0 days ago
- Low visibility — Only 5 GitHub stars
Code Pass
- Code scan — Scanned 11 files during light audit, no dangerous patterns found
Permissions Pass
- Permissions — No dangerous permissions requested
No AI report is available for this listing yet.
A fresh Linux VM on your Mac in half a second. Throwaway sandboxes for coding agents on Apple Silicon, with credentials brokered from the Mac.
skiff – a fresh Linux VM on your Mac in half a second
skiff gives you throwaway Linux VMs on Apple Silicon, one per task or coding
agent: skiff new dev boots an Ubuntu 24.04 VM with its own kernel, disk, IP
and root SSH in about two seconds (the boot itself takes half a second), andskiff rm dev throws it away. Credentials the agent needs stay on the Mac and
reach the VM through a broker, never as copies.
macOS (skiff)
├── VM dev (Virtualization.framework via Apple's `container`, 192.168.64.x, ssh dev.skiff)
├── VM ai (…) ──┐ relay: localhost:3000 → ai:3000
└── broker (credentials, MCP servers, audit log) ◀─┘ ssh -R tunnel per session
Each VM is a container machine: a Virtualization.framework VM driven by
Apple's container CLI. There is no
nested virtualization and no shared runtime VM, so JIT-heavy workloads such as
Claude Code run at Mac speed. The host side is one Go binary; the guest is a
plain Ubuntu image with systemd, sshd and skiff's key.
Requirements
- Apple Silicon (any generation)
- macOS 26 or newer (what Apple's
containersupports) - Homebrew (skiff installs
containerthrough it if needed) - Go 1.26+ to build skiff
Check with skiff doctor.
Install
go install github.com/sovyn-eu/skiff/cmd/skiff@latest
skiff prepare # one-time setup, about a minute
Or from a checkout: make install (go install ./cmd/skiff → $(go env GOPATH)/bin/skiff).
skiff prepare is idempotent. It:
- verifies the host (Apple Silicon, macOS version, Hypervisor framework,
container), - generates an SSH key pair in
~/.skiff/, - starts the
containersystem service (first run downloads Apple's guest kernel), - builds the guest image
skiff/guest(Ubuntu 24.04 with systemd, sshd,
your public key and the internet-only firewall), rebuilt only when the
image recipe or your key changes, - writes
~/.skiff/ssh_config.
Why Apple's container
skiff started as Firecracker microVMs inside a Lima VM with nested
virtualization. Measured on an M4 Pro (2 vCPU / 1 GiB guests), the nesting
cost JIT-heavy workloads such as Claude Code about twice the CPU, which is why
skiff moved to one Virtualization.framework VM per guest:
| measurement | Mac | Firecracker in Lima | container |
|---|---|---|---|
| first touch of fresh RAM, ms/MiB | 0.18 | 0.39 | 0.18–0.23 |
CTR_EL0 read (JIT icache flushes) |
n/a | 24 µs | 0.001 µs |
| Claude Code CPU in the first 3 s, JIT | 0.64–0.71 s | 1.13–1.22 s | 0.69–0.87 s |
| cold boot to sshd | – | 1.5 s (+0.1 s ssh) | 0.5 s |
There are no golden snapshots (every boot is cold and fast), no image
profiles (install what a task needs over ssh) and no --disk (the root disk
is a sparse 512 GiB volume that grows on demand); VMs need at least 1 GiB of
memory.
Usage
skiff new # boots vm1 (2 vCPUs, 1 GiB RAM, 2G disk)
skiff new dev --vcpus 4 --mem 2048
skiff ls
skiff ls --json # also: new --json, inspect --json
skiff inspect dev # details of one VM
skiff ssh dev # interactive root shell
skiff ssh dev uname -a # run a command (a shell command line, like plain ssh)
skiff exec dev -- git commit -m "it's done" # exact argv, every word quoted
skiff stop dev # keep the disk
skiff start dev
skiff rm dev # stop and delete
skiff gc --owner paseo # remove VMs a tool created and lost track of
Owned VMs and skiff gc
Tools that create VMs on your behalf label them with an owner and, if they keep a
pool, a slot. skiff records both together with the creation time in the VM's
directory, so the labels disappear with the VM:
skiff new paseo-3 --owner paseo --slot 3
skiff ls # OWNER shows paseo/3, AGE the time since creation
skiff gc --owner paseo --keep paseo-1 --keep paseo-2 # a tool reconciling its pool
skiff gc --older-than 7d --dry-run # owned VMs older than a week
gc only ever removes VMs that have an owner; VMs created by hand have none and
are never collected. --owner and --older-than select which owned VMs go,--keep exempts VMs by name, and --dry-run shows the plan. Without --owner
or --older-than, gc removes no VM and only deletes the grants and tokens left
on the Mac for VMs that no longer exist. A VM a tool is creating right now is not
in its keep list yet; guard it with --keep or --older-than. gc --json
reports {"vms": [...], "host_state": [...], "dry_run": ...}.
Port forwarding
skiff new web -p 8080:80 -p 3000 # forwards at creation
skiff forward web 5432 # localhost:5432 -> web:5432
skiff forward web # list
skiff unforward web 5432
Forwards appear as localhost:PORT on the Mac, persist across stop/start
and a reboot, follow the VM to a new address, and are removed with the VM. Host
ports must be 1024 or higher because the relay runs as your user. Under the
hood skiff _relay, started by skiff whenever forwards exist, listens on the
host port and joins each connection to the VM's guest port; a stopped VM keeps
its port and connections are closed until it runs again. skiff forward web
shows each forward as active or inactive.
To use plain ssh, add one line to ~/.ssh/config:
Include ~/.skiff/ssh_config
Then ssh dev.skiff works from anywhere, including scp, rsync and your IDE.
The alias runs skiff _dial as ProxyCommand, which connects to the VM's
current address and verifies the host key pinned at creation, so no port
forwarding or sudo is needed on macOS.
Capabilities: credentials and MCPs without copying secrets
A VM is isolated from your Mac on purpose. When an agent inside needs
GitHub, Claude Code or an MCP server that only runs on the Mac, skiff opens
one controlled hole: a broker that hands out connections, never secrets.
skiff secret set claude-token # paste the output of `claude setup-token`
skiff secret set github-token # a fine-grained token, limited to the repos the agent may use
skiff grant agent1 claude github ssh-agent
skiff ssh agent1 # the broker lives as long as this session
skiff grants agent1
skiff revoke agent1 github
While skiff ssh is open, the broker listens on a Unix socket on the Mac and
ssh forwards it to 127.0.0.1:7777 inside the guest (-R). Tools in the guest
talk to that address with a per-VM placeholder token; the broker swaps it for
the real credential from the macOS Keychain. The socket identifies the VM, so
there is nothing in the guest worth stealing, and the hole closes with the
terminal. ssh-agent is a built-in grant that adds -A; pair it withssh-add -c to confirm every signature. For clients that only speak HTTP over
a Unix socket, the guest also gets /run/skiff/broker.sock while any
capability is granted: a systemd socket that relays to the same tunnel.
When something other than skiff ssh drives the guest, such as an editor or
an agent runner with its own ssh session, keep the broker up withskiff tunnel agent1. It holds only the forward, reconnects when the
connection drops, logs tunnel=open|closed to the audit log and ends with
Ctrl-C or when the VM stops. skiff ssh agent1 keeps working alongside it
and uses the running broker instead of forwarding a second time; if the session
or tunnel that owns the broker ends first, the remaining session takes it over
within a few seconds.
Grants are the only policy: default deny, one file per VM in ~/.skiff/vms/,
checked on every request and logged to ~/.skiff/audit.log. A denied request
answers with the exact skiff grant command that would allow it. A grant is
all or nothing per capability: what a granted VM can reach is what the
credential behind it can reach. Limit that at the source, for GitHub with a
fine-grained token restricted to the repositories the agent works on. GitHub
enforces that limit itself, and the token never enters the guest.
The broker is harness-neutral. It knows three primitives, and everything
specific to Claude Code, GitHub or any other tool is a capability file:
| Primitive | What the broker does |
|---|---|
proxy |
reverse-proxies /<cap>/... (or a routed Host) to an upstream and injects headers from secrets |
mcp |
runs a stdio MCP server on the Mac and serves it as an HTTP MCP endpoint |
exec |
pipes the request through an external program (skiff-cap-<name>) |
skiff cap ls # embedded: claude, github; yours in ~/.skiff/caps
skiff cap show claude # the TOML file
skiff cap show github --render --vm agent1 # what the guest receives
skiff cap init codex # commented skeleton, then: skiff cap lint codex
skiff cap add https://example.com/gemini.toml # shown before it is installed
A capability declares the primitive plus a [guest] section with environment
variables, files, managed blocks inside shared files (such as /etc/gitconfig)
and JSON merges, all templated with .BrokerURL, .BrokerSocket, .PseudoToken
and .VM. Secrets (keychain:, env:, cmd:, file:) are only valid on the
broker side; skiff cap lint rejects them in guest sections. Header
templates can encode Basic credentials with{{ secret "ref" | basicauth "user" }}. A proxy can add [[proxy.routes]]:
requests that reach the broker for another host (for example through.BrokerSocket) go to that route's upstream with its own headers. A user file in~/.skiff/caps/<name>.toml replaces an embedded capability of the same name.
See examples/caps/ for an MCP bridge and an exec plugin.
The embedded github capability serves git and the gh CLI with one token.
git is rewritten to the broker in /etc/gitconfig; gh sends its API calls
through .BrokerSocket (http_unix_socket) with a placeholder token in itshosts.yml, which gh reads on every call, so a grant applies to running
processes without a restart. Paths with percent-encoding are refused, and
downloads that GitHub redirects to other hosts (gh release download) do not
work through the broker. Route hosts must be public DNS names, never an IP orlocalhost, so no capability can catch the requests of another.
The embedded claude capability points ANTHROPIC_BASE_URL at the broker and
sets CLAUDE_CODE_OAUTH_TOKEN to the placeholder, so the unmodified Claude Code
binary in the guest sends exactly the headers of a normal subscription login.
Usage draws on your own subscription like any other session. Anthropic's terms
allow signing in to the unmodified binary with your own subscription, including
in sandboxes you run; they do not allow routing other people's requests through
your account, so keep the broker personal.
How a VM is wired
| Piece | Value |
|---|---|
| VM | container machine create skiff/guest --name NAME --cpus N --memory M |
| Kernel | Apple's default guest kernel, fetched by container system start |
| Root disk | sparse 512 GiB volume, grows on demand |
| Network | vmnet, 192.168.64.x by DHCP; the address can change across restarts |
| SSH | ssh NAME.skiff through the ProxyCommand skiff _dial, host key pinned at creation in ~/.skiff/known_hosts |
| Port forwards | skiff _relay, a detached process that listens on localhost:HOST and dials the VM |
| State | ~/.skiff/machines/NAME.json (owner, slot, creation time, host key, forwards) |
Network isolation
The guest image carries an nftables ruleset that rejects connections from the
VM to private networks: the Mac at 192.168.64.1, your LAN, other VMs, and
link-local and carrier-grade ranges. DNS and DHCP to the gateway stay open, the
internet is reachable through vmnet's NAT. It is a guard rail inside the guest,
not a boundary: root in the VM can lift it. The only controlled way back to the
Mac is the capability broker.
Because all VMs share one vmnet subnet, skiff new reads each VM's ssh host
key right after its first boot and pins it; a VM that takes a neighbour's
address cannot pose as it towards skiff ssh, grants or the broker tunnel.
Troubleshooting
skiff doctorexplains what is missing on the host.- Boot log of a VM:
container machine logs NAME - Port forwards:
skiff forward NAMEshows their state; the relay logs to~/.skiff/relay.log.
The relay runs as a child of your terminal (macOS denies a launchd agent the local
network), so it ends at logout; the nextskiffcommand starts it again. - After a reboot of the Mac, the next
skiffcommand starts thecontainerservice and
the VMs that were running come back. VMs stopped withskiff stopstay stopped. sshreportsHost key verification failedfor a VM created before host keys were
pinned, or one replaced under the same name: recreate it.skiff sshandskiff execmirror the remote exit status; exit 255 means the connection failed.skiff ssh dev a bjoins its words into one shell command line, as ssh does, so
quotes and$are interpreted by the guest shell. Useskiff exec dev -- a bfrom
scripts: it quotes every argument and the guest sees exactly that argv.- Ctrl-C during
skiff new NAMEremoves the half-created VM again. - Reset everything:
skiff rmeach VM, thenrm -rf ~/.skiffandcontainer image rm skiff/guest:latest.
Development
make test # unit tests (with -race)
make cover # coverage summary
make lint # gofmt, go vet
make e2e # boots a real VM (requires skiff prepare), cleans up afterwards
Layout: cmd/skiff (entry point), internal/cli (cobra commands),internal/app (workflows), internal/backend (the interface, the container
backend and an in-memory fake for tests), internal/host (host facts),internal/broker (per-VM capability broker), internal/cap (capability files,
embedded defaults), internal/grant (per-VM policy), internal/guest
(guest provisioning scripts), internal/secret (Keychain/env/cmd/file resolver),internal/vm (spec validation), internal/sshcfg (ssh_config rendering).
Contributing
Bug reports and pull requests are welcome. Please read
CONTRIBUTING.md for the development setup, coding
guidelines and PR process, and SECURITY.md for how to report
vulnerabilities privately. This project follows the
Contributor Covenant.
License
skiff is licensed under the Apache License 2.0.
Copyright 2026 IT-Labs GmbH. Apple's container CLI, its guest kernel and the
Ubuntu image are installed or downloaded at skiff prepare time and keep
their own licenses; see NOTICE.
Reviews (0)
Sign in to leave a review.
Leave a reviewNo results found