# Troubleshoot network policies, CAs, and disk space

> Inspect blocked requests with devvm network log, configure corporate HTTP/HTTPS proxies and custom CAs in ~/.config/devvm/ca, and recover disk space.

- 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/network.go`
- `internal/netpolicy/log.go`
- `internal/trust/trust.go`
- `internal/settings/companyproxy.go`
- `internal/vm/housekeep.go`
- `internal/cli/gc.go`

---

---
title: "Troubleshoot network policies, CAs, and disk space"
description: "Inspect blocked requests with devvm network log, configure corporate HTTP/HTTPS proxies and custom CAs in ~/.config/devvm/ca, and recover disk space."
---

`devvm` confines VM network traffic through a per-VM host proxy (`devvm _proxy`), validates custom and corporate root certificates from `~/.config/devvm/ca`, and enforces pre-flight disk space floors before building images or creating VMs. When requests fail, TLS handshakes are rejected, or disk checks block a command, use `devvm network log`, `devvm doctor`, `devvm space`, and `devvm gc` to diagnose the failure and restore operation.

```mermaid
flowchart LR
  subgraph Guest["Guest VM"]
    App["Process / Agent"]
    Why["devvm-why / Prompt Hook"]
    LocalProxy["devvm-local-proxy<br/>(127.0.0.1 socat)"]
    GuestTrust["/etc/ssl/certs/ca-certificates.crt<br/>/etc/ssl/devvm.pem"]
  end

  subgraph Host["Host Machine"]
    Proxy["devvm _proxy"]
    NetLog["vms/&lt;hash&gt;/logs/network.jsonl<br/>(+ .1 rotation)"]
    PolicyFiles["Built-in policies<br/>~/.config/devvm/policy<br/>vms/&lt;hash&gt;/policy"]
    CADir["~/.config/devvm/ca/*.pem, *.crt"]
    VMCA["vms/&lt;hash&gt;/run/ca/<br/>(ca.pem, next.pem)"]
    Config["HTTPS_PROXY / HTTP_PROXY<br/>or config.toml [proxy]<br/>+ secret 'proxy'"]
  end

  subgraph External["Upstream Network"]
    CorpProxy["Corporate HTTP Proxy"]
    Target["Target Host"]
  end

  App -->|"HTTP_PROXY / HTTPS_PROXY"| Proxy
  App -.->|"GUI / Browsers"| LocalProxy --> Proxy
  Why -->|"http://devvm.proxy/blocked"| Proxy
  PolicyFiles --> Proxy
  CADir -->|"Sync on launch"| GuestTrust
  VMCA -->|"Sync on launch"| GuestTrust
  CADir -->|"UpstreamCAs"| Proxy
  VMCA -->|"TLS interception"| Proxy
  Proxy -->|"Append events"| NetLog
  Config --> Proxy
  Proxy -->|"CONNECT / HTTP"| CorpProxy --> Target
  Proxy -->|"Direct (if no upstream / NO_PROXY)"| Target
```

## Troubleshoot blocked requests and network policies

On the `allowlist`, `internet`, and `none` tiers, a VM cannot resolve DNS names or open outbound connections directly. All outbound traffic must pass through the VM's host proxy (`devvm _proxy`), which logs every decision to `~/.local/share/devvm/vms/<hash>/logs/network.jsonl` (rotating to `network.jsonl.1` at 10 MiB, keeping up to ~20 MiB per VM).

### Inspect blocked requests

You can inspect blocked connections from either the host or inside the VM:

<Tabs>
  <Tab title="On the host">
    Run `devvm network log --blocked` in the project folder to aggregate all blocked requests across `network.jsonl` and `network.jsonl.1` by host and port, ordered by the most recent block:

    ```sh
    devvm network log --blocked
    ```

    ```text
    LAST                  COUNT  HOST              ALLOW WITH
    2026-10-09T10:01:00Z  2      example.com:443   devvm network allow example.com
    2026-10-09T10:02:15Z  1      10.0.0.5:5432     devvm network allow 10.0.0.5:5432
    2026-10-09T10:03:00Z  1      api.github.com:443  -
    ```

    To stream live connection decisions (allowed tunnels, decrypted secret injections, Nix cache hits, VM link connections, and blocks) every 250 ms as they occur:

    ```sh
    devvm network log --follow
    ```
  </Tab>
  <Tab title="Inside the VM">
    In an interactive bash shell inside the VM, a `PROMPT_COMMAND` hook (`~/.bashrc.d/devvm-blocked.sh`) queries `http://devvm.proxy/blocked/new?after=N` before each prompt and prints newly blocked hosts and the host command to allow them to standard error.

    To view the last 50 blocked requests at any time from inside the VM (including from coding agents):

    ```sh
    devvm-why
    ```

    `devvm-why` executes `curl -sS --proxy "$HTTP_PROXY" http://devvm.proxy/blocked`.
  </Tab>
  <Tab title="JSON output">
    Pass `--json` to inspect full event metadata (`method`, `path`, `rule`, `reason`, `agent`, `secrets`, `refused`, `upstream`, `target`) or aggregated block records:

    ```sh
    devvm network log --lines 20 --json
    devvm network log --blocked --json
    ```
  </Tab>
</Tabs>

<Note>
`--blocked` cannot be combined with `--follow` or `--lines` (`-n`), and `--json` cannot be combined with `--follow`. Passing incompatible flags exits with code `2`.
</Note>

### Understand why `ALLOW WITH` shows `-`

In `devvm network log --blocked`, the `ALLOW WITH` column prints a paste-ready `devvm network allow <rule>` command whenever an allowlist rule can permit the request (and includes `"allow": "<rule>"` in `--blocked --json`). It prints `-` (and omits `allow` in JSON) in two cases:

1. **Unconditional proxy refusals (`"refused": true`):** The request matched a security boundary that `devvm _proxy` refuses regardless of allowlist rules, such as GitHub endpoints that mint new credentials or requests blocked while a policy file contains a broken deny rule.
2. **Invalid policy target:** The requested host string is not a valid hostname or IP address that `proxy.AllowRule` can format into a policy rule.

### Allow or deny hosts immediately

Add or remove rules for the current project's VM (`~/.local/share/devvm/vms/<hash>/policy`) or globally for all VMs (`~/.config/devvm/policy` with `-g` / `--global`):

```sh
# Allow a host on ports 80 and 443 for this folder's VM
devvm network allow example.com

# Allow any subdomain of example.com
devvm network allow '*.example.com'

# Allow a specific port or internal IP/CIDR
devvm network allow 10.0.0.5:5432

# Allow a host for every VM
devvm network allow -g pypi.org

# Deny a host (denies override any allow rule)
devvm network deny tracker.example.com

# Remove an allow or deny rule
devvm network rm example.com
devvm network rm '!tracker.example.com'
```

`devvm _proxy` watches policy files and reloads rules on the next connection without restarting the VM.

<Warning>
If you run `devvm network allow` while the VM is on the `internet` or `none` tier, `devvm` saves the rule to the policy file but prints a note that only `!RULE` denies apply on `internet` and `none`. Switch the VM to `allowlist` (`devvm network allowlist`) for allow rules to take effect.
</Warning>

### Fix malformed policy lines and broken denies

If you edit `~/.config/devvm/policy`, `~/.config/devvm/layers/<name>.policy`, or `~/.local/share/devvm/vms/<hash>/policy` by hand, `devvm` validates every non-comment line:

| Symptom | Cause | Effect | Resolution |
| --- | --- | --- | --- |
| `devvm network` prints `# not a rule, skipped (line N)` | A line contains invalid syntax (e.g., `example.com:x`, `a*.example.com`, or spaces in a host rule). | The proxy skips that line and applies the remaining valid rules. `devvm doctor` and `devvm network` emit a warning. | Edit the file named in the header (`# /path/to/policy`) and fix or delete line `N`. |
| Every outbound request is blocked and `devvm doctor` warns of a broken deny | A line starts with `!` but is not a valid rule (e.g., `! example.com` with a space after `!`). | Because the proxy cannot determine what the rule intended to deny, it fails closed and **denies all new outbound connections** (except VM links) for every VM reading that file. | Fix the syntax (remove the space after `!`) or remove the line with `devvm network rm` or an editor. |

### Resolve DNS errors when tools bypass the proxy

If a command inside a confined VM fails with `no such host`, `Temporary failure in name resolution`, `ENOTFOUND`, or `EAI_AGAIN` for a host that is already on the allowlist—and `devvm network log` and `devvm-why` show no blocked event—the tool attempted to resolve DNS or connect directly instead of using `HTTP_PROXY` / `HTTPS_PROXY`.

Configure the tool to use the proxy environment variables set in the VM:

| Tool / Runtime | Why it bypasses the proxy | How to fix inside the VM |
| --- | --- | --- |
| **Node.js `fetch` (older releases)** | Built-in `fetch` honors `NODE_USE_ENV_PROXY=1` (which `devvm` sets automatically) only in Node `>=24.0.0` and `>=22.21.0`. | Upgrade the project's Node version to `>=22.21.0` or `>=24.0.0`, or configure an explicit `undici.ProxyAgent` using `process.env.HTTPS_PROXY`. |
| **Python `aiohttp`** | `aiohttp.ClientSession` ignores environment proxies by default. | Pass `trust_env=True` when constructing `aiohttp.ClientSession(trust_env=True)`. |
| **Go custom `http.Transport`** | Custom `http.Transport{}` structs omit `Proxy` unless set. | Set `Proxy: http.ProxyFromEnvironment` on the custom `http.Transport`. |
| **Java / JVM tools** | The JVM ignores `HTTP_PROXY` and `HTTPS_PROXY`. | Pass `-Dhttps.proxyHost=<host> -Dhttps.proxyPort=<port>` (parsed from `$HTTPS_PROXY`). |
| **Hugo (`resources.GetRemote`)** | Hugo disables environment proxies unless enabled in site configuration. | Set `security.http.proxyFromEnvironment = true` in `hugo.toml` / `config.toml`. |
| **Desktop browsers / GUI apps** | GUI browsers prompt interactively for proxy credentials instead of reading `http://devvm:KEY@host:port` from `$HTTP_PROXY`. | Run `devvm-local-proxy start` in the VM and point the app at `$(devvm-local-proxy url)`. `devvm-local-proxy` runs a loopback `socat` listener that injects the VM's `Proxy-Authorization` header automatically. |

### Fix `unenforced` network status

`devvm ls` and `devvm network` report a VM's policy as `unenforced` when the VM is configured for a confined tier (`allowlist`, `internet`, or `none`) but is currently running without network confinement or failed `Proxy.SelfCheck`:

- **VM started outside `devvm` or switched from `open` while running:** Run `devvm` in the project folder and confirm the prompt to restart the VM confined, or run `devvm restart` directly. In non-interactive mode or with `--no-input`, `devvm` stops the unconfined VM so the next `devvm` launch starts it confined.
- **Direct route detected while confined (`NetDirectRoute`):** If `Proxy.SelfCheck` detects that `curl --noproxy '*' -sS -m 3 -o /dev/null https://1.1.1.1` succeeds from inside a VM that the backend started confined, `devvm` stops the VM immediately and errors because an external network interface is routing around the proxy.
- **Host proxy failed to start or port conflict:** For confined VMs, if `devvm _proxy` cannot start, `devvm` stops the VM (`NetProxyDownStopped`) so it never runs unproxied. Run `devvm restart` to allocate a fresh host port and restart the proxy. For `open` VMs, if `http://devvm.proxy/blocked` does not answer during the startup check, `devvm` warns (`NetProxyUnreachable`) and leaves the VM running; run `devvm restart` so tools that use the proxy (`apt`, `docker`, `devvm-why`) work.

---

## Configure corporate HTTP/HTTPS proxies

When the host machine accesses the internet through a corporate proxy, `devvm _proxy`, image/tool builds, host `mise`, and OAuth token refreshes route allowed outbound requests through that upstream proxy.

### Proxy configuration precedence

`settings.HostProxy` resolves the corporate proxy using all-or-nothing precedence between the shell environment and `~/.config/devvm/config.toml`:

1. **Shell environment (`HTTPS_PROXY`, `HTTP_PROXY`, `NO_PROXY` or lowercase):** If the shell where you run `devvm` sets either `HTTPS_PROXY` (`https_proxy`) or `HTTP_PROXY` (`http_proxy`), `devvm` uses `HTTPS_PROXY`, `HTTP_PROXY`, and `NO_PROXY` from the environment and ignores `config.toml`'s `[proxy]` table completely.
2. **`~/.config/devvm/config.toml` `[proxy]` table:** When neither `HTTPS_PROXY` nor `HTTP_PROXY` is set in the environment (for example, inside an IDE terminal that does not source your shell profile), `devvm` reads `[proxy]` from `config.toml`.

<Steps>
  <Step title="Configure [proxy] in ~/.config/devvm/config.toml">
    Add the `[proxy]` table to `~/.config/devvm/config.toml`. Use an `http://` URL (or `host:port`, which defaults to `http://` and port `80`). If the proxy requires authentication, include **only the username** in the URL—never the password:

    ```toml title="~/.config/devvm/config.toml"
    [proxy]
    https = "http://alice@proxy.example.com:8080"
    http = "http://alice@proxy.example.com:8080"
    no-proxy = "localhost,.example.com,10.0.0.0/8"
    ```
  </Step>
  <Step title="Save the proxy password in devvm secrets">
    When `proxy.https` or `proxy.http` in `config.toml` includes a username, save the password in the `proxy` secret (stored in the macOS Keychain under service `devvm` or `~/.local/share/devvm/secrets/proxy` with mode `0600` on Linux):

    ```sh
    devvm secret set proxy
    # Or from standard input:
    printf '%s\n' "$CORP_PROXY_PASS" | devvm secret set proxy --stdin
    ```
  </Step>
  <Step title="Restart running VM proxies">
    Re-run `devvm` or `devvm restart` in each running VM's project folder so its `devvm _proxy` restarts with the upstream proxy configuration and credentials:

    ```sh
    devvm restart
    ```
  </Step>
</Steps>

### Corporate proxy error cases and constraints

- **Password in `config.toml` rejected:** `checkedProxyURL` refuses any `proxy.https` or `proxy.http` value in `config.toml` that contains a `user:password@...` password. Remove `:password` from `config.toml` and run `devvm secret set proxy`.
- **Missing `proxy` secret (`proxy_password_missing`):** If `config.toml` names a username in `proxy.https` or `proxy.http` and the `proxy` secret has not been saved, `devvm` refuses to create VMs, start VM proxies, or run builds before any process starts.
- **Unsupported proxy scheme (`https://` or `socks5://`):** `proxy.ParseUpstream` supports only `http://` upstream proxies (`ErrUpstreamScheme`). If `HTTPS_PROXY`, `HTTP_PROXY`, or `config.toml` specifies `https://` or `socks5://`, replace it with the proxy's `http://` endpoint.
- **Unresolved hostnames on the `internet` tier or under `**`:** When routing through an upstream proxy, `devvm _proxy` first attempts to resolve the requested hostname on the host (with a 3-second timeout) to verify that it does not resolve to an internal or denied IP address (`floorUpstream`). If the host machine cannot resolve external DNS names (because only the corporate proxy resolves them), `devvm _proxy` forwards the unresolved name to the corporate proxy **only when the VM's policy explicitly matches the hostname** (`example.com` or `*.example.com`). If a host is allowed only by `**` (including on the `internet` tier), `devvm _proxy` blocks it with:
  ```text
  this machine can't resolve it, and only ** allows it; allow the name itself to reach it through the company proxy
  ```
  Run `devvm network allow <host>` (or `devvm network allow '*.domain.com'`) to allow the name explicitly.
- **Upstream proxy disappeared between launches:** If a VM's proxy previously used an upstream proxy (from shell environment variables) and you later run `devvm` from a shell without `HTTPS_PROXY`/`HTTP_PROXY` and without `[proxy]` in `config.toml`, `devvm` warns and starts the proxy with direct connections. Set `[proxy]` in `~/.config/devvm/config.toml` so every shell uses the corporate proxy consistently.

---

## Configure custom CAs in `~/.config/devvm/ca`

When a corporate proxy inspects TLS traffic (such as Zscaler or Netskope) or when your organization runs internal HTTPS services signed by a private root CA, place the root CA certificates in `~/.config/devvm/ca`.

### Install custom CA certificates

<Steps>
  <Step title="Copy PEM-encoded CA certificates into ~/.config/devvm/ca">
    Create `~/.config/devvm/ca` and copy your root CA certificate files into it with a `.pem` or `.crt` extension:

    ```sh
    mkdir -p ~/.config/devvm/ca
    cp /path/to/corporate-root-ca.pem ~/.config/devvm/ca/corporate-root-ca.crt
    ```
  </Step>
  <Step title="Verify CA validation with devvm doctor">
    Run `devvm doctor` to verify that `trust.Read` parses the folder without warnings:

    ```sh
    devvm doctor
    ```

    If `HTTPS_PROXY` or `config.toml`'s `proxy.https` is set and `~/.config/devvm/ca` contains no certificates, `devvm doctor` notes that a TLS-inspecting proxy needs its CA in `~/.config/devvm/ca`.
  </Step>
  <Step title="Apply to existing VMs">
    Run `devvm` in each project folder. On every launch, `devvm` sends the parsed certificates to the VM and runs `ca.sh` to update the system trust store and `/etc/ssl/devvm.pem` without rebuilding any image.
  </Step>
</Steps>

### Strict validation rules for `~/.config/devvm/ca`

`trust.Read` validates `~/.config/devvm/ca` before every VM launch, image build, and shared tool resolution. If **any** file fails validation, `devvm` refuses the entire folder and names the offending path:

| Rule | Refusal condition | Why `devvm` enforces it |
| --- | --- | --- |
| **Regular files only (no symlinks)** | `~/.config/devvm/ca` itself is a symlink (`CAFolderLink`), or any `*.pem` / `*.crt` entry is a symlink (`CALink`). | A symlink pointing into a project folder would allow untrusted code inside that VM to replace the root CA trusted by all other VMs, builds, and host proxies. Always `cp` files into `~/.config/devvm/ca`. |
| **Extensions** | Only `*.pem` and `*.crt` (case-insensitive) are read; other filenames are ignored. | Prevents editor swap files or READMEs from breaking validation. |
| **PEM `CERTIFICATE` blocks only** | File contains a `PRIVATE KEY` block, non-certificate PEM block, or zero certificates (`CANotCertificate`), or corrupt ASN.1 DER (`CAInvalid`). | Ensures private keys are never copied into VMs or build scripts. `trust.Read` re-encodes only parsed `x509.Certificate.Raw` bytes. |
| **Must be a CA certificate** | Certificate lacks `BasicConstraintsValid` or `IsCA: true` (`CANotCA`). | Leaf server certificates cannot act as trust anchors in `update-ca-certificates` or X.509 cert pools. |

### How CAs are trusted across guests, builds, and the host

Inside every project VM, `ca.sh` installs each user CA as `/usr/local/share/ca-certificates/devvm-extra-N.crt` alongside the VM's own proxy CAs (`devvm-proxy.crt` and `devvm-proxy-next.crt`), runs `update-ca-certificates --fresh` to rebuild `/etc/ssl/certs/ca-certificates.crt` (and triggers `/etc/ca-certificates/update.d` hooks such as Java's `cacerts`), and writes all `devvm-*.crt` certificates to `/etc/ssl/devvm.pem`.

Every VM exports the following environment variables (`trust.Env`) so runtimes that do not read the OS trust store by default trust both the system bundle and `devvm`'s CAs:

| Environment variable | Value in guest | Target tools and runtimes |
| --- | --- | --- |
| `NODE_EXTRA_CA_CERTS` | `/etc/ssl/devvm.pem` | Node.js, npm, pnpm, Bun |
| `SSL_CERT_FILE` | `/etc/ssl/certs/ca-certificates.crt` | OpenSSL, Go `crypto/tls`, Rust `rustls-native-certs` |
| `REQUESTS_CA_BUNDLE` | `/etc/ssl/certs/ca-certificates.crt` | Python `requests` |
| `PIP_CERT` | `/etc/ssl/certs/ca-certificates.crt` | Python `pip` |
| `UV_SYSTEM_CERTS` | `1` | `uv` |

- **Image and tool builds:** During `devvm build`, `devvm rebuild`, and shared tool builds, `CAs.Build` installs `~/.config/devvm/ca` certificates into the temporary build VM and exports `trust.Env` at the start of each build script, then strips all `devvm-*.crt` certificates and `/etc/ssl/devvm.pem` before finalizing the image. Images never retain corporate CAs, and adding or removing a CA never invalidates image cache keys.
- **Host `mise` (`invalid peer certificate: UnknownIssuer`):** When `devvm` resolves shared tool versions on the host using its managed `mise` binary:
  - On **Linux**, `CAs.Host` combines the host's CA bundle with `~/.config/devvm/ca` into a temporary bundle and sets `SSL_CERT_FILE` (and `SSL_CERT_DIR` when neither `SSL_CERT_FILE` nor `SSL_CERT_DIR` is set in the host environment).
  - On **macOS**, `mise`'s Sigstore/GitHub build-provenance verifier queries the macOS Keychain directly. In addition to placing the certificate in `~/.config/devvm/ca`, import the corporate CA into **Keychain Access** on macOS and set its trust setting to **Always Trust**.

### Expired per-VM proxy CA (`devvm's CA has ended`)

To inject secrets (`claude`, `github`, and agent OAuth logins) into HTTPS requests, each VM's proxy uses a per-VM ECDSA P-256 CA stored in `~/.local/share/devvm/vms/<hash>/run/ca/` (`ca.pem`/`ca.key` and `next.pem`/`next.key`).

- Each CA is valid for 30 days (`caLife`), and `next.pem` becomes valid 7 days plus 1 hour before `ca.pem` expires (`caRenew + caSkew`).
- Every time you run `devvm` in the project folder, `trust.InitCA` rotates any CA with less than 7 days remaining and installs both `ca.pem` and `next.pem` into the VM.
- If a VM stays running for more than 30 days without `devvm` being run on the host (for example, when you connect exclusively via `ssh <vm>`), `next.pem` eventually expires. Requests to secret-bearing hosts then fail and `devvm network log --blocked` shows the reason `devvm's CA has ended`.
- **Fix:** Run `devvm` in the project folder to generate a new CA pair, install it into the VM, and restart the VM's proxy.

---

## Audit and recover disk space

Before building an image, building shared tools, or creating a VM, `backend.Space.Check` checks `statfs` available space on the target volumes and refuses the operation if free space falls below the step's floor:

| Operation | macOS (Lima) floor | Data directory (`~/.local/share/devvm`) floor |
| --- | --- | --- |
| Build `base` image | `8 GiB` | — |
| Build layer image or create VM | `2 GiB` | — |
| Build `N` shared tool images | `4 GiB` (1 tool) to `8 GiB` (10 tools) | `512 MiB + (N × 256 MiB)` |

### Inspect storage usage with `devvm space`

Run `devvm space` (or `devvm space --json`) to inspect every category of disk usage across `~/.local/share/devvm` and Lima without starting any VM:

```sh
devvm space
```

`devvm space` prints two tables and diagnostic notes:

1. **`devvm` storage table:** Lists `VMs`, `images`, `tools gnu` / `tools musl`, `history` (agent history exports in `exports/`), `network logs` (`vms/<hash>/logs/` and `logs/tools-check.log`), `run logs` (`logs/runs/*.log`), `mise` (`cache/mise/`), `packages` (`packages/*.deb`), `Nix cache` (`cache/nix-cache/`), `leftover state` (orphaned VM folders and dead link sockets), and `state`. For each row, `GC` shows how much `devvm gc` will free immediately, `FREED BY` shows the command that frees the remainder, and `RETAIN` shows any active retention policy and how much it would free now.
2. **Backend caches table:** Lists Lima's download cache (`~/Library/Caches/lima` on macOS, `~/.cache/lima` or `$XDG_CACHE_HOME/lima` on Linux), which stores downloaded Ubuntu cloud images (`600–900 MB` compressed plus `~2.4 GB` raw disk conversion on macOS).
3. **Diagnostic notes:** Lists `strays` (Lima VMs bearing `devvm`'s label that belong to a different `XDG_DATA_HOME`), `folder_gone` (VMs whose project folder was moved or deleted), and `folder_not_connected` (VMs whose project folder is on an unmounted external drive).

### Reclaim disk space with `devvm gc`

`devvm gc` never deletes a project VM. By default, an interactive `devvm gc` run lists what it plans to remove, asks for confirmation (`--force` skips the prompt), and deletes:

- **Stale and unfinished images** (`STATE` is `stale` or `unfinished` in `devvm images`).
- **Unused shared tool images** (`STATE` is `unused` in `devvm images`—older tool versions not referenced by any VM's `vm.json`).
- **Stale host `mise` binaries** in `~/.local/share/devvm/cache/mise/` left behind after `devvm` upgrades (skipping any modified within the last hour).
- **Stale `.deb` package sets and files** in `~/.local/share/devvm/packages/` no longer referenced by any current or built `base` image.
- **Shared Nix cache files over the cap** in `~/.local/share/devvm/cache/nix-cache/`, evicting least-recently-used files until the cache is within `nix-cache.size` (`10 GiB` when unset).

Use `devvm gc` flags to recover additional space:

| Command / Flag | What it deletes | Constraints and safety checks |
| --- | --- | --- |
| `devvm gc --tool-images [TOOL...]` | Current shared tool images (`tools/*.erofs`) that no VM uses (`VMS` is `0`), or only those matching `TOOL...` (`node`, `python`, `rust`, `go`, `dotnet`, `claude`, `codex`, `opencode`). | Acquires `making.lock` (`tools.Making`). If another `devvm` process is currently creating a VM or updating tools, `gc` keeps all current unused tool images and reports which ones it kept. |
| `devvm gc --nix-cache` | The entire shared Nix cache (`~/.local/share/devvm/cache/nix-cache/`), not just files over the 10 GiB cap. | VMs with the `nix` layer re-fetch needed narinfos/nars from `cache.nixos.org` on demand. |
| `devvm gc --history [--older-than DURATION]` | Saved agent history archives in `~/.local/share/devvm/exports/*.tar.gz` (and `.folder` files). With `--older-than` (e.g., `30d`, `2w`, `12h`), deletes only exports at least that old. | Exports are the `only copy` of deleted VMs' agent transcripts; `devvm gc` never touches them without `--history`. `--older-than` requires `--history`. |
| `devvm gc --orphans` | State folders (`vms/<hash>/`), SSH config entries, dead VM links, and unused link sockets (`run/`) for VMs deleted outside `devvm` (e.g., via `limactl delete`). | Skips VMs whose backend is unreachable, VMs created under a different `DEVVM_LIMA_HOME`, and VMs whose `vm.json` changed within the last hour. Once orphan state is removed, shared tools held only by that orphan become eligible for `devvm gc`. |
| `devvm gc --caches` | `devvm`'s unused Ubuntu cloud image downloads inside Lima's cache (`~/Library/Caches/lima` or `~/.cache/lima`). | Deletes only Ubuntu cloud image cache entries from `devvm`'s download URL that no `devvm` image record references and that have not changed in the last hour. Never runs `limactl prune`. |
| `devvm gc --retention` | Immediately runs all configured `[retention]` policies (`logs.runs`, `history.age`, `history.count`, `tools.unused-age`, `orphans.age`, `nix-cache.size`). | Unlike daily automatic retention at command startup, `devvm gc --retention` also queries backends to apply `orphans.age`. |
| `devvm gc --force` | Runs any of the above without interactive confirmation prompts. | Required when running `devvm gc` without a terminal or with `--no-input`. |

### Delete current images, old VMs, and stray instances

When `devvm gc` does not free enough disk space, inspect and remove current images or unneeded VMs:

<AccordionGroup>
  <Accordion title="Delete current base or layer images (devvm images rm)">
    `devvm gc` keeps images whose `STATE` is `current`. To list all images with their sizes and delete a current image by name or key:

    ```sh
    devvm images
    devvm images rm docker+desktop
    ```

    - On **macOS**, project VMs are APFS clones of the image disk, so deleting a current image succeeds even while VMs cloned from it exist (note that shared APFS blocks are only freed on disk once both the image and all VMs sharing those blocks are deleted).
    - On **Linux**, project VMs use QCOW2 overlays backed by the image's disk. `devvm images rm` and `devvm gc` refuse to delete an image while any VM's overlay depends on it, naming the VMs that use it.
  </Accordion>

  <Accordion title="Delete VMs whose project folder is gone (devvm rm VM)">
    When you delete or move a project directory on the host, `devvm` never auto-deletes the VM. `devvm ls` marks the folder `(gone)` and `devvm space` lists it under `folder_gone`.

    - If the folder was moved, run `devvm adopt <vm-name>` inside the new folder location.
    - To delete the VM and free its disk, pass the VM's name to `devvm rm` from any directory. If the VM is stopped and has a coding agent, pass `--no-export` so `devvm rm` does not start the VM (which would recreate the missing mount folder) to export history:

    ```sh
    devvm rm devvm-my-project-71a99b1076d92c --no-export
    ```
  </Accordion>

  <Accordion title="Delete stray Lima VMs and broken vms/ entries">
    - **Stray Lima VMs:** If `devvm space` reports a stray VM (a Lima VM with `devvm`'s label that has no state in `~/.local/share/devvm/vms/`, such as one created under a different `XDG_DATA_HOME`), delete it with:
      ```sh
      devvm limactl delete <vm-name>
      ```
    - **Incomplete folders in `vms/`:** If `devvm doctor` warns of a folder in `~/.local/share/devvm/vms/` that has no `vm.json` (left behind by a killed process) or an unrecognized entry, remove that directory directly with `rm -rf ~/.local/share/devvm/vms/<entry>`.
  </Accordion>

  <Accordion title="Repair a missing shared tool mount (devvm tools update)">
    If a shared tool image in `~/.local/share/devvm/tools/` was manually deleted while a VM's `vm.json` still lists it, launching the VM stops with an error naming the unmounted tool (`/run/devvm/tools-unmounted`). Rebuild and remount the missing tool image by running:

    ```sh
    devvm tools update
    ```
  </Accordion>
</AccordionGroup>

## Related pages

<CardGroup cols={2}>
  <Card title="Network tiers and host proxy" href="/network-tiers-and-proxy">
    Traffic confinement across allowlist, open, and offline tiers, host-side TLS SNI and HTTP inspection via devvm _proxy, and per-VM CA rotation.
  </Card>
  <Card title="Manage disk retention, garbage collection, and upgrades" href="/disk-retention-and-upgrades">
    Configure automatic retention policies for run logs, agent history exports, unused tools, orphaned state, and Nix caches, and upgrade devvm installations.
  </Card>
  <Card title="Diagnose host, boot, and SSH failures" href="/host-and-vm-diagnostics">
    Resolve devvm doctor host problems, Ubuntu cloud image boot fallbacks, SSH host-key verification errors, run logs, and moved project directories with devvm adopt.
  </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="CLI reference: images, storage, and host commands" href="/cli-image-space-and-host-commands">
    Syntax, subcommands, and flags for devvm build, rebuild, images, layer, tools, space, gc, retention, setup, doctor, upgrade, completion, and limactl.
  </Card>
  <Card title="Configuration (config.toml) and --json schema reference" href="/configuration-and-json-reference">
    Complete reference for ~/.config/devvm/config.toml keys, settings.json precedence, environment variables, state directory layout, and version 1 --json payloads.
  </Card>
</CardGroup>
