# CLI reference: images, storage, and host commands

> Syntax, subcommands, and flags for devvm build, rebuild, images, layer, tools, space, gc, retention, setup, doctor, upgrade, completion, and limactl.

- Repository: devvmhq/devvm
- GitHub: https://github.com/devvmhq/devvm
- Human docs: https://grok-wiki.com/public/docs/devvmhq-devvm-4242986d2853
- Complete Markdown: https://grok-wiki.com/public/docs/devvmhq-devvm-4242986d2853/llms-full.txt

## Source Files

- `internal/cli/build.go`
- `internal/cli/images.go`
- `internal/cli/layer.go`
- `internal/cli/tools.go`
- `internal/cli/space.go`
- `internal/cli/gc.go`
- `internal/cli/retention.go`
- `internal/cli/upgrade.go`

---

---
title: "CLI reference: images, storage, and host commands"
description: "Syntax, subcommands, and flags for devvm build, rebuild, images, layer, tools, space, gc, retention, setup, doctor, upgrade, completion, and limactl."
---

`devvm` groups image construction, shared tool management, disk accounting, garbage collection, and host-level administration across three help sections in `internal/cli`: **Images** (`build`, `rebuild`, `images`, `layer`), **Disk space** (`space`, `gc`, `retention`), and **This machine** (`setup`, `doctor`, `upgrade`, `completion`, `limactl`), plus `devvm tools` and `devvm layer add`/`rm` under **Change your VM**. Every command validates flags and positional arguments before running `PersistentPreRunE`, exiting with code `2` on usage errors, `1` on runtime failures, `130` on `INT`/`TERM`/`HUP`, and `0` on success.

## Command inventory and pre-run behavior

Before executing a command's `RunE`, the root command enforces directory permissions (`0700` on `~/.config/devvm` and `~/.local/share/devvm`), checks layout version compatibility (`state.CheckLayout`), and runs daily automatic retention (`autoRetain`) unless the target command is explicitly exempted.

| Command | Help group | Modifies VM or disk | Takes `--json` | Skips layout check | Skips auto-retention |
| --- | --- | --- | --- | --- | --- |
| `devvm build IMAGE` | Images | Builds image chain | No | No | No |
| `devvm rebuild IMAGE` | Images | Rebuilds image and deletes unused stale images | No | No | No |
| `devvm images` | Images | Read-only | Yes | No | No |
| `devvm images rm IMAGE\|KEY...` | Images | Deletes built images | No | No | No |
| `devvm layer new NAME` | Images | Writes layer script and policy in `~/.config/devvm/layers` | No | No | No |
| `devvm layer add LAYER...` | Change your VM | Remakes project VM from new image | No | No | No |
| `devvm layer rm LAYER...` | Change your VM | Remakes project VM without specified layers | No | No | No |
| `devvm tools` | Change your VM | Interactive picker remounts tools; non-TTY / `--json` is read-only | Yes | No | No |
| `devvm tools add TOOL...` | Change your VM | Builds missing tools and mounts into project VM | No | No | No |
| `devvm tools rm TOOL...` | Change your VM | Unmounts shared tools from project VM | No | No | No |
| `devvm tools update` | Change your VM | Resolves, builds, and mounts newest shared tools | No | No | No |
| `devvm space` | Disk space | Read-only | Yes | No | Yes |
| `devvm gc` | Disk space | Deletes stale/unused artifacts and prunes caches | No | No | Yes |
| `devvm retention [KEY=VALUE...]` | Disk space | Bare call is read-only; `KEY=VALUE` writes `settings.json` | Yes (bare only) | No | Yes |
| `devvm setup` | This machine | Saves base features and new-VM defaults to `settings.json` | No | No | No |
| `devvm doctor` | This machine | Read-only host and backend readiness audit | Yes | No | No |
| `devvm upgrade` | This machine | Replaces binary or runs `brew upgrade` (`--check`/`--json` read-only) | Yes | Yes | Yes |
| `devvm completion SHELL` | This machine | Writes completion script to stdout | No | Yes | Yes |
| `devvm limactl [ARGS...]` | This machine | Passes through to `limactl` on `~/.local/share/devvm/lima` | No | No | No |

```mermaid
flowchart LR
  subgraph CLI["internal/cli command layer"]
    ImgCmds["build / rebuild / images / layer new"]
    VMCmds["layer add / layer rm / tools"]
    SpaceCmds["space / gc / retention"]
    HostCmds["setup / doctor / upgrade / completion / limactl"]
  end

  subgraph Services["Domain services"]
    BuildSvc["internal/build\nBuilds, Images, Choice"]
    VMSvc["internal/vm\nLifecycle, Tools, Housekeeping"]
    SettingsSvc["internal/settings\nRead, Save, ParseRetention"]
    UpgradeSvc["internal/upgrade\nUpgrader, Source, Detect"]
  end

  subgraph Storage["Host storage & backend"]
    DataDir["~/.local/share/devvm\nimages/, tools/, vms/, exports/, cache/"]
    ConfigDir["~/.config/devvm\nconfig.toml, layers/, ca/"]
    Backend["internal/backend (lima)\n~/.local/share/devvm/lima"]
  end

  ImgCmds --> BuildSvc
  VMCmds --> VMSvc
  SpaceCmds --> VMSvc
  SpaceCmds --> SettingsSvc
  HostCmds --> SettingsSvc
  HostCmds --> UpgradeSvc
  HostCmds --> Backend

  BuildSvc --> DataDir
  BuildSvc --> ConfigDir
  BuildSvc --> Backend
  VMSvc --> DataDir
  VMSvc --> Backend
  SettingsSvc --> DataDir
  SettingsSvc --> ConfigDir
```

### Global flags

Every command except `devvm limactl` (which sets `DisableFlagParsing: true` and forwards all flags directly to `limactl`) inherits two root persistent flags:

<ParamField body="--no-input" type="boolean" default="false">
  Never prompt interactively. Commands that require confirmation or interactive selection fail immediately and report the flag that answers the prompt (such as `--force`).
</ParamField>

<ParamField body="-v, --verbose" type="boolean" default="false">
  Stream external command output directly to stderr instead of displaying a single status line.
</ParamField>

<Note>
  `devvm` rejects empty string arguments and empty string flag values (such as `--from ""` or `--features ""`) across all commands via `noBlanks` before execution, exiting with code `2`.
</Note>

---

## Image and layer commands

### `devvm build`

Builds a target image and every parent image beneath it in the chain, skipping images that are already built and current.

```bash
devvm build IMAGE [--from IMAGE]
```

#### Arguments and flags

<ParamField body="IMAGE" type="string" required>
  Target image name: `base`, or one or more layer names joined with `+` from bottom to top (for example, `docker` or `docker+mailpit`). Repeated layers and explicit `base+` prefixes are normalized (`base+foo+bar` and `foo+bar+foo` both resolve to `foo+bar`). Passing a shared tool name (`node`, `python`, `rust`, `go`, `dotnet`, `claude`, `codex`, `opencode`) fails with a message pointing to `devvm tools`.
</ParamField>

<ParamField body="--from" type="string" default="$DEVVM_FROM">
  OCI/Docker container image reference that `base` starts from (such as `node:24` or `alpine:3.21`) instead of Ubuntu's cloud image.
</ParamField>

#### Behavior

1. Validates `--from` syntax (`from.Check`), parses `IMAGE` (`build.Names.Parse`), and verifies backend readiness (`backend.Check`).
2. If `base` features have not yet been chosen in `config.toml` or `settings.json`, prompts interactively in a terminal via `pickFeatures` (or selects all built-in features `cli-tools`, `fzf`, `omb` when non-interactive).
3. Computes the 12-hex-character SHA-256 content key for each image in the chain (`base` first, then each layer in order) and builds any missing or stale step.

---

### `devvm rebuild`

Rebuilds an existing image from its parent as the parent currently stands, increments its rebuild counter in `~/.local/share/devvm/images/rebuilds.json`, and deletes stale images that no VM uses.

```bash
devvm rebuild IMAGE [--from IMAGE] [-f|--force]
```

#### Arguments and flags

<ParamField body="IMAGE" type="string" required>
  Image to rebuild (`base`, or layers joined with `+` bottom first).
</ParamField>

<ParamField body="--from" type="string" default="$DEVVM_FROM">
  Container image reference that `base` starts from instead of Ubuntu's cloud image.
</ParamField>

<ParamField body="-f, --force" type="boolean" default="false">
  Skip the confirmation prompt when child images are built on top of `IMAGE`. Required when running without a terminal or with `--no-input` if dependent images exist.
</ParamField>

#### Behavior

- When `IMAGE` is `base` (without `--from`), `PinOrigin` pins the newest upstream Ubuntu cloud image release (`backend.PinNewest`) and fetches fresh apt packages instead of reusing cached package sets in `~/.local/share/devvm/packages/`.
- When other built images depend on `IMAGE`, `devvm rebuild` lists those dependent images and prompts for confirmation unless `--force` is passed. Those child images become `stale` until rebuilt or next used.
- After building the new image, `devvm rebuild` deletes the superseded image and any stale images built on it that no VM uses.

---

### `devvm images`

Lists all VM images across backends and all shared tool images, followed by summary notes on stderr (`imagesNotes`) and optional JSON output.

```bash
devvm images [--json]
devvm images rm IMAGE|KEY... [--from IMAGE] [-f|--force]
```

#### Flags (`devvm images`)

<ParamField body="--json" type="boolean" default="false">
  Print the image inventory, shared tool inventory, totals, and Nix cache usage as a version 1 JSON object on stdout.
</ParamField>

#### Table output (`devvm images`)

`devvm images` outputs two tables separated by a blank line:

1. **Images table** (`IMAGE`, `FROM`, `DIGEST`, `KEY`, `STATE`, `SIZE`):
   - `STATE` values: `current` (green), `stale` (yellow; inputs or rebuild counters changed), `unfinished` (yellow; interrupted build), or `building` (another `devvm` process holds the image lock).
   - `DIGEST` shows the first 12 hex characters of the origin SHA-256 (`-` if unknown).
2. **Shared tools table** (`NAME`, `TOOL`, `VERSION`, `LIBC`, `SIZE`, `VMS`, `STATE`):
   - `STATE` values: `current` (green; version in `mise.lock` for new VMs), `in use` (plain; older version still mounted by at least one VM), `unused` (yellow; older version mounted by `0` VMs), `not built` (yellow; current version in `mise.lock` when another version of the same tool is built), or `not installed` (no version built).

#### Subcommand: `devvm images rm`

Deletes one or more built images by name (such as `foo+bar`) or by 12-hex-character image key (such as `7a76021c46ef`).

<ParamField body="IMAGE|KEY..." type="string" required>
  One or more image names or 12-hex-digit keys. Every specified target must exist and be built, or `devvm images rm` deletes nothing. Stale and unfinished images can only be targeted by their `KEY`.
</ParamField>

<ParamField body="--from" type="string" default="$DEVVM_FROM">
  Resolve named images against the specified `--from` container base image.
</ParamField>

<ParamField body="-f, --force" type="boolean" default="false">
  Delete without prompting for confirmation. Required without a terminal or with `--no-input`.
</ParamField>

<Warning>
  On macOS, VMs cloned from an image continue working after the image is deleted because APFS clones share blocks independently. On Linux hosts, VMs use qcow2 backing overlays; `devvm images rm` refuses to delete an image whose disk is still backed by an existing VM.
</Warning>

<RequestExample>
```bash
devvm images --json
```
</RequestExample>

<ResponseExample>
```json
{
  "version": 1,
  "images": [
    {
      "name": "base",
      "from": "ubuntu 26.04 (20261001)",
      "digest": "sha256:8eb876c2ccc4b6ff89e4a65d511815923b285e04503ba465809bf587f763da24",
      "key": "7a76021c46ef",
      "state": "current",
      "size": 2254857830,
      "sized": true,
      "backend": "lima"
    }
  ],
  "tools": [
    {
      "name": "node",
      "tool": "node",
      "version": "24.11.0",
      "built": true,
      "backend": "lima",
      "libc": "gnu",
      "size": 125829120,
      "vms": 1,
      "state": "current"
    }
  ],
  "total": 2380686950,
  "unused_tools": 0,
  "nix_cache": {
    "files": 0,
    "size": 0,
    "cap": 10737418240
  }
}
```
</ResponseExample>

---

### `devvm layer`

Creates custom layer scripts in `~/.config/devvm/layers` or remakes the current project folder's VM with layers added or removed. Running `devvm layer` with no subcommand prints its help and exits `0`.

```bash
devvm layer new NAME
devvm layer add LAYER... [-f|--force] [--no-export] [--clean-home] [--here | --parent]
devvm layer rm LAYER... [-f|--force] [--no-export] [--clean-home] [--here | --parent]
```

#### Subcommands

<Tabs>
  <Tab title="devvm layer new">
    Creates `~/.config/devvm/layers/<NAME>.sh` (with `#!/bin/bash` and `set -euo pipefail`) and `~/.config/devvm/layers/<NAME>.policy` (if the policy file does not already exist). Refuses to overwrite an existing `<NAME>.sh`.

    <ParamField body="NAME" type="string" required>
      Layer identifier matching `^[a-z0-9][a-z0-9-]*$`. Must not be `base`, a built-in layer (`desktop`, `docker`, `nix`), or a shared tool name (`node`, `python`, `rust`, `go`, `dotnet`, `claude`, `codex`, `opencode`).
    </ParamField>
  </Tab>
  <Tab title="devvm layer add">
    Remakes the current folder's VM from an image with `LAYER...` appended on top of its existing layers in the order given, then opens an interactive shell in the VM.

    <ParamField body="LAYER..." type="string" required>
      One or more built-in (`desktop`, `docker`, `nix`) or user-defined layer names whose scripts exist in `~/.config/devvm/layers/<name>.sh`. Layers the VM already has are kept in their current position; if the VM already has all requested layers, `devvm` reports that and changes nothing.
    </ParamField>

    <ParamField body="-f, --force" type="boolean" default="false">
      Remake the VM without prompting for confirmation.
    </ParamField>

    <ParamField body="--no-export" type="boolean" default="false">
      Skip exporting coding agent history to `~/.local/share/devvm/exports/` before replacing the VM disk.
    </ParamField>

    <ParamField body="--clean-home" type="boolean" default="false">
      Start with the new image's `/home/<user>` directory instead of copying the existing VM's home directory across the remake.
    </ParamField>

    <ParamField body="--here" type="boolean" default="false">
      Target this directory's own VM, ignoring any parent directory's VM. Mutually exclusive with `--parent`.
    </ParamField>

    <ParamField body="--parent" type="boolean" default="false">
      Target the nearest ancestor directory's VM, even across a nested Git repository boundary. Mutually exclusive with `--here`.
    </ParamField>
  </Tab>
  <Tab title="devvm layer rm">
    Remakes the current folder's VM from an image without `LAYER...`, preserving the remaining layers in their original order (or falling back to `base` if all layers are removed), then opens a shell in the VM.

    <ParamField body="LAYER..." type="string" required>
      One or more layer names to remove. Unlike `layer add`, `layer rm` allows removing a custom layer whose `~/.config/devvm/layers/<name>.sh` script has already been deleted.
    </ParamField>

    <ParamField body="-f, --force" type="boolean" default="false">
      Remake the VM without prompting for confirmation.
    </ParamField>

    <ParamField body="--no-export" type="boolean" default="false">
      Skip exporting coding agent history before replacing the VM disk.
    </ParamField>

    <ParamField body="--clean-home" type="boolean" default="false">
      Use the new image's home directory instead of preserving the VM's existing home directory.
    </ParamField>

    <ParamField body="--here / --parent" type="boolean" default="false">
      Control folder-to-VM resolution (mutually exclusive).
    </ParamField>
  </Tab>
</Tabs>

<Note>
  Both `devvm layer add` and `devvm layer rm` refuse `--clone` VMs because the repository clone resides on the VM's root disk. Run `devvm clone off` first, then modify layers, then run `devvm clone on`.
</Note>

---

## Shared tool commands (`devvm tools`)

Inspects or modifies the read-only EROFS shared toolchains (`node`, `python`, `rust`, `go`, `dotnet`) and coding agents (`claude`, `codex`, `opencode`) mounted into the current folder's VM.

```bash
devvm tools [--json] [--here | --parent]
devvm tools add TOOL... [--here | --parent]
devvm tools rm TOOL... [--here | --parent]
devvm tools update [--here | --parent]
```

### Modes and subcommands

| Invocation | Behavior | Opens VM shell |
| --- | --- | --- |
| `devvm tools` (in a TTY without `--no-input` or `--json`) | Opens an interactive multi-select picker showing toolchains, coding agents, and the VM's read-only layers; builds any newly selected tools and applies changes | Yes |
| `devvm tools` (non-TTY, `--no-input`, or `--json`) | Lists the VM's mounted tools, their versions, any newer version in `mise.lock`, and whether that newer version is built (`TOOL`, `VERSION`, `NEWER`, `BUILT`). Starts no VM and makes no network requests | No |
| `devvm tools add TOOL...` | Validates tool names against `image.Toolchains` + `image.Agents`, builds any unbuilt tools, adds them to the VM, and prompts before restarting if a new coding agent is added to a running VM | Yes |
| `devvm tools rm TOOL...` | Removes the specified shared tools from the VM's state and unmounts them; tools installed locally inside the VM stay untouched | Yes |
| `devvm tools update` | Checks upstream for newer shared tool versions, builds them, switches the VM to the newest versions without modifying the VM disk, and opens a shell | Yes |

### Argument validation and JSON output

- `devvm tools add` and `devvm tools rm` require each tool name as a separate argument (`devvm tools add python claude`). Passing names joined with `+` or `,` (such as `python,claude` or `python+claude`) fails with exit code `2` (`msg.ToolsJoined`).
- `devvm tools --json` outputs a version 1 JSON object where `newer` and `newer_built` are omitted when the VM already has the version in `mise.lock`:

```json
{
  "version": 1,
  "tools": [
    {
      "tool": "node",
      "version": "24.10.0",
      "newer": "24.11.0",
      "newer_built": true
    }
  ]
}
```

---

## Storage, garbage collection, and retention commands

### `devvm space`

Reports disk usage across all categories in `~/.local/share/devvm` and across registered backends without starting any VM or modifying state. Each backend query is bounded by a 30-second timeout (`spaceTimeout`).

```bash
devvm space [--json]
```

#### Table columns (`devvm space`)

| Column | Description |
| --- | --- |
| `WHAT` | Artifact category: `VMs`, `images`, `tools gnu` / `tools musl` / `tools -`, `history`, `network logs`, `run logs`, `mise`, `packages`, `Nix cache`, `leftover state`, `state`, and `total` |
| `COUNT` | Item count in the category |
| `SIZE` | Disk usage in binary powers of 1024 (`512K`, `2.1G`), or `-` when unsized |
| `KEPT` | Retention classification: `remade` (rebuilt on demand), `only copy` (yellow; VMs and agent history exports), `log` (logs), `state` (metadata), or `leftover` (yellow; orphaned VM folders and dead link sockets) |
| `GC` | Bytes that `devvm gc` (with no extra flags) will free immediately |
| `FREED BY` | Command or policy that frees the remaining bytes (`devvm rm`, `images rm`, `gc --tool-images`, `gc --history`, `the newest 20 stay`, `rebuild base`, `gc --nix-cache`, `gc --orphans`, or `-`) |
| `RETAIN` | Active retention setting for the row and the bytes it would free right now (for example, `30d (1.0M)`), or `-` when off |

When Lima has entries in its own download cache (`~/Library/Caches/lima` on macOS, `~/.cache/lima` on Linux), `devvm space` prints a second table (`CACHE`, `COUNT`, `SIZE`, `PATH`) outside `total`. On stderr, `devvm space` also notes any backend that did not respond (`unknown`), VMs with `devvm`'s label that have no state in this `devvm` (`strays`), VMs whose project directory was deleted (`folder_gone`), and VMs whose project directory is on an unmounted volume (`folder_not_connected`).

#### JSON schema (`devvm space --json`)

<ResponseField name="version" type="number">
  Schema version (`1`).
</ResponseField>

<ResponseField name="devvm" type="spaceRow[]">
  Rows from the primary table (`what`, optional `backend`, `count`, `size`, `sized`, `kept`, `gc`, `rest`, and optional `retain` and `retain_frees`). Never `null` (`[]` when empty).
</ResponseField>

<ResponseField name="total" type="number">
  Sum of `size` across `devvm` rows in bytes.
</ResponseField>

<ResponseField name="gc" type="number">
  Sum of `gc` across `devvm` rows in bytes.
</ResponseField>

<ResponseField name="backends" type="cacheRow[]">
  Backend cache rows (`backend`, `name`, optional `path`, `count`, `size`, `sized`). Never `null` (`[]` when empty).
</ResponseField>

<ResponseField name="unknown" type="string[]">
  Backends used by known VMs that failed to respond within 30 seconds (omitted when empty).
</ResponseField>

<ResponseField name="strays" type="strayVM[]">
  Backend VMs carrying `devvm`'s label with no matching local state (`backend`, `name`, `data`; omitted when empty).
</ResponseField>

<ResponseField name="folder_gone" type="goneVM[]">
  Existing VMs whose host project folder no longer exists (`backend`, `name`, `folder`; omitted when empty).
</ResponseField>

<ResponseField name="folder_not_connected" type="goneVM[]">
  Existing VMs whose host project folder resides on a disconnected mount (`backend`, `name`, `folder`; omitted when empty).
</ResponseField>

---

### `devvm gc`

Deletes stale and unfinished images, unused shared tool images, stale host `mise` versions, unreferenced base `.deb` packages, and least-recently-used files in the shared Nix cache exceeding its cap (`nix-cache.size`, or `10 GiB` when unset). `devvm gc` never deletes a project VM.

```bash
devvm gc [--tool-images [TOOL...]] [--history [--older-than DURATION]] [--nix-cache] [--orphans] [--caches] [--retention] [-f|--force]
```

#### Flags and arguments

<ParamField body="--tool-images" type="boolean" default="false">
  Also delete `current` shared tool images that no VM uses. Optionally accepts positional `TOOL...` names (`node`, `python`, `rust`, `go`, `dotnet`, `claude`, `codex`, `opencode`) to restrict deletion to those specific tools. Passing positional `TOOL...` arguments without `--tool-images` fails with exit code `2`.
</ParamField>

<ParamField body="--history" type="boolean" default="false">
  Also delete saved agent history exports in `~/.local/share/devvm/exports/`.
</ParamField>

<ParamField body="--older-than" type="string">
  Requires `--history` (fails with exit code `2` if given without `--history`). Deletes only history exports at least `DURATION` old (such as `12h`, `30d`, `2w`).
</ParamField>

<ParamField body="--nix-cache" type="boolean" default="false">
  Delete the entire shared Nix binary cache (`~/.local/share/devvm/cache/nix-cache/`) instead of only trimming files above the cap.
</ParamField>

<ParamField body="--orphans" type="boolean" default="false">
  Forget state folders (`vms/<hash>/`), SSH config entries, `devvm` Git remotes (for `--clone` VMs), links from other VMs, and dead link sockets for VMs that no longer exist in their backend. Skips VMs whose backend is unreachable, VMs created under a different `DEVVM_LIMA_HOME`, and VMs whose state changed within the last hour.
</ParamField>

<ParamField body="--caches" type="boolean" default="false">
  After running `devvm`'s own cleanup, delete `devvm`'s share of each backend's download cache (Ubuntu cloud images downloaded by `devvm` that no image record references and that have not changed in the last hour), prompting separately per backend unless `--force` is set.
</ParamField>

<ParamField body="--retention" type="boolean" default="false">
  Immediately apply all configured retention policies in `settings.json` / `config.toml`, including `orphans.age`.
</ParamField>

<ParamField body="-f, --force" type="boolean" default="false">
  Delete planned items and backend cache entries without interactive confirmation. Required when running without a terminal or with `--no-input`.
</ParamField>

<Info>
  While another `devvm` process is creating a VM or updating tools (holding `making.lock`), `devvm gc --tool-images` retains all `current` shared tool images that have no VM yet, prints which tools were kept (`msg.GCToolsMaking`), and deletes the rest of the plan.
</Info>

---

### `devvm retention`

Displays or updates automatic retention rules in `~/.local/share/devvm/settings.json`. Every retention setting defaults to `off` until explicitly configured in `config.toml` or via `devvm retention`.

```bash
devvm retention [--json]
devvm retention KEY=VALUE...
```

#### Retention keys and value formats

| Key | Value type | Default when `off` | Applied by daily `autoRetain` | Applied by `devvm gc --retention` | What it deletes |
| --- | --- | --- | --- | --- | --- |
| `logs.runs` | Integer `1`..`10000` or `off` | Keeps newest `20` run logs | Yes (every run via `KeepRunLogs`) | Yes | Oldest `.log` files in `~/.local/share/devvm/logs/runs/` beyond the count |
| `history.age` | Duration (`30d`, `12w`, `24h`) or `off` | `off` (keeps all) | Yes | Yes | Agent history exports in `exports/` older than the duration |
| `history.count` | Integer `1`..`10000` or `off` | `off` (keeps all) | Yes | Yes | Oldest agent history exports in `exports/` beyond the newest `N` |
| `tools.unused-age` | Duration (`30d`, `2w`) or `off` | `off` (keeps all) | Yes (unless `tools.lock` is busy) | Yes | `current` shared tool images that have had `0` VMs for at least the duration |
| `orphans.age` | Duration (`7d`, `30d`) or `off` | `off` (keeps all) | No | Yes | Leftover state in `vms/<hash>/` for VMs missing from their backend whose state last changed at least `DURATION` ago |
| `nix-cache.size` | Byte size (`5G`, `500M`, `10GiB`) or `off` | `10 GiB` cap | Yes | Yes | Least-recently-used files in `cache/nix-cache/` exceeding the size cap |

<Note>
  When both `history.age` and `history.count` are set, an export is deleted if either rule retires it. Passing `--json` together with `KEY=VALUE` arguments is rejected with exit code `2` (`showsOnly`). If a key is set in `~/.config/devvm/config.toml` under `[retention]`, `config.toml` takes precedence over `settings.json`.
</Note>

<CodeGroup>
```bash Terminal output
$ devvm retention
logs.runs=off # built-in
history.age=90d # ~/.local/share/devvm/settings.json
history.count=off # built-in
tools.unused-age=30d # ~/.config/devvm/config.toml
orphans.age=off # built-in
nix-cache.size=off # built-in
```

```json devvm retention --json
{
  "version": 1,
  "settings": {
    "history.age": "90d",
    "history.count": "off",
    "logs.runs": "off",
    "nix-cache.size": "off",
    "orphans.age": "off",
    "tools.unused-age": "30d"
  },
  "sources": {
    "history.age": "settings.json",
    "history.count": "built-in",
    "logs.runs": "built-in",
    "nix-cache.size": "built-in",
    "orphans.age": "built-in",
    "tools.unused-age": "config.toml"
  }
}
```
</CodeGroup>

---

## Host configuration and maintenance commands

### `devvm setup`

Configures `base` image features and default settings for newly created VMs in `~/.local/share/devvm/settings.json`. Existing VMs keep the settings, agents, and layers they were created with.

```bash
devvm setup [--features LIST|none] [--agents LIST|none] [--layers LIST|none]
```

#### Flags

<ParamField body="--features" type="string">
  Comma-separated list of `base` image features (`cli-tools`, `fzf`, `omb`), or `none`. Changing the feature selection marks `base` and any images built on it for rebuild on next use.
</ParamField>

<ParamField body="--agents" type="string">
  Comma-separated list of coding agents that new VMs receive by default (`claude`, `codex`, `opencode`), or `none`.
</ParamField>

<ParamField body="--layers" type="string">
  Comma-separated list of layers that new VMs' images receive by default, ordered bottom first (such as `docker,mailpit`), or `none`.
</ParamField>

#### Interactive vs. flag execution

- **When any of `--features`, `--agents`, or `--layers` is passed:** `devvm setup` applies only the specified flags without prompting for the other settings.
- **When run with no flags in a terminal:** `devvm setup` walks through five prompts in order, starting from current values and skipping any setting locked in `~/.config/devvm/config.toml` (printing a note naming `config.toml`):
  1. `base` image features (`cli-tools`, `fzf`, `omb`)
  2. Default CPUs and memory for new VMs (`vm.SizeChoice.Ask`)
  3. Default network tier for new VMs (`allowlist`, `open`, `local-only`, `offline`)
  4. Default coding agents for new VMs (`claude`, `codex`, `opencode`)
  5. Default image layers for new VMs (built-in layers and custom layers in `~/.config/devvm/layers`)

---

### `devvm doctor`

Checks host prerequisites for the active backend, inspects `PATH` and state directory hygiene, reports whether `docker` (for `--from` builds) and corporate proxy CAs are present, and prints backend feature support. `devvm doctor` never installs software or modifies files.

```bash
devvm doctor [--json]
```

#### Flags

<ParamField body="--json" type="boolean" default="false">
  Output the diagnostic report as a version 1 JSON object and exit `0` even when backend `problems` are present.
</ParamField>

#### Checks and exit behavior

1. **Feature support table (`FEATURE`, `SUPPORT`):** Lists all 19 `backend.Features` (`build-image`, `build-confinement`, `from`, `clone`, `project-mount`, `agent-folders`, `tools-share`, `egress-proxy`, `ssh`, `ports`, `ports-all`, `agent-forwarding`, `agent-toggle`, `cpus`, `memory`, `disk`, `docker-layer`, `nix-layer`, `link`) with support status (`native`, `emulated`, `unreviewed`, or `none`).
2. **Host Docker and proxy CA notes:** Reports whether `docker` is on `PATH` (used when converting `--from` container images) and whether `HTTPS_PROXY` / `proxy.https` is configured while `~/.config/devvm/ca` is empty.
3. **Non-fatal warnings (`warnings`):** Never cause `devvm doctor` to exit non-zero:
   - Another `devvm` binary appears earlier on `PATH` than the running executable.
   - Disk free space on the backend volume is below the backend's floor.
   - Host hardware is below recommended minimums (`< 4` CPUs or `< 8 GiB` RAM).
   - Malformed lines in `.policy` files (`netpolicy.NotRules`).
   - Unexpected files, missing `vm.json`, or unreadable directories inside `~/.local/share/devvm/vms/` (`state.VMFolderProblems`).
4. **Fatal backend problems (`problems`):** Missing or unsupported `limactl`, `qemu-system-*`, or `/dev/kvm` access on Linux. Without `--json`, `devvm doctor` exits `1` when one or more `problems` exist; with `--json`, it prints the report and exits `0`.

```json
{
  "version": 1,
  "backend": "lima",
  "problems": [
    {
      "key": "lima",
      "fix": "brew install lima",
      "message": "Lima isn't installed. Run: brew install lima"
    }
  ],
  "warnings": [],
  "docker": true,
  "features": {
    "lima": {
      "agent-folders": "native",
      "agent-forwarding": "native",
      "agent-toggle": "native",
      "build-confinement": "native",
      "build-image": "native",
      "clone": "native",
      "cpus": "native",
      "disk": "native",
      "docker-layer": "native",
      "egress-proxy": "native",
      "from": "native",
      "link": "native",
      "memory": "native",
      "nix-layer": "native",
      "ports": "native",
      "ports-all": "native",
      "project-mount": "native",
      "ssh": "native",
      "tools-share": "native"
    }
  },
  "release": {
    "latest": "0.2.0",
    "checked": "2026-11-05T00:00:00Z"
  }
}
```

---

### `devvm upgrade`

Upgrades `devvm` using the installation method detected for the running binary (`install.sh` layout in `~/.local/lib/devvm/<version>` or Homebrew formula `devvm` / `devvm-rc`), or checks for newer releases without installing.

```bash
devvm upgrade [--check] [--to VERSION] [-f|--force] [--json]
```

#### Flags and mutual exclusions

<ParamField body="--check" type="boolean" default="false">
  Check GitHub releases and report whether a newer version is available without installing anything. Mutually exclusive with `--force` and `--to`.
</ParamField>

<ParamField body="--to" type="string">
  Install the exact release `VERSION` (leading `v` is stripped; validated by `upgrade.Valid`), allowing downgrades or prereleases (`0.2.0-rc.1`). Only supported for `install.sh` installations. Mutually exclusive with `--check` and `--json`.
</ParamField>

<ParamField body="-f, --force" type="boolean" default="false">
  Upgrade without prompting for confirmation. Mutually exclusive with `--check` and `--json`.
</ParamField>

<ParamField body="--json" type="boolean" default="false">
  Query release availability and print a version 1 JSON object (`current`, `latest`, `newer`, `install`, `path`) without installing anything. Mutually exclusive with `--force` and `--to`.
</ParamField>

#### Verification and installation methods

- **`install.sh` (`~/.local/lib/devvm/<version>/bin/devvm`):** Fetches the release archive, `SHA256SUMS`, and `SHA256SUMS.minisig`, verifies the Minisign `ED` signature against the embedded public key in `packaging/minisign.pub`, verifies the archive SHA-256, unpacks into `~/.local/lib/devvm/<new-version>`, atomically switches the `~/.local/bin/devvm` symlink (with rollback on failure), and removes old versions under `~/.local/lib/devvm/` that are not running.
- **Homebrew (`homebrew` or `homebrew-rc`):** Runs `brew upgrade devvm` or `brew upgrade devvm-rc`. Passing `--to` on a Homebrew installation is refused because Homebrew manages formula versions.

---

### `devvm completion`

Generates the shell completion script for `bash`, `zsh`, or `fish` on stdout. Completions dynamically resolve image names, `+`-joined layer stacks, `,`-joined `--tools`, `--agents`, `--layers`, and `--features` lists, and `--ports` keywords (`all`, `none`), while disabling default filename completion (`ShellCompDirectiveNoFileComp`).

```bash
devvm completion SHELL
```

<ParamField body="SHELL" type="string" required>
  Target shell: must be `bash`, `zsh`, or `fish`. Any other value exits with code `2`.
</ParamField>

<CodeGroup>
```bash ~/.bashrc
source <(devvm completion bash)
```

```zsh ~/.zshrc
source <(devvm completion zsh)
```

```fish ~/.config/fish/config.fish
devvm completion fish | source
```
</CodeGroup>

---

### `devvm limactl`

Runs Lima's `limactl` binary directly against `devvm`'s isolated `LIMA_HOME` (`~/.local/share/devvm/lima`, or `$DEVVM_LIMA_HOME` when set), forwarding all arguments and flags verbatim, including `--help`.

```bash
devvm limactl [ARGS...]
```

<Warning>
  `devvm limactl` is intended for low-level debugging and cleanup (such as `devvm limactl list` or deleting a stray VM with `devvm limactl delete NAME`). Starting a VM via `devvm limactl start` boots it **unconfined** without `devvm _proxy`, regardless of the VM's configured network tier. Always start project VMs with `devvm`.
</Warning>

## Related pages

<CardGroup cols={2}>
  <Card title="CLI reference: VM and workspace commands" href="/cli-vm-and-workspace-commands">
    Syntax, flags, and JSON schemas for devvm (up), ls, restart, stop, update, rm, adopt, size, ports, host, code, desktop, and history.
  </Card>
  <Card title="CLI reference: network, security, and agent commands" href="/cli-security-network-and-agent-commands">
    Syntax, subcommands, and flags for devvm network, secret, agent, git-protect, clone, ssh-agent, link, and group.
  </Card>
  <Card title="Images, layers, and content keys" href="/image-and-layer-model">
    How devvm computes 12-hex SHA-256 content keys for base images, built-in and custom layers, and read-only EROFS shared tool images.
  </Card>
  <Card title="Manage disk retention, garbage collection, and upgrades" href="/disk-retention-and-upgrades">
    Operational workflows for automatic retention policies, garbage collection locks, and Minisign-verified binary upgrades.
  </Card>
</CardGroup>
