# CLI reference: VM and workspace commands

> Syntax, flags, exit codes, and examples for devvm (up), ls, restart, stop, update, rm, adopt, size, ports, host, code, desktop, and history.

- 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/root.go`
- `internal/cli/up.go`
- `internal/cli/flags.go`
- `internal/cli/args.go`
- `internal/cli/ls.go`
- `internal/cli/size.go`
- `internal/cli/update.go`
- `internal/cli/restart.go`

---

---
title: "CLI reference: VM and workspace commands"
description: "Syntax, flags, exit codes, and examples for devvm (up), ls, restart, stop, update, rm, adopt, size, ports, host, code, desktop, and history."
---

The `devvm` root command and its VM and workspace subcommands (`ls`, `restart`, `stop`, `update`, `rm`, `adopt`, `size`, `ports`, `host`, `code`, `desktop`, and `history`) manage the lifecycle, resource allocation, port forwarding, editor/desktop connections, and agent history exports for per-project virtual machines. Commands execute through `cli.Execute` and `ui.Main`, validating all flags and positional arguments before running layout version checks or inspecting backend state.

## Global flags, folder resolution, and exit codes

Every command inherits persistent flags from the root command, validates arguments and flag values prior to execution, and resolves the target VM from the working directory unless a command operates globally across all VMs (`ls`, `history`) or targets a specific VM by name (`rm VM`, `adopt VM`).

### Persistent and shared flags

| Flag | Shorthand | Applies to | Description |
| --- | --- | --- | --- |
| `--verbose` | `-v` | All commands | Stream external command output (`LoggedCmd`) directly to `stderr` instead of displaying a status spinner. |
| `--no-input` | — | All commands | Never prompt interactively; fail immediately at any prompt and print the flag or command that answers the question. |
| `--help` | `-h` | All commands | Print command synopsis, examples, flags, and documentation URL. |
| `--here` | — | Folder VM commands | Target only the current directory's own VM, ignoring any parent folder VM. Mutually exclusive with `--parent` and `--global` (`-g`). |
| `--parent` | — | Folder VM commands | Target the nearest parent directory's VM, crossing nested Git repositories and bypassing the current directory's own VM. Mutually exclusive with `--here`, `--clone`, and `--global` (`-g`). |
| `--json` | — | `ls`, `size`, `ports`, `history` | Emit a single indented JSON object with `"version": 1` to `stdout` and route informational messages to `stderr`. |

### Target VM folder resolution

Commands that act on a project folder resolve their target VM through `finder.target` and `resolveFolder` using one of three `FolderRule` modes:

```mermaid
flowchart TD
    Start["Command invoked in working directory (dir)"] --> CheckOwn["Read state for project.Hash(dir) or moved folder ID"]
    CheckOwn --> Rule{"FolderRule (--here / --parent)"}
    
    Rule -->|"FolderHere (--here)"| OwnOnly{"Own VM state exists?"}
    OwnOnly -->|Yes| UseOwn["Target current folder's VM"]
    OwnOnly -->|No| NoVM["New VM (devvm) or error: no_vm"]

    Rule -->|"FolderAuto (default)"| HasOwn{"Own VM state exists?"}
    HasOwn -->|Yes| UseOwn
    HasOwn -->|No| SearchParent["Walk project.Parents(dir, HOME)"]
    SearchParent --> FoundParent{"Parent VM exists?"}
    FoundParent -->|No| NoVM
    FoundParent -->|Yes| NestedGit{"Nested .git between dir and parent?"}
    NestedGit -->|Yes| NoVM
    NestedGit -->|No| UseParent["Target parent VM (prints UsingParentVM)"]

    Rule -->|"FolderParent (--parent)"| ForceParent["Walk project.Parents(dir, HOME) ignoring nested .git"]
    ForceParent --> HasParent{"Parent VM exists?"}
    HasParent -->|Yes| UseParent
    HasParent -->|No| ErrNoParent["Error: no_parent_vm"]
```

- **Default (`FolderAuto`)**: Uses the current folder's own VM if `vm.json` exists or the backend holds the instance. If the folder has no VM and was moved or renamed from another path on the same filesystem (`platform.FolderID` match), `devvm` prompts to move the VM record to the new path. Otherwise, it walks parent directories up to (but excluding) `$HOME` and `/`, selecting the nearest parent VM unless a nested `.git` directory or file sits between the current directory and that parent.
- **`--here` (`FolderHere`)**: Restricts lookup to the current directory only.
- **`--parent` (`FolderParent`)**: Forces lookup of the nearest parent folder with a VM, even if the current folder has its own VM or a nested `.git` repository lies in between. Fails with `no_parent_vm` if no parent folder has a VM.

### Exit codes and pre-execution checks

| Exit code | Meaning | Trigger conditions |
| --- | --- | --- |
| `0` | Success | Command completed, or the user declined an interactive confirmation prompt (`[y/N]`) that leaves the VM unchanged. |
| `1` | Runtime failure | `RunE` returned an error, `state.CheckLayout` detected an incompatible state directory version, a prompt was refused under `--no-input` or non-TTY `stdin`, or a backend command failed. |
| `2` | Usage error | Unknown command or flag, blank flag argument (`--flag ""`), invalid flag value (`--cpus`, `--memory`, `--disk`, `--ports`, `--network`, `--tools`, `--agents`, `--layers`), mutually exclusive flags, or wrong positional argument count. Prints the error and points to `<command> --help`. |
| `130` | Interrupted | Process received `SIGINT`, `SIGTERM`, or `SIGHUP`. Cancels root context, waits for active logged commands and up to `2s` (`returnGrace`) when cleanups are registered, runs the cleanup stack, and suppresses post-interrupt error output. |

<Note>
Before any command logic or VM lookup runs, `checkCommand` rejects empty string arguments or blank string flag values (`noBlanks`), validates mutually exclusive flag groups, and parses typed flag values. Passing a toolchain, agent, or layer name as a bare positional argument to `devvm` (such as `devvm node` or `devvm node+claude`) exits with code `2` and prints the exact `--tools`, `--agents`, or `--layers` flags to use instead.
</Note>

---

## `devvm` (up)

Opens an interactive shell in the current folder's VM at the matching guest path, creating and provisioning the VM first if the folder does not yet have one.

```bash
devvm [flags]
```

### Behavior

1. **New VM creation**:
   - Checks whether the folder is safe to mount (`checkUnsafe` refuses `$HOME`, directories holding `$HOME` or `devvm` state/config/Lima directories, or directories inside `devvm`'s own folders unless `--unsafe-folder` is passed).
   - Evaluates `.devcontainer/devcontainer.json` or `.devcontainer.json` unless `--no-devcontainer` is set.
   - On first run (when base features or default agents are unconfigured in `settings.json` / `config.toml`), prompts in the terminal for default VM size, default coding agents, and base image features.
   - Displays a summary of the planned VM (image layers, detected and selected toolchains, coding agents, network tier, forwarded ports, and CPU/memory/disk size) and offers an interactive picker (`Customize? [y/N]`) when running in a terminal with unflagged options.
   - Builds the base and layer image chain and any missing read-only EROFS/ext4 shared tool images, holding a creation lock (`holdCreate`) and tool-making lock (`tools.Making.Hold`) so concurrent `gc` or retention passes do not delete in-progress artifacts.
   - Protects `.git` read-only (unless `--no-git-protect` or `--clone`), starts the VM and host proxy (`devvm _proxy`), offers to import the newest saved agent history export for this folder if the VM includes a coding agent, syncs guest environment scripts, updates `~/.local/share/devvm/ssh/config`, runs `devcontainer.json` lifecycle commands, and replaces the `devvm` process (`e.Exec`) with the guest SSH shell.
   - If interrupted (`Ctrl+C`) before the first shell opens, the registered cleanup deletes the partially created VM, removes its state and proxy, and unlocks `.git`.
2. **Existing VM launch**:
   - If a prior remake (`devvm update`, `devvm layer add/rm`, `devvm clone`) was interrupted before completion, `devvm` resumes and finishes the pending remake first.
   - Warns if the VM's image chain or shared tools are outdated compared to current definitions or locks.
   - Warns and ignores creation-only flags (`--tools`, `--agents`, `--layers`, `--network`, `--no-git-protect`, `--cpus`, `--memory`, `--disk`, `--ports`, `--ssh-agent`, `--from`, `--no-devcontainer`), printing the corresponding subcommand (`devvm tools`, `devvm size`, etc.) to modify the existing VM. Passing `--clone` on an existing folder-mounted VM fails and directs you to `devvm clone on`.

### Flags

<ParamField body="--tools" type="string" default="auto-detected from project files">
  Comma-separated `LIST` of shared toolchains for a new VM (`node`, `python`, `rust`, `go`, `dotnet`), or `none` alone. Passing an agent or layer name to `--tools` fails with a usage error naming `--agents` or `--layers`.
</ParamField>

<ParamField body="--agents" type="string" default="devvm setup default">
  Comma-separated `LIST` of coding agents for a new VM (`claude`, `codex`, `opencode`), or `none` alone.
</ParamField>

<ParamField body="--layers" type="string" default="devvm setup default">
  Comma-separated `LIST` of image layers for a new VM, ordered bottom-first (built-ins: `docker`, `nix`, `desktop`, plus custom layers in `~/.config/devvm/layers/`), or `none` alone.
</ParamField>

<ParamField body="--from" type="string" default="$DEVVM_FROM">
  OCI/Docker container `IMAGE` reference (such as `node:24`) to build the base image from instead of Ubuntu's cloud image.
</ParamField>

<ParamField body="--network" type="string" default="devvm network -g (else allowlist)">
  Network confinement `TIER` for a new VM: `open`, `internet`, `allowlist`, or `none`.
</ParamField>

<ParamField body="--cpus" type="string" default="devvm size -g (else image default)">
  Whole number `N` of virtual CPUs (`>= 1`, up to host CPU count).
</ParamField>

<ParamField body="--memory" type="string" default="devvm size -g (else image default)">
  Memory allocation in `GiB` (such as `16` or `16G`; decimals allowed, `>= 0.5`, capped at `backend.MaxMemory` of host RAM).
</ParamField>

<ParamField body="--disk" type="string" default="devvm size -g (else image default)">
  Disk size in `GiB` (such as `100` or `100G`; at least the base image size).
</ParamField>

<ParamField body="--ports" type="string" default="devvm ports -g (else all)">
  Guest ports to forward to host `127.0.0.1`: `all`, `none`, or a comma-separated list of ports and ranges (`1`–`65535`) such as `5173,8000-8010`.
</ParamField>

<ParamField body="--clone" type="boolean" default="false">
  Create the VM with an isolated Git clone of the current repository inside the VM disk instead of mounting the host project directory. Mutually exclusive with `--parent`; implies `--here`.
</ParamField>

<ParamField body="--unsafe-folder" type="boolean" default="false">
  Allow mounting a directory that `devvm` normally refuses (such as `$HOME` or a parent of `$HOME`).
</ParamField>

<ParamField body="--no-git-protect" type="boolean" default="false">
  Create the VM without read-only `.git` mount protection.
</ParamField>

<ParamField body="--ssh-agent" type="boolean" default="false">
  Create the VM with host `SSH_AUTH_SOCK` agent forwarding enabled (`on`).
</ParamField>

<ParamField body="--no-devcontainer" type="boolean" default="false">
  Ignore `.devcontainer/devcontainer.json` and `.devcontainer.json` when creating the VM.
</ParamField>

<ParamField body="--ssh-config" type="boolean" default="false">
  Add `Include ~/.local/share/devvm/ssh/config` to `~/.ssh/config` without prompting.
</ParamField>

### Examples

```bash
# Open this folder's VM, interactively choosing parts if creating a new VM
devvm

# Create a VM with Claude Code and auto-detected project toolchains
devvm --agents claude

# Create a VM with Node.js, no coding agents, and the docker layer only
devvm --tools node --agents none --layers docker

# Create a VM with an isolated repo clone and 16 GiB RAM
devvm --clone --memory 16
```

---

## `devvm ls`

Lists all project VMs across all registered backends, sorted alphabetically by VM name.

```bash
devvm ls [--json]
```

### Columns and JSON schema

In terminal/table output, `devvm ls` prints 8 columns (`-` when empty or unset):

| Column | JSON field | Type | Values and semantics |
| --- | --- | --- | --- |
| `NAME` | `vms[].name` | `string` | Full VM identifier (`devvm-<slug>-<14-hex>`). |
| `STATUS` | `vms[].status` | `string` | `Running` (green), `Stopped` (yellow), or `Broken` / other backend status (red). |
| `IMAGE` | `vms[].image`, `vms[].from` | `string` | Layer chain name (e.g., `base`, `docker`, `docker+nix`). In table output, formats as `<image> (from <from>)` when `from` is non-empty. |
| `TOOLS` | `vms[].tools` | `[]string` | Shared toolchains and coding agents mounted in the VM (comma-separated in table output; `-` if empty). |
| `GIT` | `vms[].git` | `string` | `.git` protection mode: `on`, `strict`, `clone` (green), `off`, or `""`. |
| `NET` | `vms[].net` | `string` | Network policy tier: `allowlist`, `internet`, `none` (green), `open`, or `unenforced` (red). |
| `LINKS` | `vms[].links` | `number` | Count of outbound links configured from this VM to other VMs (`-` in table output when `0`). |
| `FOLDER` | `vms[].folder`, `vms[].folder_state` | `string` | Host project path. In table output, appends `(gone)` or `(not connected)` in yellow when `folder_state` is `"gone"` or `"not_connected"`; normal when `"there"`. |
| — | `vms[].backend` | `string` | Backend driver name (e.g., `"lima"`). Included in `--json` output. |

<Note>
If any `vm.json` state file under `~/.local/share/devvm/vms/` is unreadable or corrupt, `devvm ls` emits a warning to `stderr` (`vm_state_unreadable`) for each unreadable file and lists the remaining VMs.
</Note>

<CodeGroup>
```bash title="Table output"
devvm ls
# NAME                          STATUS   IMAGE   TOOLS        GIT  NET        LINKS  FOLDER
# devvm-api-0123456789abcd      Running  docker  go,claude    on   allowlist  1      /Users/alex/src/api
# devvm-web-71a99b1076d92c      Stopped  base    node,claude  on   allowlist  -      /Users/alex/src/web (gone)
```

```json title="devvm ls --json"
{
  "version": 1,
  "vms": [
    {
      "name": "devvm-api-0123456789abcd",
      "status": "Running",
      "image": "docker",
      "tools": [
        "go",
        "claude"
      ],
      "git": "on",
      "net": "allowlist",
      "backend": "lima",
      "folder": "/Users/alex/src/api",
      "folder_state": "there",
      "links": 1
    }
  ]
}
```
</CodeGroup>

---

## `devvm restart` and `devvm stop`

Control the running state of the current folder's VM. Both commands accept `--here` and `--parent` and take no positional arguments.

```bash
devvm restart [--here | --parent]
devvm stop [--here | --parent]
```

### `devvm restart`

1. Resolves the target VM (must be a completed VM; fails if no VM exists or if a remake is pending).
2. Stops the VM instance via the backend (`t.stop`).
3. Runs the full existing-VM launch sequence (`open`): checks `devcontainer.json`, brings the VM up with any pending configuration changes (new CPU/memory/disk size, updated agent directory mounts, SSH agent forwarding mode, network tier confinement, and port forwards), syncs guest scripts, updates SSH config and links, and replaces the process with an interactive shell in the VM.

### `devvm stop`

1. Resolves the target VM. If the VM is already stopped, prints `==> <vm> is already stopped` (`already_stopped`); otherwise stops the backend instance and prints `==> Stopped <vm>`.
2. Stops the host egress proxy (`e.Proxy.Stop`), port forwarders (`e.Forwarders.Stop`), and inter-VM link sessions (`e.linkRuns.stop`).
3. Applies any `.git` protection mode queued while the VM was running, re-locks `.git` on the host, runs host safety checks (including set-id file checks on Linux), and removes the VM's entry from `~/.local/share/devvm/ssh/config`.

---

## `devvm update`

Updates the current folder's VM to the newest shared tool versions and, if its image chain or `devcontainer.json` has changed since creation, remakes the VM from the updated image while preserving its home directory and settings, then opens a shell in it.

```bash
devvm update [--force] [--no-export] [--clean-home] [--here | --parent]
```

### Execution flow

1. **Pending remake recovery**: If a prior remake on the VM was interrupted, finishes the pending remake first while holding `RemakeLock`.
2. **Clone restriction**: Refuses `--clone` VMs (`remake_clone`) because the repository clone resides on the VM disk; only `devvm clone` can remake a clone VM.
3. **Outdated check (`updatePlan`)**:
   - Checks whether the VM's base or layer image build chain (`st.Chain`) differs from current image definitions (`Builds.Outdated`).
   - Re-reads `.devcontainer/devcontainer.json` or `.devcontainer.json` (`devcontainers.reread`) to detect changes to the container base (`From`, falling back to `$DEVVM_FROM` or Ubuntu cloud image if removed), toolchains/agents, forwarded ports, or lifecycle commands.
4. **Confirmation**: If an image or `devcontainer.json` remake is needed, validates backend capabilities and disk space, then prompts in the terminal (`[y/N]`) before modifying anything. `--force` answers yes automatically.
5. **Shared tools update**: Updates mounted shared tools to their newest lock versions (`e.vmTools.update`), which takes effect on launch without remaking the VM disk.
6. **Remake and launch**:
   - If a remake was agreed to: builds the new image and any reread tools, exports agent history to `~/.local/share/devvm/exports/` (unless `--no-export`), archives `/home/<user>` from the running VM (unless `--clean-home`), stops the VM and renames the old machine aside (`<vm>-old`), creates and starts the new machine, restores `/home/<user>`, deletes the old machine, and opens a shell. Until `/home/<user>` is restored and committed, any failure or interrupt rolls back to the old machine (`putBack`).
   - If only shared tools were updated (or the user declined the image remake after tools updated), launches the existing VM with the newer tool mounts.
   - If neither tools nor images changed, prints that the VM is current (`vm_current`) and exits `0`.

### Flags

<ParamField body="--force" type="boolean" default="false">
  (`-f`) Remake the VM without prompting for confirmation. Required when running non-interactively or with `--no-input` if an image or `devcontainer.json` change requires a remake. Does not bypass the parent-VM confirmation prompt when run from a subfolder.
</ParamField>

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

<ParamField body="--clean-home" type="boolean" default="false">
  Start the remade VM with a fresh home directory from the image instead of copying the existing VM's home directory.
</ParamField>

---

## `devvm rm`

Deletes the current folder's VM, or deletes a named VM whose project folder no longer exists on the host.

```bash
devvm rm [VM] [--force] [--no-export] [--no-clone-check] [--here | --parent]
```

### Target modes and safeguards

- **Current folder mode (`devvm rm`)**:
  - Acquires the creation lock (`holdForRm`) so it never deletes a VM that another `devvm` process is actively creating.
  - Prompts for confirmation (`[y/N]`), which `--force` answers for the current folder's own VM.
  - **Non-overridable prompts**: If `devvm rm` resolves to a parent folder's VM from a subdirectory (`t.sub != ""`), or if the VM at this path was created for an earlier deleted folder with a different inode/FolderID (`t.other`), `devvm` always prompts interactively and `--force` does not bypass the prompt.
  - **Clone check**: If the VM is a `--clone` VM, inspects the guest clone for uncommitted changes and commits not yet fetched into the host repository, offering to save them to `saved/devvm/<branch>` on the host before deletion unless `--no-clone-check` is passed.
  - **Agent history export**: If the VM has coding agents installed and finished creation, starts the VM (if stopped) and exports agent transcripts/sessions to `~/.local/share/devvm/exports/` unless `--no-export` is set. If the export fails, the VM is kept (`export_failed_kept`); if the export exceeds `2 GiB` (`historyMax`), `devvm` warns and proceeds without saving the oversized archive.
  - **Teardown**: Deletes the backend VM instance, state directory (`~/.local/share/devvm/vms/<hash>/`), proxy, port forwarders, link sessions, `.git` protection locks or `devvm` git remote, inbound links from other VMs, pinned SSH host keys, and SSH config entry.
- **Named VM mode (`devvm rm VM`)**:
  - Accepts a VM name (e.g., `devvm-web-71a99b1076d92c`) or SSH alias (e.g., `devvm-web`). Cannot be combined with `--here` or `--parent` (`rm_vm_folder_flag`).
  - If `VM` is the current directory's own VM, behaves identically to `devvm rm`.
  - If `VM`'s recorded project folder still exists (`FolderThere`), refuses (`rm_vm_folder_there`) and instructs you to run `devvm rm` inside that folder.
  - If `VM`'s folder is on an unmounted external volume (`FolderNotConnected`), prompts interactively; `--force` never answers this prompt (`rm_vm_not_connected`).
  - If `VM` is a `--clone` VM whose host repo is gone, requires `--no-clone-check` (`rm_gone_clone`).
  - If `VM` has coding agents and is stopped, refuses unless `--no-export` is given (`rm_gone_stopped`), because starting a stopped folder-mounted VM would cause the backend to recreate the missing host folder.

### Flags

<ParamField body="--force" type="boolean" default="false">
  (`-f`) Skip confirmation when deleting the current folder's own VM or a named VM whose folder is permanently gone (`FolderGone`). Never skips confirmation for a parent folder's VM, an earlier folder's VM at the same path, or a VM whose folder is on a disconnected disk (`FolderNotConnected`).
</ParamField>

<ParamField body="--no-export" type="boolean" default="false">
  Do not export coding agents' history before deleting the VM. Required when deleting a stopped named VM (`devvm rm VM`) that has coding agents and a missing host folder.
</ParamField>

<ParamField body="--no-clone-check" type="boolean" default="false">
  Do not check a `--clone` VM for uncommitted changes or unfetched branches before deletion (unfetched work is permanently lost). Required when deleting a `--clone` VM whose host repository is gone.
</ParamField>

### Examples

```bash
# Delete this folder's VM interactively
devvm rm

# Delete this folder's VM in a script without prompting
devvm rm --force

# Delete a stopped VM whose project folder was deleted, skipping history export
devvm rm devvm-web-71a99b1076d92c --no-export --force
```

---

## `devvm adopt`

Associates the current folder with a VM that was created for an earlier folder at the same path, a folder that was moved or renamed, or a named VM whose original folder is gone (such as a folder copied to another disk).

```bash
devvm adopt [VM] [--force]
```

### Behavior

`devvm adopt` always acts on the current directory (`own: true`) and does not accept `--here` or `--parent`.

1. **Without `VM` (`devvm adopt`)**:
   - Checks if a VM's folder was moved or renamed to the current path on the same filesystem (`moveHere` matches `st.Folder == platform.FolderID(dir)` when the old path is gone or is a symlink to the new path). If found, stops the VM if running, updates `st.Dir`, records the path transition in `movedSection` so agent state paths update on next launch, and preserves `.git` protection state.
   - Otherwise, if a VM exists for the current path's hash (`project.Hash(dir)`) but its recorded `FolderID` belongs to an earlier deleted directory (`t.other`), prompts to adopt the VM (`[y/N]`, answered by `--force`), updates `st.Folder` to the new directory's ID, and resets `.git` protection state (`gitprotect.Made{}`) so `.git` is locked fresh on next launch.
   - If the current folder already owns the VM, prints `==> <vm> is already this folder's VM` (`vm_already_here`) and exits `0`.
2. **With `VM` (`devvm adopt VM`)**:
   - Looks up `VM` by full name or SSH alias.
   - Refuses if `VM` is a `--clone` VM (`adopt_clone`), if the backend lost the instance (`rm_vm_lost`), if `VM` creation is unfinished (`vm_unfinished`), if `VM`'s recorded folder still exists on disk (`adopt_folder_there`), or if the current folder already has its own VM (`adopt_has_vm`).
   - Checks that the current directory is not an unsafe folder (`$HOME` or `devvm` directories).
   - Prompts for confirmation (`[y/N]`, answered by `--force`), stops `VM` if running, updates `st.Dir` and `st.Folder` to the current folder, and resets `.git` protection state so the new folder's `.git` is locked fresh on next start.

---

## `devvm size`

Displays or updates the CPU, memory, and disk allocation for the current folder's VM, or with `-g` (`--global`) for new VMs.

```bash
devvm size [--cpus N] [--memory GiB] [--disk GiB] [-g | --here | --parent] [--json]
```

### Behavior and constraints

- **Showing size (`devvm size` with no size flags)**:
  - For a folder VM: prints active `cpus=<N> memory=<M>GiB disk=<D>GiB` to `stdout`. If changes are queued in `vm.json` for the next boot (`pendingSize`), emits a note on `stderr` showing the pending values.
  - With `-g`: prints three lines (`cpus=`, `memory=`, `disk=`) with `# <source>` comments indicating whether each default comes from `~/.config/devvm/config.toml`, `devvm: settings.json`, or `devvm: default` (image default).
- **Changing size (`--cpus`, `--memory`, `--disk`)**:
  - Validates against host capacity (`ThisHost`): `--cpus` cannot exceed host logical CPUs (`too_many_cpus`), and `--memory` cannot exceed `backend.MaxMemory` (`too_much_memory`).
  - **Disk growth only**: For an existing VM, `--disk` must be greater than or equal to the VM's current disk size (`disk_shrink`); for `-g`, `--disk` must be at least `backend.BaseDiskGiB`.
  - For a running VM, size changes take effect on next start; `devvm` prompts in the terminal (`Restart <vm> now? [Y/n]`) and runs `devvm restart` immediately if confirmed, or warns that the new size applies on next start.
  - With `-g`, writes to `~/.local/share/devvm/settings.json`. Fails with `set_in_config` if the target key (`size.cpus`, `size.memory`, or `size.disk`) is pinned in `~/.config/devvm/config.toml`.
  - `--json` cannot be combined with `--cpus`, `--memory`, or `--disk` (`showsOnly` exits with code `2`).

### Flags

<ParamField body="--cpus" type="string">
  Whole number `N` of CPUs (`>= 1`).
</ParamField>

<ParamField body="--memory" type="string">
  Memory in `GiB`, such as `16` or `16G` (`>= 0.5`).
</ParamField>

<ParamField body="--disk" type="string">
  Disk size in `GiB`, such as `200` or `200G` (cannot shrink an existing disk).
</ParamField>

<ParamField body="--global" type="boolean" default="false">
  (`-g`) Show or change default size settings for new VMs in `settings.json` instead of this folder's VM. Mutually exclusive with `--here` and `--parent`.
</ParamField>

<ParamField body="--json" type="boolean" default="false">
  Print the current or global size as a version 1 JSON object. Cannot be combined with `--cpus`, `--memory`, or `--disk`.
</ParamField>

### Output examples

<CodeGroup>
```bash title="Folder VM and global text output"
devvm size
# cpus=4 memory=8GiB disk=100GiB

devvm size -g
# cpus= # devvm: default
# memory=16 # /Users/alex/.config/devvm/config.toml
# disk=100 # devvm: settings.json
```

```json title="devvm size --json (with pending restart changes)"
{
  "version": 1,
  "cpus": 4,
  "memory_gib": 8,
  "disk_gib": 100,
  "pending": {
    "cpus": 6,
    "memory_gib": 16
  }
}
```

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

---

## `devvm ports`

Displays or updates which TCP ports the current folder's VM forwards from the guest to `127.0.0.1` on the host, or with `-g` (`--global`) what new VMs forward by default. Changes to a running VM restart the host SSH port forwarder (`vmssh.Forwards.Start`) immediately without restarting the VM.

```bash
devvm ports [all|none|LIST] [-g | --here | --parent] [--json]
devvm ports add LIST [-g | --here | --parent]
devvm ports rm LIST [-g | --here | --parent]
```

### Subcommands and arguments

| Command | Argument | Behavior |
| --- | --- | --- |
| `devvm ports` | None | Prints `all`, `none`, or the normalized comma-separated list of ports/ranges (e.g., `5173,8000-8010`). With `-g`, appends `# <source>`. |
| `devvm ports [all\|none\|LIST]` | `all`, `none`, or `LIST` | Replaces the entire port forwarding configuration at once. Cannot be combined with `--json`. |
| `devvm ports add LIST` | `LIST` (e.g., `5432,8000-8010`) | Merges ports or ranges into the current list. Rejects `all` or `none` as `LIST` (`ports_list_needed`). If the VM already forwards `all`, warns (`ports_all_already`) and keeps `all`. |
| `devvm ports rm LIST` | `LIST` (e.g., `5173`) | Removes exact matching port or range entries from the current list; transitions to `none` when the last entry is removed. Fails if the VM currently forwards `all` (`ports_rm_all`) or if an entry in `LIST` is not present as listed (`port_not_listed`). |

<CodeGroup>
```bash title="Examples"
# Show forwarded ports for this folder's VM
devvm ports

# Restrict forwarding to specific ports and ranges immediately
devvm ports 5173,8000-8010

# Add PostgreSQL port forwarding while the VM runs
devvm ports add 5432

# Change the default for new VMs to forward no ports
devvm ports -g none
```

```json title="devvm ports --json & devvm ports -g --json"
// devvm ports --json
{
  "version": 1,
  "ports": "5173,8000-8010"
}

// devvm ports -g --json
{
  "version": 1,
  "ports": "all",
  "source": "built-in"
}
```
</CodeGroup>

---

## `devvm host`, `devvm code`, and `devvm desktop`

Prepare the current folder's VM (starting it if stopped, syncing guest scripts, and writing `~/.local/share/devvm/ssh/config`) and connect external tools over SSH or VNC.

| Command | Syntax | Description |
| --- | --- | --- |
| `devvm host` | `devvm host [--ssh-config] [--here \| --parent]` | Starts the VM if stopped, ensures `~/.ssh/config` includes `devvm`'s SSH config (prompting unless `--ssh-config` is passed), and prints the SSH host alias (e.g., `devvm-website`) and guest working directory (`shellDir`). |
| `devvm code` | `devvm code [--ssh-config] [--here \| --parent]` | Runs the same preparation as `devvm host`, locates the `code` binary on `$PATH` (failing with `no_code_command` if missing), and replaces the `devvm` process with `code --remote ssh-remote+<host> <shellDir>`. |
| `devvm desktop` | `devvm desktop [--clipboard] [--no-open] [--here \| --parent]` | Starts the VM, verifies the `desktop` layer is installed (`/usr/local/bin/devvm-desktop`), starts the guest desktop service, and runs a host VNC relay on `127.0.0.1:<port>` until `Ctrl+C` or VM shutdown. |

### `devvm desktop` relay architecture and flags

`devvm desktop` requires a VM built with the `desktop` layer (`devvm --layers desktop` or `devvm layer add desktop`). It never exposes the guest VNC socket directly on the network:

1. **Per-VM password**: Generates a random password on first run (`desktop.NewPassword`), stores it in the `desktop` section of `vm.json` on the host, and validates VNC client handshakes (`15s` timeout, `1s` failure delay) in the host relay (`internal/desktop.Relay`) before opening a channel to the VM.
2. **Isolated SSH socket channel**: Upon successful password verification, the host relay dials the guest Unix socket (`/run/devvm-desktop.sock`) via `ssh -W` using `BatchMode=yes`, `IdentityAgent=none`, `ForwardAgent=no`, and `ClearAllForwardings=yes`.
3. **Sticky loopback port**: Allocates and persists a dedicated loopback port (`127.0.0.1:<port>`) in `vm.json` (`ports.Host.Desktop`) so VNC viewers reconnect to the same address across runs.
4. **Health polling**: Polls backend status every `5s` (`desktopEvery`) and terminates cleanly if the VM stops.

<ParamField body="--clipboard" type="boolean" default="false">
  Share the clipboard bidirectionally between the host and the VM desktop. By default (`false`), the host VNC relay strips `ClientCutText` and `ServerCutText` messages and starts `devvm-desktop` with clipboard sync `off`.
</ParamField>

<ParamField body="--no-open" type="boolean" default="false">
  Print the `127.0.0.1:<port>` address and VNC password without automatically launching macOS Screen Sharing (`vnc://127.0.0.1:<port>`).
</ParamField>

---

## `devvm history`

Lists, exports, and imports tarballs of coding agents' session transcripts and history without including agent credentials or login tokens.

```bash
devvm history [--json]
devvm history export [FILE] [--here | --parent]
devvm history import [FILE] [--here | --parent]
```

### Storage layout and behavior

Exports managed by `devvm` live in `~/.local/share/devvm/exports/` (permissions `0700`) with filenames matching `<vm>-YYYYMMDD-HHMMSS.tar.gz` (in host local time, mode `0600`) paired with a sidecar metadata file `<export>.folder` containing two lines: the host project directory path and the VM's network tier at export time.

- **`devvm history`**:
  - Lists saved exports in `~/.local/share/devvm/exports/` sorted oldest-first (`FILE`, `VM`, `FOLDER`, `DATE`, `SIZE`). Starts no VM and modifies no state.
- **`devvm history export [FILE]`**:
  - Starts the folder's VM if stopped (`ensureRunning`).
  - Streams a `.tar.gz` of the guest's agent history directories (excluding login/credential files) to a temporary file `.devvm-export-*` capped at `2 GiB` (`historyMax`), then atomically renames it to `FILE` (if specified) or to `~/.local/share/devvm/exports/<vm>-<timestamp>.tar.gz` along with its `.folder` sidecar.
- **`devvm history import [FILE]`**:
  - Selects `FILE` if given, or locates the newest export in `~/.local/share/devvm/exports/` whose `.folder` sidecar matches the current VM's folder (`Exports.Newest`). Fails with `history_no_export` if none exists.
  - **Network tier downgrade warning (`warnTier`)**: Compares the export's recorded source network tier against the target VM's tier (`netpolicy.Tier.AtLeast`). If the export came from a less strict tier (e.g., `open` or `internet` imported into an `allowlist` or `none` VM) or from an external `FILE` without a trusted sidecar, warns on `stderr` before importing.
  - Starts the VM if stopped and streams the tarball into guest `tar` extraction without reading the archive on the host, preserving existing files in the VM and rejecting unsafe archive paths (`historyUnsafe`).

<CodeGroup>
```bash title="Export and import commands"
# List all saved agent history exports
devvm history

# Export this folder's VM agent history to the default exports store
devvm history export

# Export to a custom archive path
devvm history export ~/backups/api-agents.tar.gz

# Import the newest export saved for this folder's VM
devvm history import
```

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

## Related pages

<CardGroup cols={2}>
  <Card title="VM lifecycle and state" href="/vm-lifecycle-and-state">
    How project paths map to VM names and 8-hex hashes, state persistence in vm.json, layout version checks, and in-place vs. remake transitions.
  </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>
  <Card title="Connect SSH, VS Code, and VNC desktop" href="/ssh-editors-and-desktop">
    Use generated ~/.ssh/config entries, configure per-VM SSH agents, launch VS Code Remote-SSH via devvm code, and connect to the VNC desktop relay.
  </Card>
  <Card title="Isolate work with repository clones" href="/working-with-repo-clones">
    Create VMs with independent Git clones using devvm --clone, switch existing VMs between mounted folders and clones, and fetch VM branches to the host.
  </Card>
</CardGroup>
