# Manage disk retention, garbage collection, and upgrades

> Configure automatic retention policies for run logs, agent history exports, unused tools, orphaned state, and Nix caches, and upgrade devvm installations.

- 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/vm/housekeep.go`
- `internal/vm/exports.go`
- `internal/settings/retention.go`
- `internal/upgrade/upgrade.go`
- `internal/upgrade/source.go`
- `internal/upgrade/install.go`

---

---
title: "Manage disk retention, garbage collection, and upgrades"
description: "Configure automatic retention policies for run logs, agent history exports, unused tools, orphaned state, and Nix caches, and upgrade devvm installations."
---

`devvm` stores virtual machine disks, base and layer images, read-only shared tool images, agent history exports, run logs, and build caches under `~/.local/share/devvm` (`$XDG_DATA_HOME/devvm`). Through `vm.Housekeeping`, `devvm space` inspects disk consumption across host directories and Lima caches, `devvm retention` configures automatic daily pruning policies, and `devvm gc` executes interactive or scripted garbage collection. For the CLI binary itself, `devvm upgrade` (`upgrade.Upgrader`) performs Minisign-verified atomic binary upgrades for script installations or delegates to Homebrew for formula installations.

<Warning>
Neither `devvm gc` nor automatic retention ever deletes a project VM (`vms/<hash>/` backed by an existing backend instance) or a current base/layer image. Project VMs can contain unsaved work and are only removed explicitly with `devvm rm`.
</Warning>

## Inspect disk usage (`devvm space`)

`devvm space` inventories everything `devvm` and its backends store on the host without starting any VM or modifying state. Each registered backend is queried with a 30-second timeout (`spaceTimeout`).

```sh
devvm space
devvm space --json
```

### Reported storage categories

`devvm space` outputs two tables: a primary table of `devvm`-managed assets with a `total` summary row, and a secondary `CACHE` table for backend-owned caches (such as Lima's download cache at `~/Library/Caches/lima` on macOS or `~/.cache/lima` on Linux).

| `WHAT` row | Host location (relative to `~/.local/share/devvm`) | `KEPT` classification | `GC` column (`devvm gc` default) | `FREED BY` column | Applicable `RETAIN` keys |
| --- | --- | --- | --- | --- | --- |
| `VMs` | `lima/<vm-name>/` | `only copy` | `0` | `devvm rm` | — |
| `images` | `lima/devvm-img-<key>/` and `images/<key>/` | `remade` | Stale and unfinished images | `images rm` | — |
| `tools gnu` / `tools musl` | `tools/<image>.erofs` and `<image>.json` | `remade` | Tool images in `unused` state | `gc --tool-images` | `tools.unused-age` |
| `history` | `exports/<vm>-<YYYYMMDD>-<HHMMSS>.tar.gz` | `only copy` | `0` | `gc --history` | `history.age`, `history.count` |
| `network logs` | `vms/<hash>/logs/` and `logs/tools-check.log` | `log` | `0` | `devvm rm` | — |
| `run logs` | `logs/runs/*.log` | `log` | `0` | `the newest <N> stay` (`20` when off) | `logs.runs` |
| `mise` | `cache/mise/<version>/` | `remade` | Older host `mise` versions | `-` | — |
| `packages` | `packages/*.deb` and `packages/sets/` | `remade` | Unreferenced `.deb` files and sets | `rebuild base` | — |
| `Nix cache` | `cache/nix-cache/` | `remade` | Bytes exceeding `nix-cache.size` cap | `gc --nix-cache` | `nix-cache.size` |
| `leftover state` | `vms/<hash>/` (orphans) and dead sockets in `run/` | `leftover` | `0` | `gc --orphans` | `orphans.age` |
| `state` | `vms/`, `images/`, `run/`, `mise.lock*`, `cache/from-digests.json` | `state` | `0` | `-` | — |

<Note>
On macOS, Lima clones VM disks and child images using APFS copy-on-write (`cp -c`). Because `devvm space` measures each file's allocated blocks the way `du` does, shared blocks are counted in both the image and the cloned VM, making the reported `total` larger than actual physical disk consumption.
</Note>

### VM and backend anomaly notices

During inventory, `devvm space` detects four out-of-band states, printing notes to standard error and populating top-level arrays in `--json` output:

- **Unreachable backends (`unknown`)**: Backends referenced by at least one VM's `vm.json` that failed `backend.Check` or timed out after 30 seconds. Their VMs and images are omitted from the totals.
- **Stray VMs (`strays`)**: Backend instances carrying `devvm`'s label that have no corresponding state directory in `~/.local/share/devvm/vms/` (for example, VMs created under a different `XDG_DATA_HOME`). `devvm` never deletes strays; remove them with `devvm limactl delete <name>`.
- **Missing project folders (`folder_gone`)**: VMs that still exist in the backend whose project directory on the host was moved or deleted. They remain counted under `VMs` until re-linked with `devvm adopt` or deleted with `devvm rm <vm-name>`.
- **Unmounted disks (`folder_not_connected`)**: VMs whose project folder resides on an unmounted external volume. `devvm rm <vm-name>` requires interactive terminal confirmation and refuses `--force` when a volume is disconnected.

<RequestExample>
```sh
devvm space --json
```
</RequestExample>

<ResponseExample>
```json
{
  "version": 1,
  "devvm": [
    {
      "what": "VMs",
      "backend": "lima",
      "count": 2,
      "size": 5368709120,
      "sized": true,
      "kept": "only copy",
      "gc": 0,
      "rest": "devvm rm"
    },
    {
      "what": "history",
      "count": 3,
      "size": 3145728,
      "sized": true,
      "kept": "only copy",
      "gc": 0,
      "rest": "gc --history",
      "retain": "30d",
      "retain_frees": 1048576
    }
  ],
  "total": 5371854848,
  "gc": 0,
  "backends": [
    {
      "backend": "lima",
      "name": "download cache",
      "path": "/Users/you/Library/Caches/lima",
      "count": 1,
      "size": 629145600,
      "sized": true
    }
  ]
}
```
</ResponseExample>

---

## Configure automatic retention (`devvm retention`)

Every retention policy is `off` by default. When enabled, `devvm` applies configured retention rules automatically at command startup at most once every 24 hours (`retainEvery`), or immediately when you run `devvm gc --retention`.

### Retention keys and behavior

| Key | Value format | Built-in behavior when `off` | What retention deletes |
| --- | --- | --- | --- |
| `logs.runs` | Integer `1`–`10000` | Keeps newest `20` run logs | Run logs in `logs/runs/*.log` beyond the newest `N` files, pruned by the UI logger on command execution. |
| `history.age` | Duration (`30m`, `12h`, `90d`, `2w`) | Keeps all exports | Agent history archives (`exports/<vm>-<stamp>.tar.gz` and their `.folder` metadata files) whose timestamp is at least `history.age` old. |
| `history.count` | Integer `1`–`10000` | Keeps all exports | Oldest remaining agent history exports after `history.age` filtering so at most the newest `N` exports remain. |
| `tools.unused-age` | Duration (`30d`, `4w`) | Keeps all current tool images | Current shared tool images in `tools/` that no VM lists in `vm.json` and that have been continuously idle for at least `tools.unused-age` (tracked in `<image>.json`). |
| `orphans.age` | Duration (`30d`, `2w`) | Keeps all orphan state | State folders (`vms/<hash>/`), SSH config entries, and `--clone` git remotes for VMs missing from their backend whose `vm.json` mtime is at least `orphans.age` old. Applied **only** by `devvm gc --retention`. |
| `nix-cache.size` | Binary size (`500M`, `20G`, `1T`) | `10G` (`10 GiB`) cap | Least-recently-used `.narinfo` and `nar/*` files in `cache/nix-cache/` until total size is at or below the cap. |

- **Durations** (`state.ParseDuration`) require a positive integer followed by `m` (minutes), `h` (hours), `d` (days, 24h), or `w` (weeks, 7d).
- **Sizes** (`state.ParseSize`) require a positive integer followed by `K`, `M`, `G`, or `T` (powers of 1024).

### Configuration precedence and CLI usage

`devvm` resolves retention settings in this order:
1. `[retention]` in `~/.config/devvm/config.toml` (user-authored, read-only to `devvm`).
2. `~/.local/share/devvm/settings.json` (written by `devvm retention KEY=VALUE...`).
3. Built-in default (`off`).

If a key is defined in `config.toml`, running `devvm retention <key>=<value>` refuses the change and names `config.toml` so the file on disk never silently overrides what you just ran.

<CodeGroup>
```sh title="CLI (settings.json)"
# Show current settings and their sources
devvm retention

# Enable automatic pruning for exports and idle current tool images
devvm retention history.age=90d history.count=10 tools.unused-age=30d

# Disable a specific retention setting
devvm retention history.age=off

# Inspect settings and sources as JSON
devvm retention --json
```

```toml title="~/.config/devvm/config.toml"
[retention]
logs.runs = 20
history.age = "90d"
history.count = 10
tools.unused-age = "30d"
orphans.age = "30d"
nix-cache.size = "20G"
```
</CodeGroup>

### Daily automatic execution (`autoRetain`)

At the start of normal CLI commands, `autoRetain` configures `KeepRunLogs(r.RunLogs)` and checks the modification time of `~/.local/share/devvm/retention.run`:

1. **Schedule gate**: Retention runs only if at least one setting is enabled and `retention.run` is missing, at least 24 hours old, or has a future timestamp (indicating the system clock moved backward). Commands that inspect or manage retention directly (`devvm gc`, `devvm space`, `devvm retention`, and internal plumbing commands that skip layout checks) skip `autoRetain`.
2. **Non-blocking lock**: `autoRetain` calls `platform.TryLockFile` on `~/.local/share/devvm/retention.lock`. If another `devvm` process holds the lock, `autoRetain` returns immediately without waiting.
3. **Non-blocking tool share check**: If `b.Tools.ShareBusy()` is true (an image or tool build holds `tools.lock`), `autoRetain` sets `r.ToolsAge = 0` for that run so tool pruning defers to the next day rather than blocking the command. Furthermore, if a VM is actively being created (`making.lock` is held), `retainTools` leaves all current tool images and their idle timestamps untouched.
4. **Backend-free execution**: `autoRetain` invokes `Housekeeping.Retain(ctx, b, r, false)`, passing `orphans = false` so ordinary commands never wait on backend VM listings. To apply `orphans.age`, run `devvm gc --retention`.

---

## Run garbage collection (`devvm gc`)

`devvm gc` plans deletions across all reachable backends and host state directories, prints the plan, prompts for confirmation in an interactive terminal, and applies the removals in deterministic order.

```mermaid
flowchart TD
    subgraph CLI["internal/cli (gc.go & gc_caches.go)"]
        GCCmd["devvm gc [flags]"]
        GCRet["gcRetention (--retention)"]
        GCCache["gcCaches (--caches)"]
    end

    subgraph Locks["Host File Locks (~/.local/share/devvm)"]
        ShareLock["tools.lock (LockShare)"]
        MakingLock["making.lock (Making.Exclude)"]
        RetLock["retention.lock (lockRetention)"]
        NixLock["cache/nix-cache/.lock (lockNix)"]
    end

    subgraph Housekeep["internal/vm (Housekeeping)"]
        Plan["PlanGC(ctx, b, GCOpts)"]
        Apply["GCPlan.Apply(ctx)"]
        Retain["Housekeeping.Retain(ctx, b, r, true)"]
    end

    subgraph Targets["Pruned Host & Backend Targets"]
        Orphans["vm.Leftovers: vms/<hash>, dead links, sockets"]
        Images["build.Images: stale & unfinished backend images"]
        Mise["tools.Versions: stale cache/mise/<ver>"]
        Pkgs["build.Images: stale packages/*.deb & sets"]
        Exports["vm.Exports: exports/<vm>-<stamp>.tar.gz"]
        Tools["tools.Share: unused & --tool-images .erofs"]
        Nix["proxy.TrimNixCache: LRU narinfo & nar/*"]
        LimaCache["backend.DeleteCache: Lima cloud image cache"]
    end

    GCCmd --> ShareLock
    ShareLock -. "--tool-images" .-> MakingLock
    GCCmd --> Plan
    Plan --> Apply
    Apply --> Orphans
    Apply --> Images
    Apply --> Mise
    Apply --> Pkgs
    Apply --> Exports
    Apply --> Tools
    Apply --> NixLock --> Nix
    GCCmd -. "--retention" .-> RetLock --> GCRet --> Retain
    GCCmd -. "--caches" .-> GCCache --> LimaCache
```

### Default sweep vs. opt-in flags

By default, running `devvm gc` with no flags plans and deletes:
1. **Stale and unfinished images** (`build.StateStale` or `build.StateUnfinished`) on every reachable backend, skipping any image currently being built by another `devvm` process (`build.StateBuilding`). On Linux hosts, where VM disks use QCOW2 overlays backed by the image disk, the backend refuses deletion if a VM still references the stale image, and `devvm gc` warns and continues.
2. **Unused shared tool images** (`tools.StateUnused`): older tool builds in `tools/` that are neither the current version in `mise.lock` nor listed in any VM's `vm.json`.
3. **Stale host `mise` binaries**: older versions under `cache/mise/<version>` left behind by previous `devvm` releases, excluding directories modified within the last hour.
4. **Stale base `.deb` package caches**: package set lists in `packages/sets/` for base keys that are neither current nor built, plus `.deb` files in `packages/` not referenced by any remaining set.
5. **Shared Nix cache over cap**: least-recently-used files in `cache/nix-cache/` exceeding `nix-cache.size` (or `10 GiB` when `nix-cache.size` is `off`).

### Flags and parameters

<ParamField body="--tool-images [TOOL...]" type="boolean">
  Also delete `current` shared tool images in `tools/` that no VM lists in `vm.json` (across all C libraries, `gnu` and `musl`). Pass specific tool or agent names (such as `devvm gc --tool-images node claude`) to restrict deletion to those tools. If another `devvm` process holds `making.lock` (creating a VM or updating a VM's tools), `gc` keeps all current unattached tool images, reports them, and cleans the rest.
</ParamField>

<ParamField body="--history" type="boolean">
  Also delete saved agent history exports (`exports/<vm>-<YYYYMMDD>-<HHMMSS>.tar.gz` and their `.folder` sidecars). In-progress export files (`.devvm-export-*`) and non-matching files are ignored.
</ParamField>

<ParamField body="--older-than" type="DURATION">
  Requires `--history`. Restricts export deletion to archives whose filename timestamp (parsed in local wall-clock time) is at least `DURATION` old (for example, `30d` or `12h`).
</ParamField>

<ParamField body="--nix-cache" type="boolean">
  Evict the entire shared Nix cache (`Limit = 0`) instead of only files exceeding the `nix-cache.size` cap. Active in-flight downloads (`.tmp-*` modified within the last hour) are preserved.
</ParamField>

<ParamField body="--orphans" type="boolean">
  Forget state for VMs that no longer exist in their backend (such as VMs deleted directly via `limactl delete`), remove dead inter-VM links, and delete leftover link sockets in `run/` that no process listens on.
</ParamField>

<ParamField body="--caches" type="boolean">
  After the main GC pass, inspect each reachable backend's own download cache (`be.DevvmCache`) and prompt separately to delete entries created by `devvm` builds that no image record uses and that have not changed in the last hour. Never runs `limactl prune`.
</ParamField>

<ParamField body="--retention" type="boolean">
  After the main GC pass, immediately run `Housekeeping.Retain` with `orphans = true`, applying all configured `devvm retention` policies including `orphans.age`.
</ParamField>

<ParamField body="--force" type="boolean">
  Skip interactive confirmation prompts for the main GC plan and `--caches`. Required when running `devvm gc` without a terminal or with `--no-input`.
</ParamField>

### Orphaned VM safety gates (`vm.Leftovers`)

When `devvm gc --orphans` or `devvm gc --retention` (with `orphans.age`) evaluates a VM state folder `vms/<hash>/`, `Leftovers.FindIn` only classifies the VM as an `Orphan` if every check below passes:

1. **Readable state**: `vm.json` exists and parses cleanly.
2. **Backend usable**: The backend named in `vm.json` was queried in the inventory, passed `backend.Check` within 30 seconds (`leftoverAsk`), and did not skip image listing.
3. **No remake in progress**: `remakePending(st)` is false (a VM mid-remake may temporarily lack a backend instance until the next `devvm` command finishes it).
4. **Confirmed missing**: Calling `b.Inspect(ctx, vmName)` explicitly returns `backend.ErrNotFound`.
5. **Matching backend home (`madeHere`)**: `st.BackendAt` matches the backend's active location in this run (or the backend is using its default `~/.local/share/devvm/lima` home when `st.BackendAt` is empty). If `DEVVM_LIMA_HOME` points elsewhere, `devvm` lists the VM under `Elsewhere` and leaves its state untouched.
6. **Minimum age (`leftoverRecent`)**: `vm.json`'s modification time is at least **1 hour** old, preventing a newly initializing VM from being mistaken for an orphan before its backend instance is created.
7. **Creation lock on teardown**: `Leftovers.Forget` acquires the per-VM creation lock (`holdCreate`), re-reads `vm.json`, and calls `b.Inspect` one final time before running `teardown` (which removes `vms/<hash>/`, SSH config entries, host keys, inter-VM links, and any `--clone` git remote).

### Shared Nix cache eviction (`internal/proxy/nixcache.go`)

VMs built with the `nix` layer use `http://devvm.proxy/nix/` as a binary substituter (priority 30, ahead of `cache.nixos.org` at priority 40). The host proxy verifies Ed25519 signatures on `.narinfo` files against the pinned `cache.nixos.org-1` public key and verifies SHA-256 `FileHash` and `FileSize` on `.nar` archives before storing them in `~/.local/share/devvm/cache/nix-cache/`.

- **Runtime trimming**: Before writing a new file of `room` bytes (rounded up to 4 KiB disk blocks), `nixCache.reserve` acquires an exclusive `flock` on `cache/nix-cache/.lock` and checks `.size`. If `total + room > max`, `trim` sorts cached files by `mtime` (updated via `touch` on every cache hit) and deletes the least recently used files until usage drops to **90% of `max`** (`max/10*9 - room`).
- **In-flight protection**: Active NAR downloads write to pre-truncated `.tmp-*` files. `trim` counts `.tmp-*` files toward total disk usage and never deletes them unless their `mtime` is over 1 hour old (`nixTmpAge`, indicating a dead proxy process).

---

## Upgrade `devvm` (`devvm upgrade`)

`devvm upgrade` updates the `devvm` binary using the mechanism that originally installed it. Because all VMs, images, and shared tools live in `~/.local/share/devvm` and user configuration lives in `~/.config/devvm`, upgrading or reinstalling `devvm` leaves existing VMs and settings intact.

```sh
devvm upgrade                 # Upgrade to the newest stable release (prompts first)
devvm upgrade --check         # Check if a newer release is available without installing
devvm upgrade --json          # Output current/latest version and install kind as JSON
devvm upgrade --force         # Upgrade without prompting (required in scripts)
devvm upgrade --to 0.1.0      # Install a specific release (including older versions)
devvm upgrade --to 0.2.0-rc.1 # Install a prerelease (install.sh installations only)
```

### Installation detection (`upgrade.Detect`)

`upgrade.Detect` resolves symlinks on `os.Executable()` and classifies the running binary without executing external commands:

| `Install.Kind` | Detection rule | Upgrade mechanism | Prerelease channel |
| --- | --- | --- | --- |
| `install.sh` (`Script`) | Binary resolves to `<prefix>/lib/devvm/<version>/devvm` and `<prefix>/bin/devvm` is a symlink pointing to it (`~/.local` by default, or `DEVVM_PREFIX`). | Downloads release from GitHub, verifies Minisign signature and SHA-256 checksum, stages, smoke-tests, and atomically swaps symlink. | Stable `/releases/latest` only; use `--to <version>` for prereleases. |
| `homebrew` (`Homebrew`) | Resolved binary path contains `/Cellar/devvm/` or `/Cellar/devvm-rc/`. | Runs `brew upgrade devvm` or `brew upgrade devvm-rc`. Rejects `--to`. | `devvm-rc` reads `releases.atom` to follow prereleases and releases. |
| `other` (`Other`) | Any other path, `go install` binary (`FromSource`), or non-release version string (`(devel)`). | Refuses automatic upgrade and prints instructions for how the binary was built or installed. | N/A |

### Atomic script upgrade and rollback flow

When `Install.Kind` is `install.sh`, `Upgrader.script` performs a multi-stage verification and atomic swap with automatic rollback on failure:

```mermaid
sequenceDiagram
    autonumber
    participant CLI as upgrade.Upgrader
    participant GH as GitHub Releases (HTTPS)
    participant FS as <prefix>/lib/devvm & bin/devvm
    participant Proc as Process Table (ps -A)

    CLI->>GH: GET SHA256SUMS (64 KiB cap) & SHA256SUMS.minisig (4 KiB cap)
    CLI->>CLI: Verify Minisign ED (BLAKE2b-512) signature & trusted comment "devvm <version>"
    CLI->>GH: GET devvm-<ver>-<os>-<arch>.tar.gz (256 MiB cap) & verify SHA-256
    CLI->>FS: Unpack flat regular files into .<ver>.new.<pid> (512 MiB cap)
    CLI->>FS: Exec .<ver>.new.<pid>/devvm --version (30s timeout)
    alt If <prefix>/lib/devvm/<ver> already exists
        CLI->>FS: Rename <ver> -> .<ver>.old.<pid>
    end
    CLI->>FS: Rename .<ver>.new.<pid> -> <ver>
    CLI->>FS: Symlink .devvm.new.<pid> -> <ver>/devvm & rename over <prefix>/bin/devvm
    CLI->>FS: Exec <prefix>/bin/devvm --version (30s timeout)
    alt Final link execution fails
        CLI->>FS: Restore <prefix>/bin/devvm -> previous Exe & restore .<ver>.old.<pid>
    else Final link execution succeeds
        CLI->>FS: Remove .<ver>.old.<pid>
        CLI->>Proc: Inspect running commands via ps -A -ww -o command=
        CLI->>FS: Prune unused older <version> dirs & dead .<ver>.new/.old.<pid> dirs
    end
```

Key security and reliability invariants in `internal/upgrade/`:
- **Minisign `ED` verification (`minisign.go`)**: `PublicKey.Verify` requires a prehashed `ED` signature over the BLAKE2b-512 digest of `SHA256SUMS` using the public key embedded from `packaging/minisign.pub` (`packaging.MinisignKey()`), rejects legacy non-prehashed `Ed` signatures, verifies the global signature over the trusted comment, and requires the trusted comment to equal `"devvm " + version` so a signature from another release cannot be replayed.
- **Strict archive unpacking (`unpack.go`)**: `unpack` enforces a 512 MiB uncompressed cap (`unpackCap`) and accepts only regular files directly inside the single top-level directory `devvm-<version>-<os>-<arch>/`. Any symlink, subdirectory, or path traversal entry aborts unpacking immediately.
- **Downgrade refusal**: If the latest release reported by the server is older than `u.Current` (`Compare(target, u.Current) < 0`) and `--to` was not explicitly passed, `Upgrade` refuses to install it—even with `--force`—unless `u.Current` is a prerelease of an upcoming version that is not yet out.
- **In-use binary preservation (`prune`)**: When cleaning old version directories under `<prefix>/lib/devvm/`, `Upgrader.prune` inspects `ps -A -ww -o command=`. Any version directory referenced by a running process (such as a running VM's `devvm _proxy`, including paths with doubled commas `,,` passed to QEMU `guestfwd`) is kept until a subsequent upgrade runs after that VM stops.

### Background daily release check (`releaseNotice`)

When you launch a VM interactively in a terminal (`a.u.CanPick()`), `releaseNotice` checks `~/.local/share/devvm/cache/release.json`:
1. If the cached record (`upgrade.Known`) names a release newer than `Current`, `devvm` prints a one-line upgrade notice before opening the VM.
2. If `Known.Due(now)` is true (no check attempted in the last 24 hours), `devvm` spawns a background goroutine calling `Source.Refresh` with a 5-second timeout (`noticeTimeout`), and waits at most 1 second (`releaseWait`) after the command finishes for the check to complete.
3. Disable the check by setting `release-check = false` in `~/.config/devvm/config.toml` or `DEVVM_NO_RELEASE_CHECK=1` in the environment.

### Uninstall `devvm` completely

<Steps>
  <Step title="Remove all project VMs">
    List all VMs with `devvm ls` and run `devvm rm` in each project folder (or `devvm rm <vm-name>`). For `--clone` VMs, this also cleans up the `devvm` git remote in the host repository.
  </Step>
  <Step title="Delete images and Lima cache entries">
    Run `devvm gc` to remove stale images, then `devvm images rm <name|key>` for each remaining image in `devvm images`. Run `devvm gc --caches` to remove `devvm`'s Ubuntu cloud image downloads from Lima's cache (or `limactl prune` if you do not use Lima outside `devvm`).
  </Step>
  <Step title="Remove saved secrets">
    Run `devvm secret rm <name>` for each secret listed by `devvm secret` to clear macOS Keychain items under the `devvm` service or Linux secret files under `~/.local/share/devvm/secrets/`.
  </Step>
  <Step title="Remove the devvm binary and state directories">
    <Tabs>
      <Tab title="macOS (Homebrew)">
        ```sh
        brew uninstall devvm
        # Or if using the prerelease formula:
        brew uninstall devvm-rc
        rm -rf ~/.local/share/devvm ~/.config/devvm
        ```
      </Tab>
      <Tab title="Linux / macOS (install.sh)">
        ```sh
        rm -rf ~/.local/lib/devvm ~/.local/bin/devvm
        rm -rf ~/.local/share/devvm ~/.config/devvm
        ```
      </Tab>
    </Tabs>
    Finally, if you allowed `devvm` to configure SSH, remove the `Include ~/.local/share/devvm/ssh_config` line from `~/.ssh/config`.
  </Step>
</Steps>

---

## Troubleshooting retention, cleanup, and upgrades

<AccordionGroup>
  <Accordion title="devvm gc --tool-images keeps current unused tools">
    If another `devvm` process is creating a VM or running `devvm tools add` / `devvm tools update`, it holds `~/.local/share/devvm/making.lock`. Because the in-progress VM may have selected shared tools that are not yet written to its `vm.json`, `LockGC` sets `Making = true` and preserves all current unattached tool images. Wait for the VM creation or tool update to finish, then re-run `devvm gc --tool-images`.
  </Accordion>

  <Accordion title="devvm gc --orphans skips a deleted VM's state">
    `Leftovers.FindIn` leaves a VM's state directory untouched and prints a note when:
    - **Its state changed in the last hour**: `vm.json` must be at least 1 hour old (`leftoverRecent`) before `devvm` considers it an orphan.
    - **The VM was created under a different Lima home**: `vm.json` records `BackendAt`. Ensure `DEVVM_LIMA_HOME` matches the location where the VM was created.
    - **The backend is unreachable**: Run `devvm doctor` to diagnose Lima issues.
  </Accordion>

  <Accordion title="devvm retention refuses to change a setting">
    If a retention key is defined under `[retention]` in `~/.config/devvm/config.toml`, `devvm retention KEY=VALUE` refuses to write to `settings.json` because `config.toml` takes precedence. Edit or remove the key in `~/.config/devvm/config.toml` directly.
  </Accordion>

  <Accordion title="devvm upgrade refuses with 'cannot upgrade' or rejects --to">
    - **Homebrew installations and `--to`**: Homebrew formulas only install the version pinned in the tap formula. To install a specific historical version or prerelease via `--to`, use an `install.sh` installation, or switch formulas between `devvm` and `devvm-rc`.
    - **Source or `go install` builds**: Binaries outside `<prefix>/lib/devvm/<version>/devvm` and Homebrew's Cellar (`Install.Kind == Other`), or binaries built without the embedded `packaging/minisign.pub` key, cannot be upgraded by `devvm upgrade`. Re-run `install.sh` or `go install github.com/devvmhq/devvm/cmd/devvm@latest`.
  </Accordion>
</AccordionGroup>

## Related pages

<CardGroup cols={2}>
  <Card title="CLI reference: images, storage, and host commands" href="/cli-image-space-and-host-commands">
    Complete flag, argument, and exit code reference for `devvm space`, `devvm gc`, `devvm retention`, `devvm images`, and `devvm upgrade`.
  </Card>
  <Card title="Configuration (config.toml) and --json schema reference" href="/configuration-and-json-reference">
    Full specification of `~/.config/devvm/config.toml` tables, `settings.json` precedence, state directory layout, and version 1 JSON payloads.
  </Card>
  <Card title="Images, layers, and content keys" href="/image-and-layer-model">
    How base images, custom layers, and read-only EROFS shared tool images are keyed, cached, and mounted into VMs.
  </Card>
  <Card title="Troubleshoot network policies, CAs, and disk space" href="/network-ca-and-storage-troubleshooting">
    Diagnose host disk pressure, inspect proxy network logs, and recover from interrupted image builds or VM remakes.
  </Card>
</CardGroup>
