# Configuration (config.toml) and --json schema reference

> Complete reference for ~/.config/devvm/config.toml keys, settings.json precedence, environment variables, state directory layout, and version 1 --json payloads.

- 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/settings/config.go`
- `internal/settings/settings.go`
- `internal/settings/values.go`
- `internal/settings/retention.go`
- `internal/cli/json.go`
- `internal/paths/dirs.go`
- `internal/paths/vm.go`

---

---
title: "Configuration (config.toml) and --json schema reference"
description: "Complete reference for ~/.config/devvm/config.toml keys, settings.json precedence, environment variables, state directory layout, and version 1 --json payloads."
---

`devvm` reads host configuration from two files managed by `internal/settings` (`~/.config/devvm/config.toml` and `~/.local/share/devvm/settings.json`), resolves runtime paths and overrides from host environment variables (`internal/paths` and `internal/cli`), stores all host-side state outside project directories under XDG base directories, and emits versioned machine-readable output (`"version": 1`) for every read-only CLI inspection command that accepts `--json`.

## Configuration precedence and resolution

When resolving defaults for new VMs and automated retention (`settings.Read`), `devvm` evaluates four layers in order, where the first source that defines a setting wins:

1. **Per-command CLI flags** (such as `--network`, `--ports`, `--cpus`, `--memory`, `--disk`, `--features`, `--agents`, `--layers`).
2. **`~/.config/devvm/config.toml`** (`$XDG_CONFIG_HOME/devvm/config.toml`, source label `<path-to-config.toml>`, JSON source `"config.toml"`). Edited manually; `devvm` reads this file and never writes to it.
3. **`~/.local/share/devvm/settings.json`** (`$XDG_DATA_HOME/devvm/settings.json`, source label `devvm: settings.json`, JSON source `"settings.json"`). Written by `devvm setup`, `devvm size -g`, `devvm ports -g`, `devvm network -g`, and `devvm retention`.
4. **Built-in defaults** (source label `devvm: default`, JSON source `"built-in"`).

```mermaid
flowchart LR
    subgraph Overrides["1. Per-Invocation CLI Flags"]
        Flags["--network / --ports / --cpus\n--memory / --disk / --agents / --layers"]
    end

    subgraph UserConfig["2. User Config ($XDG_CONFIG_HOME/devvm)"]
        TOML["config.toml\n(decodeConfig: strict TOML + Undecoded check)\nSource: \"config.toml\""]
    end

    subgraph SavedState["3. Saved CLI Defaults ($XDG_DATA_HOME/devvm)"]
        JSON["settings.json\n(readSaved / writeSaved)\nSource: \"settings.json\""]
    end

    subgraph Builtin["4. Built-in Fallbacks (internal/settings)"]
        Def["NetworkDefault = \"allowlist\"\nPortsAll = \"all\"\nNixCacheDefault = 10 GiB\nSource: \"built-in\""]
    end

    Flags -->|"Overrides for single VM"| Effective["Effective Plan / settings.Defaults"]
    TOML -->|"Wins over settings.json"| Effective
    TOML -.->|"Blocks settings.Save()\n(set_in_config error)"| JSON
    JSON -->|"Wins over built-in"| Effective
    Def -->|"Fallback when unset"| Effective
```

<Warning>
`settings.Save` checks `config.toml` before writing to `settings.json`. If a CLI command (`devvm setup`, `devvm size -g`, `devvm ports -g`, `devvm network -g`, or `devvm retention`) attempts to save a key that is already defined in `config.toml`, `devvm` refuses the write with `set_in_config`, names the file path and key, and leaves `settings.json` unchanged.
</Warning>

### Strict validation and corruption handling

Both `config.toml` and `settings.json` are validated in full on every read:

- **`config.toml` (`decodeConfig`)**: Decoded with `github.com/BurntSushi/toml`. Any syntax error, type mismatch, invalid value, duplicate list element, or unknown key (`md.Undecoded()`) returns a `*state.CorruptError` containing the file path and line number (`keyLine`). Note that `config.toml` has no `backend` key; adding one fails as an unknown key.
- **`settings.json` (`readSaved`)**: Unmarshaled into `settings.Values`. Unknown top-level JSON fields and unknown retention keys are ignored so older `devvm` binaries tolerate newer saved keys, whereas invalid values for known keys return a `*state.CorruptError` naming the file path and offending key.

---

## `config.toml` reference

`~/.config/devvm/config.toml` is optional. All keys are optional and validated on read.

```toml title="~/.config/devvm/config.toml"
# Network tier for new VMs: "open", "internet", "allowlist", or "none"
network = "allowlist"

# Ports forwarded from new VMs to host 127.0.0.1: "all", "none", or port/range list
ports = "5173,8000-8010"

# Base image features in execution order, or [] for none
features = ["cli-tools", "fzf", "omb"]

# Coding agents mounted into new VMs, or [] for none
agents = ["claude", "codex"]

# Image layers applied bottom-first to new VMs, or [] for none
layers = ["docker"]

# Ubuntu archive mirror tried first during initial base image builds
ubuntu-mirror = "http://mirror.example.com/ubuntu-ports"

# Daily check for newer devvm releases when running in a terminal
release-check = true

[size]
cpus = 4
memory = 8
disk = 100

[retention]
logs.runs = 20
history.age = "90d"
history.count = 10
tools.unused-age = "30d"
orphans.age = "30d"
nix-cache.size = "20G"

[proxy]
https = "http://alice@proxy.example.com:8080"
http = "http://alice@proxy.example.com:8080"
no-proxy = "localhost,.example.com"
```

### Top-level and `[size]` keys

| Key | TOML Type | Default | Constraints & Validation | Saved in `settings.json`? |
| --- | --- | --- | --- | --- |
| `network` | `string` | `"allowlist"` | One of `"open"`, `"internet"`, `"allowlist"`, `"none"` (`settings.NetworkChoices`). | Yes (`devvm network -g`, `devvm setup`) |
| `ports` | `string` | `"all"` | `"all"`, `"none"`, or comma-separated ports/ranges (`1`–`65535`, e.g. `"5173,8000-8010"`). Normalized via `settings.ParsePorts`. | Yes (`devvm ports -g`) |
| `features` | `string[]` | Unchosen (`nil`) | Valid feature names matching `^[a-z0-9][a-z0-9-]*$` (`image.FeatureName`, e.g. `"cli-tools"`, `"fzf"`, `"omb"`). No duplicates. `[]` selects no features. | Yes (`devvm setup --features`) |
| `agents` | `string[]` | Unchosen (`nil`) | Subset of `image.Agents`: `"claude"`, `"codex"`, `"opencode"`. No duplicates. `[]` selects no agents. | Yes (`devvm setup --agents`) |
| `layers` | `string[]` | Unchosen (`nil`) | Valid layer names matching `^[a-z0-9][a-z0-9-]*$` (`image.LayerName`), excluding `"base"` and shared tool names (`node`, `python`, `rust`, `go`, `dotnet`, `claude`, `codex`, `opencode`). Built-in layers are `"desktop"`, `"docker"`, `"nix"`. Ordered bottom-first. No duplicates. | Yes (`devvm setup --layers`) |
| `size.cpus` | `int`, `float`, or `string` | `""` (image default: `4`) | Positive whole integer matching `^[1-9][0-9]*$` (`settings.ParseCPUs`). | Yes (`devvm size -g`, `devvm setup`) |
| `size.memory` | `int`, `float`, or `string` | `""` (image default: `8`) | Positive number of GiB (`N`, `NG`, `Ng`, or `NGiB`, decimals allowed, e.g. `8`, `8.5`, `"16G"`). Normalized to numeric string `N` via `settings.ParseGiB`. | Yes (`devvm size -g`, `devvm setup`) |
| `size.disk` | `int`, `float`, or `string` | `""` (image default: `100`) | Positive number of GiB (`N`, `NG`, `Ng`, or `NGiB`, decimals allowed). Normalized to numeric string `N` via `settings.ParseGiB`. | Yes (`devvm size -g`) |
| `ubuntu-mirror` | `string` | `""` (Ubuntu archive) | `http://` or `https://` URL with non-empty host and no user info, query string, fragment, whitespace, or control characters (`checkUbuntuMirror`). | No (`config.toml` only) |
| `release-check` | `bool` | `true` | TOML boolean (`true` or `false`). Disabled when `false` or when `DEVVM_NO_RELEASE_CHECK` is non-empty. | No (`config.toml` only) |

### `[retention]` keys

All retention settings default to `off` unless explicitly configured in `config.toml` or `settings.json`. When `logs.runs` is off, `devvm` retains the newest 20 run logs; when `nix-cache.size` is off, the shared Nix cache cap defaults to 10 GiB (`settings.NixCacheDefault`).

| Key in `[retention]` | CLI Key (`devvm retention`) | TOML Type | Format & Constraints | What It Prunes |
| --- | --- | --- | --- | --- |
| `logs.runs` | `logs.runs` | `int` or `string` | Whole integer `1`–`10000` (`count`). | Run logs in `logs/runs/*.log` beyond the newest `N` files (20 when off). |
| `history.age` | `history.age` | `string` | Duration matching `^[1-9][0-9]*[mhdw]$` (`1`–`100000`, e.g. `"30m"`, `"12h"`, `"90d"`, `"2w"` via `state.ParseDuration`). | Agent history archives in `exports/*.tar.gz` (and `.folder` sidecars) at least this old. |
| `history.count` | `history.count` | `int` or `string` | Whole integer `1`–`10000` (`count`). | Agent history archives in `exports/*.tar.gz` beyond the newest `N` exports. |
| `tools.unused-age` | `tools.unused-age` | `string` | Duration (`m`, `h`, `d`, `w` via `state.ParseDuration`). | Current shared tool images in `tools/` unused by any VM for at least this duration. |
| `orphans.age` | `orphans.age` | `string` | Duration (`m`, `h`, `d`, `w` via `state.ParseDuration`). | State directories in `vms/<hash>/` for VMs deleted outside `devvm` whose state has not changed for this duration (applied only by `devvm gc --retention`). |
| `nix-cache.size` | `nix-cache.size` | `string` | Whole number of `K`, `M`, `G`, or `T` (powers of 1024, `> 0`, `<= 1 PiB`, e.g. `"500M"`, `"20G"` via `state.ParseSize`). | Least-recently-used entries in `cache/nix-cache/` when total size exceeds the cap (10 GiB when off). |

### `[proxy]` keys

The `[proxy]` table configures an upstream corporate proxy (`settings.HostProxy`) used by per-VM host proxies, image builds, and host-side `devvm` downloads when neither `HTTPS_PROXY`/`https_proxy` nor `HTTP_PROXY`/`http_proxy` is set in the host environment. If the environment defines `HTTPS_PROXY` or `HTTP_PROXY`, the environment's `HTTPS_PROXY`, `HTTP_PROXY`, and `NO_PROXY` override the entire `[proxy]` table.

| Key | TOML Type | Format & Constraints |
| --- | --- | --- |
| `proxy.https` | `string` | `http://` URL or `host:port` (`backend.CheckProxyURL`). Non-`http://` schemes and line breaks are rejected. Must **not** contain a password (`proxy_password_in_config`). If a username is present (`http://user@host:port`), `devvm` reads the password from the `proxy` secret (`devvm secret set proxy`). |
| `proxy.http` | `string` | Same format and password restrictions as `proxy.https`. |
| `proxy.no-proxy` | `string` | Comma-separated host/domain/IP bypass list without line breaks (`backend.CheckNoProxy`). |

### `settings.json` wire format

`~/.local/share/devvm/settings.json` serializes the `settings.Values` struct with 2-space indentation. Unset scalar fields are omitted; chosen empty lists serialize as `[]`:

```json title="~/.local/share/devvm/settings.json"
{
  "network": "allowlist",
  "ports": "5173,8000-8010",
  "size": {
    "cpus": "4",
    "memory": "8",
    "disk": "100"
  },
  "features": [
    "cli-tools",
    "fzf",
    "omb"
  ],
  "agents": [
    "claude"
  ],
  "layers": [],
  "retention": {
    "history.age": "90d",
    "tools.unused-age": "30d"
  }
}
```

---

## Environment variables

`internal/structure` enforces single-owner rules on environment variable lookups across the codebase (`checkEnv`).

### User and host environment variables

| Variable | Owner / Reader | Behavior |
| --- | --- | --- |
| `HOME` | `internal/paths/dirs.go` | Must be an absolute path (`home_not_absolute` if relative or empty). Base for default data, config, and cache directories. |
| `XDG_CONFIG_HOME` | `internal/paths/dirs.go` | Base directory for user config (`$XDG_CONFIG_HOME/devvm`). Ignored if relative; defaults to `~/.config/devvm`. |
| `XDG_DATA_HOME` | `internal/paths/dirs.go` | Base directory for `devvm` state (`$XDG_DATA_HOME/devvm`). Ignored if relative; defaults to `~/.local/share/devvm`. |
| `XDG_CACHE_HOME` | `internal/paths/dirs.go` | On Linux, locates Lima's download cache (`$XDG_CACHE_HOME/lima`, default `~/.cache/lima`). On macOS, `UserCache` is always `~/Library/Caches`. |
| `DEVVM_LIMA_HOME` | `internal/paths/dirs.go` | Moves `devvm`'s isolated Lima home directory from `~/.local/share/devvm/lima` to the specified path (resolved via `filepath.Abs`). `devvm` never reads `LIMA_HOME`. |
| `DEVVM_FROM` | `internal/cli/flags.go` | Default OCI/Docker image reference for `--from` when building or launching VMs without an explicit `--from` flag. |
| `DEVVM_NO_TOOL_CHECK` | `internal/tools/tools.go` | When non-empty (e.g. `1`), disables the daily background check for newer shared tool versions. |
| `DEVVM_NO_RELEASE_CHECK` | `internal/settings/release.go` | When non-empty (e.g. `1`), disables the daily check for a newer `devvm` release. |
| `DEVVM_SECRET_SERVICE` | `internal/secrets/secrets.go` | Overrides the macOS Keychain service name used to store secrets (defaults to `"devvm"`). |
| `CLAUDE_CODE_OAUTH_TOKEN` | `internal/secrets/claude.go` | Supplies the Claude Code OAuth token from the host environment, taking precedence over any saved `claude` secret or agent login. |
| `HTTPS_PROXY`, `https_proxy`, `HTTP_PROXY`, `http_proxy`, `NO_PROXY`, `no_proxy` | `internal/backend/hostproxy.go` | Upstream corporate proxy settings. Uppercase takes precedence over lowercase. When either `HTTPS_PROXY` or `HTTP_PROXY` is non-empty, overrides `[proxy]` in `config.toml`. |
| `NO_COLOR`, `FORCE_COLOR`, `CLICOLOR_FORCE`, `TERM` | `internal/ui/style.go` | Controls ANSI terminal color output. Evaluated in order: `NO_COLOR != ""` (or `DEVVM_MESSAGES=names`) disables color; `FORCE_COLOR` or `CLICOLOR_FORCE` set to any non-empty value other than `"0"` forces color on non-TTYs; `TERM=dumb` disables color. |
| `SSH_AUTH_SOCK`, `SSH_ASKPASS` | `internal/vm/sshagent.go`, `internal/sshagent/sshagent.go` | `SSH_AUTH_SOCK` provides the user's host SSH agent socket when `ssh-agent` mode is `on`; `SSH_ASKPASS` is checked when adding keys to `devvm`'s agent with `--confirm` on Linux. |

### Internal and test-build environment variables

| Variable | Scope | Purpose |
| --- | --- | --- |
| `DEVVM_MESSAGES` | Any build (`internal/ui/style.go`) | When set to `"names"`, formats messages as machine-checkable `id key=value` lines and disables ANSI styling. |
| `DEVVM_GUESTFWD` | Internal (`internal/backend/lima/qemu.go`) | Passed by `devvm` to its Linux QEMU wrapper (`devvm _qemu`) to configure guestfwd rules. |
| `DEVVM_DAEMON_STATUS` | Internal (`internal/daemon/status.go`) | Passed to detached background processes (`_proxy`, `_ports`, `_links`) naming the readiness status file. |
| `DEVVM_HOST_MISE`, `DEVVM_PIPED_ANSWERS`, `DEVVM_PICK_ON_PIPE`, `DEVVM_HISTORY_MAX`, `DEVVM_UNREVIEWED`, `DEVVM_KVM`, `DEVVM_UBUNTU_URL`, `DEVVM_UBUNTU_RELEASE`, `DEVVM_REPO_URL`, `DEVVM_RELEASE_KEY`, `DEVVM_RELEASE_CA`, `DEVVM_TEST_VERSION`, `DEVVM_TEST_CPUS`, `DEVVM_TEST_MEMORY_GIB`, `DEVVM_TEST_FREE_GIB`, `DEVVM_TEST_LIST_WAIT` | Test builds only (`internal/cli/overrides.go`) | Read exclusively by `readOverrides` when `testbuild.On()` is true (`-ldflags` in test builds). Ignored in release binaries. |

---

## State and configuration directory layout

Every host path written by `devvm` is defined in `internal/paths` (`Dirs` and `VM`) and verified against layout version `1` (`state.Layout = 1`) at startup under `version.lock`. Commands fail with `layout_newer` if either `version` file contains a number greater than `1`. Only `version`, `--version`, `help`, and `completion` bypass the layout version check.

<Tabs>
  <Tab title="Config directory (~/.config/devvm)">
    :::files
    ~/.config/devvm/ ($XDG_CONFIG_HOME/devvm, mode 0700)
    ├── version                  # Layout version integer ("1\n")
    ├── config.toml              # Hand-edited defaults, retention, proxy, ubuntu-mirror, release-check
    ├── policy                   # Global network allow/deny rules applied to every VM
    ├── ca/                      # Trusted corporate root CA certificates (*.pem, *.crt; no symlinks)
    ├── layers/                  # User-defined custom layers (<name>.sh and optional <name>.policy)
    └── agents/                  # Imported agent settings mounted/copied into VMs
        ├── claude/
        ├── codex/
        └── opencode/
    :::
  </Tab>
  <Tab title="Data directory (~/.local/share/devvm)">
    :::files
    ~/.local/share/devvm/ ($XDG_DATA_HOME/devvm, mode 0700)
    ├── version                  # Layout version integer ("1\n")
    ├── version.lock             # Flock held during layout checks and migrations
    ├── settings.json            # CLI-saved defaults (setup, size -g, ports -g, network -g, retention)
    ├── ssh_config               # Generated OpenSSH Host blocks for running VMs
    ├── agent.sock               # UNIX domain socket for devvm's own SSH agent
    ├── agent-keys.json          # Private keys registered with devvm's SSH agent
    ├── mise.lock                # Resolved shared tool versions and SHA-256/SHA-512 checksums
    ├── mise.lock.toml           # Copy of shared-tools/mise.toml from which mise.lock was resolved
    ├── mise.lock.checked        # Timestamp marker of last upstream tool version check
    ├── retention.run            # Timestamp marker of last automatic daily retention run
    ├── retention.lock           # Flock held while retention runs
    ├── tools.lock               # Flock held while building shared tools or updating mise.lock
    ├── oci-build.lock           # Flock serializing OCI (--from) base image builds
    ├── making.lock              # Shared/exclusive flock coordinating VM creation and gc --tool-images
    ├── groups.lock              # Flock serializing group join/leave and link updates
    ├── logins/                  # Per-agent OAuth/API credentials (<agent>/...) excluded from backups
    ├── secrets/                 # Secret values on hosts without macOS Keychain (mode 0700)
    ├── exports/                 # Saved agent history archives (<vm>-<date>-<time>.tar.gz + .folder)
    ├── tools/                   # Read-only EROFS shared tool images (<tool>-<ver>-<key>.erofs + .json)
    ├── packages/                # Cached base apt .deb files (<sha256>.deb) and sets/<base-key>.json
    ├── images/                  # Image records and build locks
    │   ├── rebuilds.json        # Rebuild counters per backend, --from origin, and image name
    │   ├── ubuntu.json          # Pinned Ubuntu cloud image metadata for this host
    │   ├── locks/               # Per-image key locks plus rebuilds, ubuntu, and packages locks
    │   └── <key>/record.json    # Built image metadata (key, name, chain, sha256, origin, time)
    ├── cache/                   # Regenerable caches
    │   ├── from-digests.json    # Last resolved registry/Docker digests for --from references
    │   ├── release.json         # Latest release version and timestamp from daily release check
    │   ├── mise/<version>/      # Host mise binaries used to resolve tool locks
    │   └── nix-cache/           # Shared Nix binary cache populated by VM proxies
    ├── logs/
    │   ├── tools-check.log      # Output of daily background tool version check
    │   └── runs/<timestamp>.log # Redacted command and build run logs
    ├── run/                     # Short-path runtime sockets, locks, and build VM directories
    │   ├── claims.lock          # Flock held while claiming SSH aliases or host ports (taken after VM.Lock)
    │   ├── create-<hash>.lock   # Flock held while creating a project folder's VM
    │   ├── build-ports/<vm>     # Ephemeral SSH port reservation for starting build VMs
    │   ├── builds/<build-vm>/   # Per-build-VM state directory (same layout as vms/<hash>/)
    │   ├── qemu/qemu-system-*   # Confined QEMU wrapper binary on Linux hosts
    │   ├── ports-<hash>.ctl     # SSH control socket for a VM's port forwarder (<= 104 bytes)
    │   └── link-<hash>.sock     # UNIX socket and .ctl for inter-VM link sessions
    ├── lima/                    # Default isolated Lima home ($DEVVM_LIMA_HOME overrides)
    └── vms/<hash>/              # Per-project VM folder (14-hex-digit project path hash)
    :::
  </Tab>
  <Tab title="Per-VM directory (vms/<hash>/)">
    :::files
    ~/.local/share/devvm/vms/<hash>/
    ├── vm.json                  # Authoritative VM state (state.VM, updated under lock)
    ├── lock                     # Flock held across any vm.json read-modify-write cycle
    ├── policy                   # Per-VM network allow/deny rules
    ├── home.tar.gz              # Temporary home backup present only during an active VM remake
    ├── logs/                    # Mode 0700 log directory (10 MiB rotation with .1 backup)
    │   ├── network.jsonl        # Proxy allow/block/forward/link event log (JSON Lines)
    │   ├── proxy.log            # Host proxy daemon stderr/stdout log
    │   ├── ports.log            # Port forwarder daemon log
    │   └── links.log            # Inter-VM link session daemon log
    └── run/                     # Mode 0700 runtime directory
        ├── remake.lock          # Flock held across VM remake/recovery
        ├── started              # Records hostagent PID and confinement state for current boot
        ├── known_hosts          # Pinned guest SSH host keys for the current boot
        ├── sandbox.sb           # Seatbelt sandbox profile on macOS hosts
        ├── proxy.json           # Host proxy runtime configuration (proxy.Config)
        ├── proxy.pid            # Host proxy PID and argv verification file
        ├── proxy.status         # Host proxy readiness status file
        ├── ports.lock           # Flock serializing port forwarder restarts
        ├── ports.pid            # Port forwarder PID file
        ├── ports.status         # Port forwarder readiness status file
        ├── links.pid            # Link session daemon PID file
        ├── links.json           # Active link targets read by the VM's proxy
        ├── secrets.json         # Placeholder-to-secret bindings read by the VM's proxy
        ├── secrets.policy       # Host allow rules contributed by bound secrets
        └── ca/                  # Per-VM TLS interception CA (excluded from Time Machine on macOS)
            ├── lock             # Flock protecting CA rotation
            ├── ca.pem           # Current interception CA certificate (30-day validity)
            ├── ca.key           # Current interception CA private key (never leaves host)
            ├── next.pem         # Next CA certificate (generated 7 days before expiry)
            └── next.key         # Next CA private key
    :::
  </Tab>
</Tabs>

---

## Version 1 `--json` schema reference

Every command that displays or lists state supports `--json` (`internal/cli/json.go`). `TestJSONShapes` and `TestShowCommandsTakeJSON` in `internal/cli/json_test.go` enforce the contract across the CLI:

- **Top-level envelope**: `printJSON` writes a single 2-space indented JSON object to `stdout` whose first key is always `"version": 1` (`const jsonVersion = 1`).
- **Additive compatibility**: Fields are only added to version 1. Renaming, removing, or changing the type of any field requires incrementing `jsonVersion`.
- **Encoding rules**: HTML characters (`<`, `>`, `&`) are not escaped (`enc.SetEscapeHTML(false)`). Empty slices serialize as `[]`, never `null`. Sizes are integer byte counts unless the field name specifies a unit (such as `memory_gib` or `disk_gib`). Optional fields that do not apply are omitted (`omitempty`).
- **Read-only enforcement (`showsOnly`)**: Commands that either show a setting when invoked bare or mutate it when given arguments/flags (`size`, `ports`, `network`, `ssh-agent`, `git-protect`, `clone`, `link publish`, `retention`) reject any mutating argument or flag when `--json` is passed (`json_shows_only`).
- **Diagnostic separation**: Warnings and notes are still written to `stderr`; `stdout` receives only the JSON document without ANSI color codes.

### Single-value and default-setting payloads

Commands that inspect a single VM setting (`printValue`) or a global default (`printDefault`) emit compact objects:

| Command | JSON Payload Shape | Field Values & Notes |
| --- | --- | --- |
| `devvm ports --json` | `{"version": 1, "ports": "all"}` | `"all"`, `"none"`, or normalized port/range list (`"5173,8000-8010"`). |
| `devvm ports -g --json` | `{"version": 1, "ports": "all", "source": "built-in"}` | `source` is `"config.toml"`, `"settings.json"`, or `"built-in"`. |
| `devvm network -g --json` | `{"version": 1, "policy": "allowlist", "source": "config.toml"}` | `policy` is `"open"`, `"internet"`, `"allowlist"`, or `"none"`. |
| `devvm git-protect --json` | `{"version": 1, "mode": "on"}` | `mode` is `"on"`, `"strict"`, `"off"`, or `"clone"` (for `--clone` VMs). |
| `devvm clone --json` | `{"version": 1, "clone": "on"}` | `clone` is `"on"` or `"off"`. |
| `devvm link publish --json` | `{"version": 1, "ports": "5432,8000-8010"}` | `"none"` when no ports are published, or normalized port/range list. |

### `devvm size --json` and `devvm size -g --json`

Defined by `sizeOutput` and `sizeShape` (`internal/cli/size.go`). Unset fields (such as `disk_gib` when global defaults leave disk size to the image) are omitted.

<CodeGroup>
```json title="devvm size --json"
{
  "version": 1,
  "cpus": 4,
  "memory_gib": 8,
  "disk_gib": 100,
  "pending": {
    "cpus": 6,
    "memory_gib": 16,
    "disk_gib": 120
  }
}
```

```json title="devvm size -g --json"
{
  "version": 1,
  "cpus": 4,
  "memory_gib": 8,
  "sources": {
    "cpus": "config.toml",
    "disk": "built-in",
    "memory": "settings.json"
  }
}
```
</CodeGroup>

| Field | Type | Description |
| --- | --- | --- |
| `cpus` | `number` | Current CPU count (omitted in `-g` when unset). |
| `memory_gib` | `number` | Current memory in GiB (omitted in `-g` when unset). |
| `disk_gib` | `number` | Current disk size in GiB (omitted in `-g` when unset or on backends without disk sizing). |
| `pending` | `object` | Omitted unless a running VM has staged size changes (`cpus`, `memory_gib`, `disk_gib`) awaiting restart. |
| `sources` | `object` | Present only with `-g`. Maps `"cpus"`, `"memory"`, and `"disk"` to `"config.toml"`, `"settings.json"`, or `"built-in"`. |

### `devvm retention --json`

Defined by `retentionOutput` (`internal/cli/retention.go`). Both `settings` and `sources` always include all six retention keys (`settings.RetentionKeys()`).

```json title="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": "config.toml",
    "history.count": "built-in",
    "logs.runs": "built-in",
    "nix-cache.size": "built-in",
    "orphans.age": "built-in",
    "tools.unused-age": "settings.json"
  }
}
```

### `devvm ls --json`

Defined by `lsOutput` and `vmJSON` (`internal/cli/ls.go`). Lists all project VMs sorted by name.

```json title="devvm ls --json"
{
  "version": 1,
  "vms": [
    {
      "name": "devvm-app-0123456789abcd",
      "status": "Running",
      "image": "docker",
      "tools": [
        "node",
        "claude"
      ],
      "from": "node:24",
      "git": "on",
      "net": "allowlist",
      "backend": "lima",
      "folder": "/Users/you/src/app",
      "folder_state": "there",
      "links": 1
    }
  ]
}
```

| Field | Type | Description |
| --- | --- | --- |
| `vms[].name` | `string` | Full VM name (`devvm-<slug>-<14-hex-hash>`). |
| `vms[].status` | `string` | Backend status (`"Running"`, `"Stopped"`, `"Broken"`, or `""` if unknown). |
| `vms[].image` | `string` | Image name (`"base"` or `+`-joined layers such as `"docker+desktop"`). |
| `vms[].tools` | `string[]` | Shared toolchains and coding agents attached to the VM (`[]` when none). |
| `vms[].from` | `string` | Container image reference if built with `--from`; omitted for standard Ubuntu base images. |
| `vms[].git` | `string` | `.git` protection mode: `"on"`, `"strict"`, `"off"`, `"clone"`, or `""`. |
| `vms[].net` | `string` | Active network tier (`"open"`, `"internet"`, `"allowlist"`, `"none"`) or `"unenforced"`. |
| `vms[].backend` | `string` | Backend driver name (`"lima"`). |
| `vms[].folder` | `string` | Host project folder path. |
| `vms[].folder_state` | `string` | Host folder availability: `"there"`, `"gone"`, or `"not_connected"`. |
| `vms[].links` | `number` | Count of outbound links configured on this VM. |

### `devvm images --json` and `devvm tools --json`

`devvm images --json` (`imagesOutput` in `internal/cli/images.go`) lists all VM images, shared tool EROFS images, aggregate byte totals, and shared Nix cache usage. `devvm tools --json` (`toolsOutput` in `internal/cli/tools.go`) lists the shared tools mounted in the current folder's VM.

<CodeGroup>
```json title="devvm images --json"
{
  "version": 1,
  "images": [
    {
      "name": "base",
      "from": "ubuntu 26.04",
      "digest": "sha256:abcdef012345...",
      "key": "7a76021c46ef",
      "state": "current",
      "size": 1932735283,
      "sized": true,
      "backend": "lima"
    }
  ],
  "tools": [
    {
      "name": "node",
      "tool": "node",
      "version": "24.1.0",
      "built": true,
      "backend": "lima",
      "libc": "gnu",
      "size": 134217728,
      "vms": 2,
      "state": "current"
    }
  ],
  "total": 2066953011,
  "unused_tools": 0,
  "nix_cache": {
    "files": 42,
    "size": 52428800,
    "cap": 10737418240
  }
}
```

```json title="devvm tools --json"
{
  "version": 1,
  "tools": [
    {
      "tool": "node",
      "version": "24.1.0",
      "newer": "24.2.0",
      "newer_built": true
    }
  ]
}
```
</CodeGroup>

| Command | Field | Type | Description |
| --- | --- | --- | --- |
| `images` | `images[].state` | `string` | `"current"`, `"stale"`, `"unfinished"`, or `"building"`. |
| `images` | `images[].digest` | `string` | `"sha256:<hex>"` or `""` for unfinished images. |
| `images` | `tools[].name` / `tools[].tool` | `string` | `name` is the shared tool group (`"node"`, `"python"`); `tool` is the individual binary/runtime in `mise.lock` (`"node"`, `"pnpm"`, `"bun"`, `"python"`, `"uv"`). |
| `images` | `tools[].state` | `string` | `"current"`, `"unused"`, or `"not built"`. `backend` and `libc` (`"gnu"` or `"musl"`) are omitted when `built` is `false`. |
| `images` | `total`, `unused_tools`, `nix_cache` | `number`, `object` | Byte totals and `nix_cache` (`files`, `size`, `cap` in bytes). |
| `tools` | `tools[].newer`, `tools[].newer_built` | `string`, `bool` | Omitted when the VM already uses the latest version in `mise.lock`. |

### Network, secrets, SSH agent, links, and groups payloads

<AccordionGroup>
  <Accordion title="devvm network --json, devvm network log --json, and --blocked">
    Defined in `internal/cli/network.go` (`networkOutput`, `logOutput`, `blockedOutput`). Note that `--json` and `--follow` are mutually exclusive on `devvm network log`, and `--blocked` is mutually exclusive with `--follow` and `--lines`.

    ```json title="devvm network --json"
    {
      "version": 1,
      "policy": "allowlist",
      "allowlist": [
        {
          "builtin": "layers/base.policy",
          "rules": [
            "archive.ubuntu.com",
            "security.ubuntu.com"
          ],
          "not_rules": []
        },
        {
          "file": "/Users/you/.local/share/devvm/vms/0123456789abcd/policy",
          "denies_only": true,
          "rules": [
            "!example.com"
          ],
          "not_rules": [
            {
              "line": 2,
              "text": "invalid line"
            }
          ]
        }
      ]
    }
    ```

    ```json title="devvm network log --json"
    {
      "version": 1,
      "events": [
        {
          "time": "2026-10-01T12:00:00Z",
          "event": "forward",
          "host": "api.github.com",
          "port": 443,
          "method": "GET",
          "path": "/user",
          "rule": "api.github.com",
          "agent": "claude",
          "secrets": [
            "github"
          ],
          "upstream": "proxy.example.com:8080"
        }
      ]
    }
    ```

    ```json title="devvm network log --blocked --json"
    {
      "version": 1,
      "blocked": [
        {
          "last": "2026-10-01T12:05:00Z",
          "count": 3,
          "host": "registry.npmjs.org",
          "port": 443,
          "allow": "devvm network allow registry.npmjs.org"
        }
      ]
    }
    ```

    - In `allowlist[]`, built-in policies set `builtin` (omitting `file`), whereas user policy files set `file` (omitting `builtin`). `denies_only` is `true` on `internet` and `none` tiers when only deny rules apply.
    - In `events[]`, optional fields (`method`, `path`, `rule`, `reason`, `agent`, `secrets`, `refused`, `upstream`, `target`) are omitted when empty/false.
  </Accordion>

  <Accordion title="devvm secret --json and devvm ssh-agent --json">
    Defined in `internal/cli/secret.go` (`secretOutput`) and `internal/cli/sshagent.go` (`sshAgentOutput`). Secret values are never included in JSON output.

    ```json title="devvm secret --json"
    {
      "version": 1,
      "secrets": [
        {
          "name": "claude",
          "saved": false,
          "used_by": []
        },
        {
          "name": "github",
          "saved": true,
          "used_by": [
            "devvm-app-0123456789abcd"
          ]
        },
        {
          "name": "proxy",
          "saved": false,
          "used_by": []
        }
      ]
    }
    ```

    ```json title="devvm ssh-agent --json"
    {
      "version": 1,
      "mode": "devvm",
      "keys": [
        {
          "file": "/Users/you/.ssh/deploy_key",
          "fingerprint": "SHA256:xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
          "confirm": true,
          "lifetime": "12h",
          "loaded": true
        }
      ]
    }
    ```
  </Accordion>

  <Accordion title="devvm link --json and devvm group --json">
    Defined in `internal/cli/link.go` (`linkOutput`) and `internal/cli/group.go` (`groupOutput`).

    ```json title="devvm link --json"
    {
      "version": 1,
      "links": [
        {
          "name": "db",
          "port": 5432,
          "target": "devvm-db-0123456789abcd",
          "target_folder": "/Users/you/src/db",
          "target_status": "Running",
          "applies": false,
          "reason": "unpublished"
        }
      ]
    }
    ```

    ```json title="devvm group --json"
    {
      "version": 1,
      "group": "web",
      "members": [
        {
          "name": "db",
          "vm": "devvm-db-0123456789abcd",
          "folder": "/Users/you/src/db",
          "published": "5432"
        }
      ]
    }
    ```

    - In `links[]`, `applies` is `true` when `reason` is `""`. When `applies` is `false`, `reason` is one of `"gone"`, `"replaced"`, `"unpublished"`, `"policy"`, or `"ungrouped"`.
    - In `group`, when the VM belongs to no group, `group` is `""` and `members` is `[]`.
  </Accordion>
</AccordionGroup>

### Storage, history, diagnostics, and upgrade payloads

<AccordionGroup>
  <Accordion title="devvm space --json and devvm history --json">
    Defined in `internal/cli/space.go` (`spaceReport`) and `internal/cli/history.go` (`historyOutput`).

    ```json title="devvm space --json"
    {
      "version": 1,
      "devvm": [
        {
          "what": "VMs",
          "backend": "lima",
          "count": 1,
          "size": 5368709120,
          "sized": true,
          "kept": "only copy",
          "gc": 0,
          "rest": "devvm rm"
        },
        {
          "what": "history",
          "count": 2,
          "size": 3145728,
          "sized": true,
          "kept": "only copy",
          "gc": 0,
          "rest": "gc --history",
          "retain": "90d",
          "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
        }
      ],
      "unknown": [
        "qemu"
      ],
      "strays": [
        {
          "backend": "lima",
          "name": "devvm-other-0123456789abcd",
          "data": "/tmp/other-data/devvm"
        }
      ],
      "folder_gone": [
        {
          "backend": "lima",
          "name": "devvm-old-0123456789abcd",
          "folder": "/Users/you/src/deleted"
        }
      ],
      "folder_not_connected": [
        {
          "backend": "lima",
          "name": "devvm-ext-0123456789abcd",
          "folder": "/Volumes/Work/ext"
        }
      ]
    }
    ```

    ```json title="devvm history --json"
    {
      "version": 1,
      "exports": [
        {
          "file": "/Users/you/.local/share/devvm/exports/devvm-app-0123456789abcd-20261001-120000-000000000.tar.gz",
          "vm": "devvm-app-0123456789abcd",
          "folder": "/Users/you/src/app",
          "date": "2026-10-01T12:00:00Z",
          "size": 1572864
        }
      ]
    }
    ```

    - In `spaceReport`, `devvm[].what` values are `"VMs"`, `"images"`, `"tools gnu"`, `"tools musl"`, `"tools -"`, `"history"`, `"network logs"`, `"run logs"`, `"mise"`, `"packages"`, `"Nix cache"`, `"leftover state"`, and `"state"`. `devvm[].kept` values are `"remade"`, `"only copy"`, `"log"`, `"state"`, and `"leftover"`.
    - `unknown`, `strays`, `folder_gone`, and `folder_not_connected` are omitted when empty.
  </Accordion>

  <Accordion title="devvm doctor --json and devvm upgrade --json">
    Defined in `internal/cli/doctor.go` (`doctorOutput`) and `internal/cli/upgrade.go` (`upgradeJSON`). `devvm doctor --json` always exits `0` even when `problems` is non-empty. `devvm upgrade --json` checks for updates without installing anything and is mutually exclusive with `--force` and `--to`.

    ```json title="devvm doctor --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": {
          "clone-from-image": "native",
          "confined-network": "native",
          "disk-resize": "native",
          "isolated-networks": "native",
          "nested-virt": "native",
          "oci-import": "native",
          "read-only-mounts": "native",
          "ssh": "native",
          "tool-mounts": "native",
          "writable-mounts": "native"
        }
      },
      "release": {
        "latest": "0.2.0",
        "checked": "2026-11-05T00:00:00Z"
      }
    }
    ```

    ```json title="devvm upgrade --json"
    {
      "version": 1,
      "current": "0.1.0",
      "latest": "0.2.0",
      "newer": true,
      "install": "install.sh",
      "path": "/Users/you/.local/lib/devvm/0.1.0/devvm"
    }
    ```

    - In `doctorOutput`, `problems[].fix` is `null` when no automated fix command exists, `features.<backend>.<feature>` is `"native"`, `"emulated"`, `"unreviewed"`, or `"none"`, and `release` is `null` if no daily release check has recorded a result yet.
    - In `upgradeJSON`, `install` is `"install.sh"`, `"homebrew"`, `"homebrew-rc"`, or `"other"`.
  </Accordion>
</AccordionGroup>

## Related pages

<CardGroup cols={2}>
  <Card title="VM lifecycle and state" href="/vm-lifecycle-and-state">
    Inspect how project folders map to 14-hex-digit VM hashes, `vm.json` state sections, and layout version checks.
  </Card>
  <Card title="Manage disk retention, garbage collection, and upgrades" href="/disk-retention-and-upgrades">
    Configure automatic pruning with `devvm retention`, reclaim disk space with `devvm gc`, and upgrade `devvm` installations.
  </Card>
  <Card title="Troubleshoot network policies, CAs, and disk space" href="/network-ca-and-storage-troubleshooting">
    Configure corporate proxies in `[proxy]`, add custom root CAs in `~/.config/devvm/ca`, and inspect `devvm network log`.
  </Card>
  <Card title="CLI reference: images, storage, and host commands" href="/cli-image-space-and-host-commands">
    Complete command syntax and flags for `devvm setup`, `devvm space`, `devvm retention`, `devvm doctor`, and `devvm upgrade`.
  </Card>
</CardGroup>
