1
0
Fork
You've already forked epi
0
No description
  • Rust 94.2%
  • Nix 4.8%
  • Shell 0.7%
  • Just 0.3%
Adam C. Stephens 15000a627f
vz: cache root disk to prevent guest ext4 corruption
VZ's default Automatic disk caching corrupts the guest ext4 filesystem
under heavy I/O; use Cached + Full, matching what UTM settled on.
epi-67
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026年06月29日 12:07:24 -04:00
.cargo
.claude
.epi
.forgejo/workflows
backends vz: cache root disk to prevent guest ext4 corruption 2026年06月29日 12:07:24 -04:00
cmd clippy: collapse nested if into let-chain in cmd_stop 2026年06月29日 12:07:24 -04:00
core vz backend: daemon supervisor loop, socket-first stop/status 2026年06月07日 09:51:05 -04:00
docs
nix mounts: bind host-home shares into guest home 2026年06月25日 22:37:38 -04:00
scripts
third_party/vfrust macos: interactive serial console + console.log capture 2026年06月07日 20:22:29 -04:00
.envrc
.gitignore e2e: drive upgrade --mode switch through the CLI, assert descriptor update 2026年06月12日 15:04:01 -04:00
AGENTS.md agents: use vein cli to orient 2026年06月25日 22:37:38 -04:00
Cargo.lock release 0.9.1 2026年06月07日 21:30:35 -04:00
Cargo.toml release 0.9.1 2026年06月07日 21:30:35 -04:00
CHANGELOG.md vz: cache root disk to prevent guest ext4 corruption 2026年06月29日 12:07:24 -04:00
CLAUDE.md
flake.lock flake.lock: Update 2026年06月11日 23:40:16 -04:00
flake.nix flake.lock: Update 2026年06月07日 21:28:31 -04:00
justfile e2e: run suite against the macOS VZ backend 2026年06月25日 22:37:38 -04:00
LICENSE.txt
README.md hooks: add EPI_SSH_HOST so hooks reach the guest on macOS 2026年06月07日 20:57:35 -04:00

epi - Ephemeral Instances

Create ephemeral virtual machines using nixosConfigurations.

Requirements

  • nix
  • systemd user environment

Quick start

# Launch a VM from a flake target
epi launch myvm --target '.#myConfig'
# SSH into it
epi ssh myvm
# Execute a command
epi exec myvm -- ls /
# Copy files
epi cp ./local-file myvm:/tmp/
# Stop and remove
epi stop myvm
epi rm myvm

Configuration

epi merges configuration from three layers (highest priority first):

  1. CLI flags--cpus, --memory, --mount, --port, --disk-size
  2. Project config.epi/config.toml in the current directory
  3. User config~/.config/epi/config.toml

For scalar values (target, cpus, memory, disk_size, default_name), the highest-priority layer wins. For list values (mounts, ports), all layers are merged (union, deduplicated).

# .epi/config.toml
target = ".#myConfig"
default_name = "dev"
cpus = 4
memory = 2048
disk_size = "80G"
mounts = ["/home/user/data"]
ports = [":8080", "3000:3000"]
project_mount = true

All resolved values (cpus, memory, disk size, ports) are persisted in instance state at launch time. Subsequent start and rebuild commands use the stored values — they do not re-read config.

Projects

epi detects a project when .epi/config.toml exists in the current directory. When inside a project:

  • The project directory is automatically mounted into the guest (disable with project_mount = false or --no-project-mount)
  • The project directory path is recorded in instance state and shown in info and list output
  • default_name from the project config becomes the default instance name, so you can run epi launch without specifying one

Mount paths in config are resolved relative to the config file's directory, so mounts = ["data"] in .epi/config.toml mounts <project>/data. Tilde (~/) paths are expanded.

Project initialization

epi init

Interactively creates a .epi/config.toml with target selection and default settings.

Commands

Command Description
launch Create and start an instance from a flake target
start Start an existing stopped instance
stop Stop an instance
rm Remove an instance
rebuild Rebuild an instance (re-evaluates target, fresh disk)
info Show detailed instance information
list List known instances
ssh Open SSH session
exec Execute a command in an instance
cp Copy files between host and instance via rsync
console Attach to serial console
console-log Show captured console output
logs Show instance logs
ssh-config Output SSH config block for an instance
init Initialize a new epi project
completions Generate shell completions (fish, bash, zsh)

Port mapping

Map TCP ports from host to guest with --port:

# Auto-assign host port, forward to guest port 8080
epi launch myvm --port :8080
# Explicit host:guest mapping
epi launch myvm --port 3000:3000 --port 8443:443

Ports can also be set in config via ports = [":8080", "3000:3000"].

Shell completions

epi completions fish | source # fish
source <(epi completions bash) # bash
source <(epi completions zsh) # zsh

Completions include dynamic instance name tab-completion.

Hooks

epi supports hook scripts at three points in the instance lifecycle. Hooks are discovered from three layers, executed in this order:

  1. User hooks~/.config/epi/hooks/<hook>.d/
  2. Project hooks.epi/hooks/<hook>.d/
  3. Nix hooks — declared in the nixosConfiguration via epi.hooks.<hook>

Within each layer, scripts are sorted by filename. Only executable files are run; non-executable files produce a warning. Each layer also supports instance-specific subdirectories (<hook>.d/<instance-name>/) whose scripts run after the layer's top-level scripts.

post-launch

Runs on the host after the VM is reachable via SSH. Useful for provisioning the guest from the outside (e.g. copying dotfiles, running commands over SSH).

Scripts receive the following environment variables:

Variable Description
EPI_INSTANCE Instance name
EPI_SSH_HOST SSH host (127.0.0.1 on Linux; the guest's IP on macOS)
EPI_SSH_PORT SSH port (forwarded localhost port on Linux; the guest's sshd port on macOS)
EPI_SSH_KEY Path to the SSH private key
EPI_SSH_USER SSH username
EPI_STATE_DIR Instance state directory
EPI_BIN Path to the running epi binary

For portable guest access from a hook, prefer $EPI_BIN exec "$EPI_INSTANCE" -- ..., or connect with $EPI_SSH_HOST/$EPI_SSH_PORT rather than assuming localhost.

If any hook exits non-zero, execution stops and the error is reported.

Since hooks run on the host (not inside the VM), use $EPI_BIN exec to run commands in the guest:

jq '{oauthAccount,userID,theme,firstStartTime,installMethod,hasCompletedOnboarding}' ~/.claude.json \
 | "$EPI_BIN" exec "$EPI_INSTANCE" -- "cat > .claude.json"

guest-init

Runs inside the guest VM on first boot only, as the provisioned user, after user creation, hostname, SSH keys, and mounts are configured. Network connectivity is available. Useful for installing packages or configuring the guest environment.

File-based hooks (from user and project layers) are embedded in the seed ISO at launch time. Nix-declared hooks are baked into the VM image. Seed ISO hooks run first, then Nix hooks. If a hook fails, the failure is logged and remaining hooks continue. SSH is available before hooks finish — they do not block the boot.

pre-stop

Runs on the host before the VM is stopped. Useful for cleanup tasks like syncing data or saving state.

Receives the same environment variables as post-launch hooks. If any hook exits non-zero, execution stops and the error is reported (but the VM is still stopped).