This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
A lightweight CLI for managing microVMs with copy-on-write storage. CLI-only — no daemon, no REST API.
- Linux: Firecracker (KVM) + one of:
- ZFS zvols (default; see
docs/SPEC.md). - dm-thin (kernel-builtin device-mapper thin provisioning; see
docs/DM-THIN-SPEC.md), optionally with a dm-vdo compression layer (--vdo; seedocs/VDO-SPEC.md). Backend is selected atember init --storage <zfs|dm-thin>and persisted onGlobalConfig.
- ZFS zvols (default; see
- macOS: Apple Virtualization Framework + APFS clones. See
docs/MACOS-SPEC.mdfor the design.
# Build
cargo build
# Build and run
cargo run -- --help
# Run tests
cargo test
# Format
cargo fmt
# Check without building
cargo check
# Lint
cargo clippy# Unit tests
cargo test
# Manual testing (requires root, firecracker, and a backend)
# ZFS backend
sudo ./target/debug/ember init --pool testpool --device /dev/loop0
sudo ./target/debug/ember image pull alpine:latest
sudo ./target/debug/ember vm create testvm --image alpine:latest
# dm-thin backend (no kernel module; in-tree)
sudo ./target/debug/ember init \
--storage dm-thin \
--storage-path /var/lib/ember/dm-thin \
--size 50G
# dm-thin with transparent compression (needs Linux 6.9+ and `vdoformat`)
sudo ./target/debug/ember init \
--storage dm-thin \
--storage-path /var/lib/ember/dm-thin \
--size 50G \
--vdo
sudo ./target/debug/ember image pull alpine:latest
sudo ./target/debug/ember vm create testvm --image alpine:latest
# Tear down a backend
sudo ./target/debug/ember deinit --purge
# Grow the dm-thin data device
sudo ./target/debug/ember storage grow --size 100G
# Integration tests. `run-integration-tests.sh` is the project runner: it
# builds as the current user and only runs the test binary under sudo, so
# `target/` does not end up owned by root. It passes
# `--ignored --test-threads=1 --nocapture` and takes a suite name, or
# `suite::test` for a single test.
./run-integration-tests.sh # every suite for this platform
./run-integration-tests.sh dm_thin # root + dm-thin + thin-provisioning-tools
./run-integration-tests.sh vdo # also needs dm-vdo + vdoformat
./run-integration-tests.sh vdo::vdo_reports_compression_once_the_pool_holds_data- Prefer explicit error handling. Use
?for propagation, not.unwrap(). - Shell out to platform CLI tools — no fragile C library bindings. Linux:
zfs/zpool/iptables. macOS:hdiutil/diskutil/cp -c/ember-vz. - Value clear interfaces, boundaries, and abstractions; avoid leaks between them. Subsystems own their own formats — dm-thin owns its pool/volume names, networking owns its TAP prefix and iptables comment, and so on. Shared types like
GlobalConfigexpose generic identity (e.g.instance_namespace()) and stay free of subsystem trivia. If you find yourself reaching across a boundary to format a name, match a string, or branch on another subsystem's mode, that's the cue to move the logic to the side that owns the concept.
See specs in the docs/ folder for details, when needed.
Basic architecture choices:
- Platform-specific code lives behind backend traits (
VmBackend,StorageBackend,NetworkBackend). VmandNetworkare picked at compile time via#[cfg(target_os)].Storageis a runtime trait object (Arc<dyn StorageBackend>) so the concrete backend can be selected fromGlobalConfig.storage_backendwithout a rebuild.- Shell out to platform tools:
ember-vz(Swift helper for AVF),hdiutil,diskutil,cp -c, Homebrewe2fsprogson macOS;zfs/zpool/iptables/dmsetup/losetup/thin-provisioning-toolson Linux.
We use jujutsu (jj) for version control; prefer jj over git when possible.
The main branch/bookmark is main.
- Create individual jj changes with good descriptions; one logical change per commit.
- Prefix change description titles with the subsystem, e.g.
cli: implement CLI parsingorzfs: add pool operations. - Verify
cargo buildpasses before finalizing a change. - After
jj describe, normally runjj newto create a fresh change for unrelated or follow-up work.
- When fixing compilation across multiple changes after a rebase, work oldest-to-newest, one change at a time. Run
cargo buildand verify it passes before moving to the next change. - Prefer manual file-level reverts over
jj backoutwhen the change touches files modified in descendant changes. - When squashing, always verify the target change is correct before executing.
- Use
jj undoimmediately when an operation creates cascading conflicts, rather than trying to fix the mess. - Never squash or reorder changes without asking first.