Skip to content

rudavko/cortl

Repository files navigation

cortl

Ephemeral, container-like development environments on Apple Silicon.

Install

curl -L -o cortl.zip https://github.com/rudavko/cortl/releases/download/v0.2.2/Cortl-0.2.2-darwin-arm64.zip && unzip -o cortl.zip && ./cortl --help

How Cortl Works

cortl uses a two-layer model:

  • Images (~/.cortl/images/{distro}-{version}): shared upstream OS artifacts managed by Cortl
  • Bases (~/.cortl/bases/{name}): your VM definition (Cortlfile) plus per-base artifacts (base disk clone, templates, metadata)

Typical flow:

  1. Run cortl run for the default Alpine VM, or create a named base (cortl base create <name>)
  2. Edit ~/.cortl/bases/<name>/Cortlfile when you want a custom base
  3. Build custom bases with cortl base build <name> to create/update image + base artifacts
  4. Run a custom base with cortl run <name> [path]

Pipeline at runtime:

Validation -> FetchImage -> PrepareStorage -> ConfigInjection -> Hook (Linux only) -> Boot

Commands

cortl run                              Boot the default Alpine VM in the current directory
cortl run <path>                       Boot the default Alpine VM with a workspace path
cortl run <path> --distro macos:latest Boot the default macOS VM with a workspace path
cortl run <path> --distro macos:<version> Boot a version-pinned default macOS VM with a workspace path
cortl run <base> [path]                Boot VM from a named base
cortl run --auto-snapshot <base> [path]   Boot and save snapshot on shutdown
cortl run --save-template <base> [path]   Boot and capture a warm-boot template
cortl ls                               List running sessions
cortl stop <session>                   Stop a session
cortl clean                            Clean stopped sessions
cortl clean --session <session>        Clean one stopped session by ID/prefix
cortl clean --all                      Remove all sessions + cached images + snapshots
cortl base create <name>               Scaffold a new base (Cortlfile)
cortl base build <name>                Build base (golden master + base disk)
cortl base build <name> --force        Rebuild even if golden master exists
cortl base ls                          List available bases
cortl base rm <name>                   Delete a base
cortl snapshot ls                      List saved snapshots
cortl snapshot run <name> [path]       Boot from a snapshot
cortl snapshot rm <name...>            Delete one or more snapshots
cortl agent screenshot <session>       Capture a VM frame for agent workflows
cortl agent observe <session>          Return current VM observation JSON
cortl agent click <session> <x> <y>    Send verified input to a macOS VM
cortl agent key <session> <key>        Send a verified key to a macOS VM
cortl agent type-text <session>        Send verified text from stdin
cortl agent wait <session> <condition> Wait for display or VM state events
cortl agent shutdown <session>         Ask a macOS VM session to stop

Examples

cortl run
cortl run .
cortl run . --distro macos:latest
cortl run . --distro macos:26.2
cortl base create alpine-dev --distro alpine:3.19
cortl base create ubuntu-dev --distro ubuntu:24.04
cortl base create mac-dev --distro macos:latest
cortl base build alpine-dev
cortl run --save-template alpine-dev .
cortl run mac-dev ~/projects/app
cortl snapshot ls
cortl snapshot run shutdown-2026-...
cortl clean --session <session-id>

Cortlfile Setup

Each base is driven by a Cortlfile at:

~/.cortl/bases/<base-name>/Cortlfile

Quick start:

# 1) Create a base scaffold
cortl base create mydev --distro alpine:3.19

# 2) Edit the generated Cortlfile
$EDITOR ~/.cortl/bases/mydev/Cortlfile

# 3) Build base artifacts
cortl base build mydev

# 4) Boot VM from that base
cortl run mydev .

Cortlfile format:

# Required
BASE alpine:3.19

# Optional
MEMORY 4G
CPUS 4
HOOK worktree

# Optional install script: everything after --- runs during base build
---
chroot /mnt apk add --no-cache jq
echo 'hello from install script' > /mnt/root/hello.txt

Supported directives:

  • BASE <distro:version> (required): currently alpine:3.19, ubuntu:24.04, macos:latest, or a version-pinned macos:<version>
  • MEMORY <nM|nG> (optional): VM memory override, e.g. 512M, 4G
  • CPUS <n> (optional): VM vCPU count (>=1, up to host CPU count)
  • HOOK <name> (optional): hook script name in ~/.cortl/hooks/
  • IPSW </absolute/path/file.ipsw> (macOS): optional for macos:latest; required for version-pinned macos:<version> refs when no matching golden image already exists in ~/.cortl/images/
  • --- (optional separator): everything after this is treated as install script content. For Linux builds, the target disk is mounted at /mnt, so install packages with chroot /mnt ... and write target files under /mnt.

Default Linux bases are intentionally minimal. Install project-specific tools in the Cortlfile install script instead of relying on built-in image contents.

Alpine example:

BASE alpine:3.19
MEMORY 8G
CPUS 6
HOOK passthrough
---
chroot /mnt apk add --no-cache ripgrep

Alpine example with Bun and Claude Code:

BASE alpine:3.19
MEMORY 4G
---
echo 'https://dl-cdn.alpinelinux.org/alpine/v3.19/community' >> /mnt/etc/apk/repositories
apk add --no-cache curl unzip
chroot /mnt apk update
chroot /mnt apk add --no-cache bash git libstdc++ libgcc nodejs npm ripgrep
chroot /mnt npm install -g @anthropic-ai/claude-code

if curl -fsSL "https://github.com/oven-sh/bun/releases/latest/download/bun-linux-aarch64-musl.zip" -o /tmp/bun.zip; then
  unzip -o /tmp/bun.zip -d /tmp
  mv /tmp/bun-linux-aarch64-musl/bun /mnt/usr/local/bin/bun
  chmod +x /mnt/usr/local/bin/bun
  rm -rf /tmp/bun.zip /tmp/bun-linux-aarch64-musl
fi

macOS example:

BASE macos:latest
MEMORY 12G
CPUS 6

Notes:

  • cortl run <base> reads that base's Cortlfile to resolve distro, CPU/memory, hook, and runtime behavior.
  • cortl base build <base> also reads the same Cortlfile; changes to BASE, resources, hook, or install script are not applied to already-running sessions.
  • Current Linux install scripts run during shared image build. If a shared image already exists, rebuild behavior is tied to cortl base build --force; per-base install-script application is tracked separately.
  • run expects an already-built base/image. If missing, build first with cortl base build <base>.
  • run restores an existing compatible warm-boot template, but it only captures a new template when --save-template is passed.

Architecture Summary

┌──────────────────────────────────────────────────────────────────────────┐
│ Pipeline                                                                 │
│  Validation → FetchImage → PrepareStorage → ConfigInjection → Hook → Boot│
└──────────────────────────────────────────────────────────────────────────┘
  • FetchImage - Resolves shared image artifacts: Alpine netboot files, Ubuntu cloud-image files with signed checksum verification, or macOS restore-image state.
  • PrepareStorage - Creates session disks from base disks and copies macOS auxiliary storage when needed.
  • ConfigInjection - Builds Linux cloud-init ISO state or macOS VM platform configuration, and configures VirtioFS where supported.
  • Hook - Runs user-defined hook scripts for Linux sessions.
  • Boot - Starts Linux VMs with warm boot or cold boot through the serial console, or starts macOS VMs through the GUI window path.

Build

bun run dev      # Build + sign
bun run release  # Release build + sign

Requires macOS 15+ and Apple Silicon.

From source, use the signed binary directly:

bun run release
./.build/release/Cortl --help

Default VM

For first-time use, cortl run is a guided Alpine path. It uses the default alpine-3.19 base, creates and builds that base if needed, then captures a warm-boot template the first time no compatible template exists.

During template setup, Cortl opens the guest shell before saving the template. Install packages, edit files, and customize the environment there. Press Ctrl+] when ready; Cortl saves the warm-boot template and exits. Future cortl run calls restore that template when compatible.

Optionally add your own shell alias or symlink after reviewing where you want the binary installed.

GitHub Releases

GitHub Release assets are built by .github/workflows/release.yml. Push a version tag to run public validation, build the release binary, sign it ad-hoc, generate a SHA256 file, and create or update the GitHub Release:

git tag v0.2.0-rc1
git push origin main v0.2.0-rc1

Tags with a suffix, such as v0.2.0-rc1, are published as prereleases. The uploaded asset name is Cortl-<version>-darwin-arm64; for example, Cortl-0.2.0-rc1-darwin-arm64.

Testing

bun run test          # Run all local tests (with stage skipping)
bun run test:full     # Force all local staged tests to run
bun run test:public   # CI-safe build + unit/lightweight e2e checks
bun run test:status   # Show which local stages will run/skip

Local VM tests use staged testing - source files are hashed per stage. If sources haven't changed since the last successful run, that stage is skipped. Current stages are build, netboot, fetch, cortlfile-claude, storage, config, and boot.

The checked-in GitHub Actions workflow at .github/workflows/ci.yml runs bun run test:public on a GitHub-hosted macos-15 runner with HOME redirected to an empty temporary directory. GitHub-hosted runners are VMs, and GitHub documents nested virtualization as experimental and unsupported. Treat this lane as build, unit, crash-report, CLI, hook, and dry-run validation only.

Full Cortl runtime validation requires launching guest VMs through Apple's Virtualization.framework. Run bun run test or bun run test:full on a dedicated physical Apple Silicon Mac, preferably as a fresh CI user or with an isolated HOME, so existing ~/.cortl bases, restore images, hooks, crash reports, and GUI/TCC state do not pollute the test bench.

The intended clean-clone direction is to make VM stages create temporary bases from the same default Cortlfile templates used by cortl base create, instead of depending on pre-existing ~/.cortl/bases/* state.

For full VM coverage in GitHub Actions, use a self-hosted runner on a physical Apple Silicon Mac and run bun run test:full from the dedicated CI macOS user's active console login session.

Release-candidate onboarding is a manual end-user smoke test. Use a separate physical Apple Silicon Mac or a fresh macOS user account on that Mac. Download the GitHub Release binary asset, not GitHub's auto-generated source archive:

  1. Open the release page for the tag, for example https://github.com/OWNER/REPO/releases/tag/v0.2.0-rc1.
  2. Download Cortl-0.2.0-rc1-darwin-arm64 and Cortl-0.2.0-rc1-darwin-arm64.sha256.
  3. In the fresh macOS user's GUI session, open Terminal and run:
cd ~/Downloads
shasum -a 256 -c Cortl-0.2.0-rc1-darwin-arm64.sha256
chmod +x ./Cortl-0.2.0-rc1-darwin-arm64
./Cortl-0.2.0-rc1-darwin-arm64 --version
mkdir -p ~/cortl-smoke
./Cortl-0.2.0-rc1-darwin-arm64 base create smoke-alpine --distro alpine:3.19
./Cortl-0.2.0-rc1-darwin-arm64 base build smoke-alpine
./Cortl-0.2.0-rc1-darwin-arm64 run smoke-alpine ~/cortl-smoke
./Cortl-0.2.0-rc1-darwin-arm64 base create smoke-macos --distro macos:latest
./Cortl-0.2.0-rc1-darwin-arm64 base build smoke-macos
./Cortl-0.2.0-rc1-darwin-arm64 run smoke-macos ~/cortl-smoke

The manual smoke test verifies the released binary, checksum file, first-run base creation, Alpine artifact download/build, and macOS restore-image download/build through the same entry point an end user runs.

References:

Artifact Trust

Cortl downloads Linux boot artifacts over HTTPS. New Ubuntu cloud-image artifact downloads verify Canonical's SHA256SUMS.gpg signature with the Ubuntu cloud image signing key before trusting SHA256SUMS, then verify the artifact hash. Install GnuPG first with brew install gnupg.

Alpine netboot artifacts and package repositories are fetched over HTTPS. The guest apk package manager verifies signed repository indexes and packages.

macOS guests install from a local restore image. If no IPSW directive is present for macos:latest, Cortl uses Apple's Virtualization.framework restore-image discovery, downloads the latest supported restore image to ~/.cortl/images/, and installs from that local file. Version-pinned macos:<version> refs reuse a matching golden image in ~/.cortl/images/ when one exists; otherwise they require an IPSW path so Cortl does not silently install a different macOS release.

License

PolyForm Noncommercial License 1.0.0. See LICENSE.

About

VMs for agents to drive and be inside of: macOS and Alpine. Super fast(sub 500 ms) warm boot an alpine VM to arbitrary workspace, and drive macOS guest VM

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors