# Connect SSH, VS Code, and VNC 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.

- 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/code.go`
- `internal/cli/desktop.go`
- `internal/cli/sshagent.go`
- `internal/sshconfig/sshconfig.go`
- `internal/sshagent/sshagent.go`
- `internal/desktop/desktop.go`
- `internal/desktop/relay.go`

---

---
title: "Connect SSH, VS Code, and VNC desktop"
description: "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."
---

`devvm` exposes running project VMs to host SSH clients, editors, and VNC viewers over host loopback (`127.0.0.1`) while enforcing host-key pinning, per-VM SSH agent isolation, and host-side VNC authentication. On every VM launch or stop, `internal/sshconfig` rewrites `~/.local/share/devvm/ssh_config` with hardened `Host` blocks bound to the VM's current boot session. `devvm host` and `devvm code` bring the target VM up, synchronize guest configuration, ensure `~/.ssh/config` includes the generated file, and connect `ssh` or VS Code Remote-SSH. For graphical workflows in VMs built with the `desktop` layer, `devvm desktop` starts an in-VM Xfce and TigerVNC session on a Unix domain socket (`/run/devvm-desktop/vnc.sock`) and runs a host-side RFB relay (`internal/desktop.Relay`) that verifies an 8-character per-VM password and filters client clipboard events before tunneling over `ssh -W`.

```mermaid
flowchart LR
  subgraph Host["Host Machine (127.0.0.1)"]
    SSHClient["ssh / VS Code Remote-SSH"]
    SSHConfig["~/.local/share/devvm/ssh_config"]
    KnownHosts["vms/&lt;hash&gt;/run/known_hosts"]
    UserAgent["User SSH_AUTH_SOCK"]
    DevvmAgent["devvm Agent<br/>~/.local/share/devvm/agent.sock"]
    VNCViewer["Screen Sharing / VNC Viewer"]
    Relay["desktop.Relay<br/>127.0.0.1:&lt;port&gt;<br/>DES Auth + RFB Filter"]
  end

  subgraph Guest["Project VM"]
    SSHD["sshd"]
    VSCServer["~/.vscode-server/data/Machine/settings.json"]
    VNCUnix["Xvnc (:1, 1600x1000)<br/>/run/devvm-desktop/vnc.sock"]
    LocalProxy["devvm-local-proxy<br/>127.0.77.1:3128"]
  end

  SSHClient -->|reads| SSHConfig
  SSHConfig -->|verifies| KnownHosts
  SSHConfig -.->|mode: on| UserAgent
  SSHConfig -.->|mode: devvm| DevvmAgent
  SSHClient -->|127.0.0.1:&lt;ssh_port&gt;| SSHD
  SSHD --> VSCServer

  VNCViewer -->|RFB 3.3 / 3.7 / 3.8| Relay
  Relay -->|ssh -W /run/devvm-desktop/vnc.sock| SSHD
  SSHD -->|Unix domain socket| VNCUnix
  VNCUnix -->|HTTP_PROXY| LocalProxy
```

## SSH access and generated `ssh_config`

`devvm` generates SSH `Host` blocks in `~/.local/share/devvm/ssh_config` (`paths.Dirs.SSHConfig`) whenever a project VM starts, launches, or stops. Only VMs currently in `backend.StatusRunning` with usable loopback SSH parameters and pinned host keys receive an entry.

<Note>
A stopped VM's loopback port is released back to the host OS and could be claimed by another local process. `devvm` omits stopped VMs from `~/.local/share/devvm/ssh_config` so `ssh` and editors never connect or forward an SSH agent to an unverified local listener.
</Note>

### Host aliases and VM naming

Every project VM has a canonical name in the format `devvm-<folder>-<hash>`, where `<folder>` is the lowercase alphanumeric slug of the project directory and `<hash>` is a 14-hex-digit (56-bit) path digest (`vmname.HashLen = 14`).

When `devvm` creates a new VM (`creation.create`), it claims a permanent SSH alias inside `vm.json` (`state.VM.Alias`) under the global claims lock (`~/.local/share/devvm/run/claims.lock`) via `sshconfig.ChooseAlias`:

1. If no existing VM state records the short name `devvm-<folder>`, the new VM claims `devvm-<folder>`.
2. If another VM already holds `devvm-<folder>`, the new VM's alias falls back to its full name `devvm-<folder>-<hash>`.
3. Once recorded in `vm.json`, a VM's alias never changes even if the first VM is later deleted. Both the alias and the full name (`Host <alias> <full-name>`) are written to `ssh_config`.

### Including `ssh_config` in `~/.ssh/config`

To resolve `devvm-<folder>` hostnames with `ssh` or editors, `~/.ssh/config` must contain the `Include` directive before any `Host` or `Match` block:

```sshconfig title="~/.ssh/config"
Include /Users/you/.local/share/devvm/ssh_config
```

When you run `devvm host`, `devvm code`, or create a `--clone` VM, `sshconfig.Config.EnsureInclude` checks whether `~/.ssh/config` contains this line:
- In an interactive terminal, `devvm` prompts `Add it to ~/.ssh/config now? [Y/n]`.
- Passing `--ssh-config` to `devvm host` or `devvm code` prepends the `Include` line to `~/.ssh/config` (preserving symlinks if `~/.ssh/config` links into a dotfiles repository and setting `~/.ssh` mode to `0700`) without prompting.
- You can also target the generated file directly without modifying `~/.ssh/config`:

```bash
ssh -F ~/.local/share/devvm/ssh_config devvm-website
```

### Per-start host key pinning

Lima disables SSH host-key verification in its own generated configurations. Instead of trusting network-supplied host keys or inheriting Lima's permissive options, `vmssh.Checked` and `vmssh.PinHostKeys` pin each running VM's public keys out-of-band:

1. Before starting a stopped VM, `starter.start` removes any prior `~/.local/share/devvm/vms/<hash>/run/known_hosts` via `vmssh.ForgetHostKeys`.
2. Once the VM is running, `vmssh.PinHostKeys` queries the backend's current boot identifier (`backend.Run.ID`) and executes `cat /etc/ssh/ssh_host_*_key.pub` inside the VM through the backend execution channel (capped at 64 KiB with a 30-second timeout).
3. `vmssh.ParseHostKeys` decodes each base64 key blob, verifies that the wire-format key header matches the line's key type (`^[A-Za-z0-9][A-Za-z0-9@._+-]{0,63}$`), rejects SSH certificates (`-cert-`), and drops `@cert-authority` or `@revoked` markers and host patterns.
4. `PinHostKeys` re-verifies that `backend.Run.ID` did not change during the read, then writes `~/.local/share/devvm/vms/<hash>/run/known_hosts` starting with `# devvm: host keys of start <run-id>`.

### Generated `Host` block directives

For each running VM, `sshconfig.Config.entry` writes a hardened block that overrides any broad `Host *` settings in your personal `~/.ssh/config`:

```sshconfig title="~/.local/share/devvm/ssh_config"
# Written by devvm on every launch; edits are overwritten.

Host devvm-website devvm-website-0123456789abcd
  HostKeyAlias devvm-website-0123456789abcd
  UserKnownHostsFile "/Users/you/.local/share/devvm/vms/0123456789abcd/run/known_hosts"
  StrictHostKeyChecking yes
  NoHostAuthenticationForLocalhost no
  ForwardAgent no
  ControlMaster no
  ControlPath none
  ForwardX11 no
  ForwardX11Trusted no
  Tunnel no
  PermitRemoteOpen none
  AddKeysToAgent no
  ProxyJump none
  ProxyCommand none
  UpdateHostKeys no
  HashKnownHosts no
  Hostname 127.0.0.1
  Port 54321
  User devvm
  IdentityFile "/Users/you/.local/share/devvm/lima/_config/user"
```

| Directive | Enforced Value | Security and Operational Purpose |
| --- | --- | --- |
| `HostKeyAlias` & `UserKnownHostsFile` | `<vm-name>` & `.../run/known_hosts` | Binds host key verification to the VM's per-start pinned key file instead of `~/.ssh/known_hosts`. |
| `StrictHostKeyChecking` & `NoHostAuthenticationForLocalhost` | `yes` & `no` | Forces strict verification even though `Hostname` is `127.0.0.1`. |
| `ForwardAgent` & `IdentityAgent` | `no`, `yes`, or `"<agent.sock>"` | Locks agent forwarding to the mode the VM's current start was launched with (`off`, `on`, or `devvm`). |
| `ControlMaster` & `ControlPath` | `no` & `none` | Prevents connection multiplexing so a session never inherits the forwarded agent of an earlier connection. |
| `ForwardX11` & `ForwardX11Trusted` | `no` | Blocks guest processes from accessing the host X11 display server. |
| `Tunnel` & `PermitRemoteOpen` | `no` & `none` | Disables layer-2/3 tun/tap devices and remote SOCKS/port-open requests (requires OpenSSH 8.7+). |
| `AddKeysToAgent`, `ProxyJump`, `ProxyCommand` | `no` / `none` | Prevents `Host *` jump hosts, proxy commands, or key auto-loading from applying to VM connections. |

<Warning>
OpenSSH applies the first value it sees for most options, but `RemoteForward` directives accumulate unless `ClearAllForwardings yes` is set (which would also break explicit `ssh -R` flags on the command line). Do not place `RemoteForward` under `Host *` in `~/.ssh/config`. Inside the VM, `sshd` restricts remote forwards to `127.77.0.0/16` (used for inter-VM links) and refuses remote forwards on `localhost`.
</Warning>

---

## Prepare a VM and open VS Code (`devvm host` and `devvm code`)

Both `devvm host` and `devvm code` call `Launch.reach` (`internal/vm/ssh.go`), which executes the full existing-VM startup and setup pipeline (`readyConf`) without opening an interactive terminal shell:

<Steps>
  <Step title="Locate and verify the target VM">
    Finds the made VM for the current directory (or parent project directory), verifies the backend supports `backend.FeatureSSH`, checks `.git` protection and host safety, and starts the VM if stopped.
  </Step>
  <Step title="Verify confinement and sync guest state">
    Runs `Proxy.SelfCheck` to ensure the VM's network tier holds, executes `sync.sh` inside the VM to mount shared tools and update guest settings, pins the VM's SSH host keys, and rewrites `~/.local/share/devvm/ssh_config`.
  </Step>
  <Step title="Ensure ~/.ssh/config inclusion">
    Calls `EnsureInclude` to confirm `~/.ssh/config` includes `~/.local/share/devvm/ssh_config` (prompting if interactive, or adding immediately with `--ssh-config`).
  </Step>
  <Step title="Print host info or exec VS Code">
    `devvm host` prints the resolved SSH host alias and target directory inside the VM. `devvm code` locates `code` on `PATH` and replaces the `devvm` process via `exec` with `code --remote ssh-remote+<host> <shellDir>`.
  </Step>
</Steps>

### Command reference: `devvm host` and `devvm code`

<CodeGroup>
```bash title="Prepare VM and print SSH host alias"
devvm host
devvm host --ssh-config
```

```bash title="Open VM in VS Code over Remote-SSH"
devvm code
devvm code --ssh-config
```
</CodeGroup>

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

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

<ParamField body="--parent" type="boolean" default="false">
  Target the nearest parent directory's VM, even across a nested Git repository boundary. Mutually exclusive with `--here`.
</ParamField>

When `devvm code` invokes `code --remote ssh-remote+<host> <dir>`, it passes `e.Environ` (the environment `devvm` was started with) so VS Code uses the host's environment rather than internal agent overrides. The target directory `<dir>` (`target.shellDir`) resolves to:
- The project root `f.Dir` when run at the project root.
- The current subdirectory `t.sub` when run inside a subdirectory of a parent VM's project.
- For a `--clone` VM run from a host subdirectory, `t.sub` if that subdirectory exists inside the guest clone, or `f.Dir` otherwise.

If `code` is not on your host `PATH`, `devvm code` exits with an error naming the SSH host alias (for example, `devvm-website`) and instructs you to run *Shell Command: Install 'code' command in PATH* in VS Code or connect manually via *Remote-SSH: Connect to Host*.

### VS Code Remote-SSH guest hardening

By default, VS Code Remote-SSH automatically forwards ports opened by processes in its integrated terminal to the host's `localhost` (bypassing `devvm ports` rules) and answers `git` credential prompts in the VM using the host's GitHub sign-in.

On every launch, `assets/guest/sync.sh` updates `~/.vscode-server/data/Machine/settings.json` inside the VM using `jq` to disable these behaviors while preserving other machine settings:

```json title="~/.vscode-server/data/Machine/settings.json (in VM)"
{
  "remote.autoForwardPorts": false,
  "git.terminalAuthentication": false,
  "github.gitAuthentication": false
}
```

<Note>
If `~/.vscode-server/data/Machine/settings.json` contains comments (invalid strict JSON), `sync.sh` leaves the file untouched and writes a notice to stderr in the run log. Because root or the user inside the VM can modify `~/.vscode-server/data/Machine/settings.json` after launch, set those three keys to `false` in your host VS Code user settings when working with untrusted code.
</Note>

---

## Per-VM SSH agent forwarding (`devvm ssh-agent`)

SSH agent forwarding is `off` by default for every VM. When enabled, any process inside the VM—including coding agents—can request signatures from the forwarded agent while you are connected, though private key material never enters the VM.

`devvm` supports three per-VM forwarding modes (`internal/sshagent`):

| Mode | Forwarded Socket | Behavior |
| --- | --- | --- |
| `off` | None (`ForwardAgent no`) | Default. No SSH agent is exposed to the VM in `devvm` shells or `ssh_config`. Git over SSH fails inside the VM. |
| `on` | Host `$SSH_AUTH_SOCK` (`ForwardAgent yes`) | Forwards your host SSH agent. If `$SSH_AUTH_SOCK` holds no keys when launching and `~/.ssh` contains `id_ed25519`, `id_ecdsa`, or `id_rsa`, `devvm` offers to run `ssh-add` (`ssh-add --apple-use-keychain` on macOS). |
| `devvm` | `~/.local/share/devvm/agent.sock` | Forwards `devvm`'s dedicated, isolated `ssh-agent` process holding only the keys explicitly added via `devvm ssh-agent add`. |

### Configuring a VM's agent mode

You can enable your personal agent at VM creation time with `devvm --ssh-agent`, or inspect and change the mode on an existing VM with `devvm ssh-agent`:

```bash
# Show the current VM's forwarding mode and keys in devvm's own agent
devvm ssh-agent

# Forward devvm's isolated agent (prompts to restart if the VM is running)
devvm ssh-agent devvm

# Forward your host SSH_AUTH_SOCK
devvm ssh-agent on

# Disable SSH agent forwarding
devvm ssh-agent off
```

#### Next-start consistency (`sshagent.Running`)

A VM's backend agent socket mount is configured when the VM boots. `starter.start` records the active mode and a 128-bit SHA-256 prefix of the backend boot ID (`<mode>_<startHash>`) in `vm.json` under `agent.made.started`.

Both `devvm`'s own shells and `sshconfig.Config.entry` evaluate `sshagent.Running(st, start)` against the running VM's boot ID:
- If you run `devvm ssh-agent on`, `devvm`, or `off` while the VM is running, `devvm` prompts to restart the VM immediately (`Restart <vm> now to apply this change? [Y/n]`).
- If you decline (or run non-interactively), the running VM continues using the mode it booted with across both `devvm` shells and `ssh`/`code` connections until the next `devvm restart` or boot.
- When `devvm ssh-agent on` or `devvm` is run from a subdirectory targeting a parent directory's VM, `target.widen` requires explicit `[y/N]` confirmation before widening access on the parent VM.

### Managing keys in `devvm`'s isolated agent

`devvm`'s dedicated SSH agent runs at `~/.local/share/devvm/agent.sock` (`paths.Dirs.AgentSock`) and tracks registered keys in `~/.local/share/devvm/agent-keys.json` (`paths.Dirs.AgentKeys`). When a VM configured with `devvm ssh-agent devvm` writes its `ssh_config` block, `devvm` sets both `ForwardAgent` and `IdentityAgent` to `"/Users/you/.local/share/devvm/agent.sock"`, ensuring GUI editors like VS Code forward `devvm`'s agent regardless of the environment variables the editor started with.

```bash
# Load a deploy key with the default 12h lifetime
devvm ssh-agent add ~/.ssh/deploy_key

# Load a key with a custom lifetime and per-use ssh-askpass confirmation
devvm ssh-agent add ~/.ssh/deploy_key --lifetime 30m --confirm

# Remove a key from devvm's agent and ~/.local/share/devvm/agent-keys.json
devvm ssh-agent rm ~/.ssh/deploy_key
```

<ParamField body="KEY_FILE" type="string" required>
  Path to the private key file to add or remove. Resolved to an absolute path with parent symlinks evaluated (`paths.PhysicalParent`).
</ParamField>

<ParamField body="--lifetime" type="string" default="12h">
  Duration the agent retains the key in memory before dropping it (`30m`, `8h`, `1d`, `2w`, or `forever`). Validated by `sshagent.CheckLifetime` and passed to `ssh-add -t`.
</ParamField>

<ParamField body="--confirm" type="boolean" default="false">
  Require confirmation via an `ssh-askpass` program before each signature (`ssh-add -c`). Requires `$SSH_ASKPASS` to be set or `ssh-askpass` to exist on `PATH`.
</ParamField>

#### Key security checks (`Agent.Add`, `Agent.Ensure`, and `Agent.load`)

Whenever `devvm ssh-agent add` loads a key or `Agent.Ensure` reloads expired keys on VM launch, `internal/sshagent` enforces three invariants:

1. **No keys inside VM mounts (`Agent.mountedBy`):** `devvm` resolves the physical path (`paths.Physical`) of the key file and compares it against every directory mounted by any existing project VM (project folders, agent config directories, and shared tools). If the key resides inside a mounted folder, `devvm` refuses to add or load it because the VM could read the private key file directly.
2. **Atomic copy and fingerprint pinning (`Agent.load`):** `devvm` reads the private key into an isolated temporary file (`0600`) with no `.pub` file alongside it, derives the `SHA256:` fingerprint directly from the private key via `ssh-keygen -lf`, and verifies that it matches the `fingerprint` recorded in `agent-keys.json` when the key was added. If the file was replaced or a symlink was retargeted to a different key, `devvm` refuses to load it (`AgentKeyChanged`). (Old PEM-encrypted keys whose public fingerprint cannot be extracted without a passphrase must be converted first with `ssh-keygen -p -f KEY`).
3. **Fingerprint-based removal (`Agent.unload`):** `devvm ssh-agent rm` matches the recorded fingerprint against `ssh-add -L` output and removes the key from the running agent via `ssh-add -d` even if the original key file on disk was modified or deleted.

### JSON output (`devvm ssh-agent --json`)

Running `devvm ssh-agent --json` (without positional arguments) ensures any registered keys are loaded into `devvm`'s agent and returns a version 1 JSON object:

<RequestExample>
```bash title="Command"
devvm ssh-agent --json
```
</RequestExample>

<ResponseExample>
```json title="Output"
{
  "version": 1,
  "mode": "devvm",
  "keys": [
    {
      "file": "/Users/you/.ssh/deploy_key",
      "fingerprint": "SHA256:mVPwvezndPv/ARoIadVY98vAC0g+P/5633yTC4d/wXE",
      "confirm": false,
      "lifetime": "12h",
      "loaded": true
    }
  ]
}
```
</ResponseExample>

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

<ResponseField name="mode" type="string" required>
  The folder's VM forwarding mode configured in `vm.json`: `"off"`, `"on"`, or `"devvm"`.
</ResponseField>

<ResponseField name="keys" type="array" required>
  Keys registered in `~/.local/share/devvm/agent-keys.json` (`[]` when none are added).
  - `file` (`string`): Resolved absolute path to the private key file.
  - `fingerprint` (`string`, optional): Pinned `SHA256:...` fingerprint recorded when the key was added.
  - `confirm` (`boolean`): Whether `ssh-add -c` confirmation is enabled.
  - `lifetime` (`string`): Configured key lifetime (such as `"12h"` or `"forever"`).
  - `loaded` (`boolean`): Whether `devvm`'s running `ssh-agent` currently holds the key in memory.
</ResponseField>

---

## VNC desktop and host relay (`devvm desktop`)

Project VMs built with the built-in `desktop` layer include an Xfce desktop environment and TigerVNC (`Xvnc`). `devvm desktop` starts the guest desktop on a Unix domain socket inside the VM and exposes it on a dedicated `127.0.0.1` port on the host through `internal/desktop.Relay`.

### Adding the `desktop` layer

The `desktop` layer (~370 MB on disk) is opt-in so base VMs stay minimal. Add it when creating a new VM, set it as a default for new VMs, or remake an existing VM with it:

```bash
# Create a new VM with the desktop layer
devvm --layers desktop

# Set desktop as a default layer for future VMs
devvm setup --layers desktop

# Remake an existing VM to add or remove the desktop layer
devvm layer add desktop
devvm layer rm desktop
```

### Launching `devvm desktop`

```bash
# Start the VM's desktop, serve on 127.0.0.1, and open macOS Screen Sharing
devvm desktop

# Share the clipboard bidirectionally for this session
devvm desktop --clipboard

# Print the vnc://127.0.0.1:<port> address and password without launching Screen Sharing
devvm desktop --no-open
```

<ParamField body="--clipboard" type="boolean" default="false">
  Enable bidirectional clipboard sharing between the host VNC viewer and the VM for the current `devvm desktop` run. Without this flag, `desktop.Relay` strips all `ClientCutText` messages from the viewer and `devvm-desktop` disables `Xvnc` clipboard options.
</ParamField>

<ParamField body="--no-open" type="boolean" default="false">
  Print the loopback VNC URL and password without automatically opening macOS Screen Sharing.
</ParamField>

When run, `devvm desktop` prints the persistent loopback endpoint and 8-character password:

```text
devvm-website-0123456789abcd's desktop is at
vnc://127.0.0.1:20145
Password: k3mxq7tw
It stays there until you press Ctrl+C.
```

On macOS (unless `--no-open` is passed), `devvm` opens `vnc://127.0.0.1:<port>` in Screen Sharing. On Linux, connect any RFB 3.3/3.7/3.8 VNC client (such as TigerVNC `vncviewer` or Remmina) to `127.0.0.1:<port>`. The relay runs until you press `Ctrl+C` or the VM stops (polled every 5 seconds via `b.Inspect`). Stopping `devvm desktop` closes the host listener while leaving the Xfce session running inside the VM.

### Desktop relay and guest architecture

```mermaid
sequenceDiagram
  autonumber
  participant V as VNC Viewer (Host)
  participant R as desktop.Relay (Host 127.0.0.1:<port>)
  participant S as ssh -W Channel (Host -> VM)
  participant X as Xvnc (/run/devvm-desktop/vnc.sock)

  V->>R: TCP connect (max 8 concurrent connections)
  R->>V: "RFB 003.008\n" + SecurityType 2 (VNC Auth) + 16-byte challenge
  V->>R: 16-byte DES-encrypted challenge response
  Note over R: Mutex-serialized constant-time check<br/>against vm.json desktop.made.password<br/>(1s FailDelay on mismatch)
  alt Wrong password
    R->>V: SecurityResult = 1 (Failed) & close
  else Valid password
    R->>S: Spawn ssh -F ssh_config -W /run/devvm-desktop/vnc.sock <vm>
    S->>X: Connect to Unix socket (0600)
    X->>R: "RFB 003.008\n" + SecurityType 1 (None)
    R->>X: Select SecurityType 1 (None)
    X->>R: SecurityResult = 0 (OK)
    R->>V: SecurityResult = 0 (OK)
    par Viewer to VM
      V->>R: ClientInit + RFB client messages
      Note over R: If !Clipboard: filter() drops ClientCutText (type 6)<br/>and disconnects on unknown RFB message types
      R->>X: Forwarded RFB messages
    and VM to Viewer
      X->>R: ServerInit + FramebufferUpdates
      R->>V: Unmodified server stream
    end
  end
```

#### 1. Guest `Xvnc` and `devvm-desktop` (`/usr/local/bin/devvm-desktop`)

In `assets/layers/desktop.sh`, `devvm-desktop start on|off` launches TigerVNC `Xvnc` on display `:1` (`1600x1000`, 24-bit depth, `-AlwaysShared`) inside `/run/devvm-desktop` (mode `0700`, owned by the VM user):
- **Unix socket only:** `Xvnc` listens exclusively on `/run/devvm-desktop/vnc.sock` (`-rfbunixpath /run/devvm-desktop/vnc.sock -rfbunixmode 0600 -rfbport -1 -nolisten tcp`). Because it binds no TCP port, `devvm`'s automatic port forwarder never exposes it even when a VM forwards all ports, and no X11 TCP connections are accepted (`ForwardX11 no` is also enforced in `ssh_config` and `sshd_config`).
- **In-place clipboard toggling:** When `devvm-desktop start on|off` runs against an already-running `Xvnc` process, it updates `AcceptCutText`, `SendCutText`, `SetPrimary`, and `SendPrimary` in place using `vncconfig -set`.
- **Guest log:** Output from `Xvnc` and `dbus-launch --exit-with-session startxfce4` is written to `~/.cache/devvm-desktop.log` inside the VM.

#### 2. Guest `devvm-local-proxy` for desktop browsers (`127.0.77.1:3128`)

Every request to a VM's host proxy requires the VM's proxy password in `Proxy-Authorization`, which `devvm` provides in `HTTP_PROXY=http://devvm:KEY@192.168.5.2:PORT`. Because Chromium, Chrome, and Firefox ignore credentials embedded in proxy environment variables and prompt interactively on every site, `sync.sh` installs `/usr/local/bin/devvm-local-proxy` (mode `0700`):
- When `devvm-desktop` runs, it invokes `devvm-local-proxy start`, which starts a `socat` listener on `127.0.77.1:3128` (`proxy.LocalProxyAddr:proxy.LocalProxyPort`, restricted to `range=127.0.0.0/8`, `max-children=256`) and exports `http_proxy` and `https_proxy` as `http://127.0.77.1:3128` for the Xfce session.
- `devvm-local-proxy` accepts only `CONNECT host:port` tunnels or absolute `http://` / `https://` proxy requests (up to 100 headers, 30s timeout per line), strips any client-supplied `Proxy-Authorization`, injects the VM's `Proxy-Authorization: Basic ...` header, and forwards traffic to the host proxy. Origin-form requests (`GET /`) receive `400 Bad Request` so web pages in the browser cannot talk to the local proxy as a web server.
- `devvm`'s port forwarder never forwards listeners bound to `127.0.77.1`.

#### 3. Host loopback port allocation and password generation

- **Persistent loopback port:** `Desktop.listener` binds `127.0.0.1:<port>` using `ports.Allocator` under the claims lock and saves the port in `vm.json` under `ports.host.desktop` (`20000` up to the host OS ephemeral port boundary). If the saved port is free on later runs, `devvm desktop` reuses it so macOS Screen Sharing can match saved Keychain credentials.
- **Host-only password:** On first run, `desktopPassword` generates an 8-character password from the 31-character unambiguous alphabet `abcdefghjkmnpqrstuvwxyz23456789` (~40 bits of entropy, omitting `0`, `o`, `1`, `l`, `i`) and stores it in `vm.json` under `desktop.made.password`. The password is never sent to the VM and never written to the run log.

#### 4. Host RFB authentication, SSH socket channel, and clipboard filtering

Because root inside the VM can reconfigure `Xvnc` or replace `/run/devvm-desktop/vnc.sock` with a different server, `internal/desktop` enforces security guarantees on the host:

- **Host-side DES challenge-response (`Relay.authenticate`):** `Relay` accepts up to 8 concurrent connections (`maxConns = 8`) with a 15-second handshake timeout (`desktopTimeout`). It speaks RFB 3.3, 3.7, and 3.8 (treating Apple Screen Sharing's `3.889` as `3.8`), offers only `secVncAuth` (`2`), and verifies the 16-byte DES challenge response (`desktop.Response`, bit-reversed 8-byte key per RFC 6143 §7.2.2) in constant time under a mutex (`r.mu`). A failed attempt sleeps for `1s` (`desktopFailDelay`) while holding the mutex, limiting brute-force guessing across all connections to 1 attempt per second.
- **Isolated `ssh -W` channel (`desktopSSHArgs`):** Only after authentication succeeds does the relay spawn `ssh` to connect to `/run/devvm-desktop/vnc.sock`:
  ```text
  ssh -F <ssh_config> -T -o BatchMode=yes -o ControlMaster=no -o ControlPath=none \
    -o IdentityAgent=none -o ForwardAgent=no -o ForwardX11=no \
    -o ClearAllForwardings=yes -o PermitLocalCommand=no \
    -o ConnectTimeout=10 -o LogLevel=ERROR -W /run/devvm-desktop/vnc.sock <vm>
  ```
- **Strict RFB client message parser (`internal/desktop/filter.go`):** When `--clipboard` is not set, `filter` parses every client-to-server RFB message after `ClientInit`:
  - `msgClientCutText` (`6`), including negative-length extended clipboard payloads, is discarded before reaching the VM.
  - Known message types (`SetPixelFormat=0`, `SetEncodings=2`, `FramebufferUpdateRequest=3`, `KeyEvent=4`, `PointerEvent=5`, `ContinuousUpdates=150`, `Fence=248` with max 64-byte payload, `Xvp=250`, `SetDesktopSize=251`, `GII=253`, and `QEMU=255` subtypes `0` and `1`) are framed and forwarded.
  - Any unknown message type immediately terminates the viewer connection so unparsed client extensions cannot smuggle clipboard data into the VM.

---

## Troubleshooting

<AccordionGroup>
  <Accordion title="ssh refuses ~/.local/share/devvm/ssh_config with Bad configuration option: permitremoteopen">
    `PermitRemoteOpen none` requires OpenSSH 8.7 or newer. Check your client version with `ssh -V`. On macOS or Linux systems with an older OpenSSH binary early on `PATH`, update OpenSSH or ensure `/usr/bin/ssh` (macOS system OpenSSH) is used.
  </Accordion>

  <Accordion title="ssh: Could not resolve hostname devvm-<folder>">
    1. Verify that `~/.ssh/config` contains `Include ~/.local/share/devvm/ssh_config` at the very top of the file, above any `Host` or `Match` block. Run `devvm host --ssh-config` in the project directory to add it automatically and start the VM.
    2. Confirm the VM is running (`devvm ls`). Stopped VMs are removed from `ssh_config` until started with `devvm`, `devvm host`, or `devvm code`.
  </Accordion>

  <Accordion title="devvm ssh-agent add refuses a key or warns during launch">
    - **Key inside a mounted folder (`AgentKeyMounted`):** The key file (or symlink target) is inside a directory mounted by a project VM. Move the private key to `~/.ssh/` or another directory outside all project folders.
    - **Key fingerprint unreadable (`AgentKeyNoFingerprint`):** The key is not a valid private key or uses legacy PEM encryption that hides the public key until decrypted. Convert legacy PEM keys in place with `ssh-keygen -p -f <key_file>`.
    - **Key changed since added (`AgentKeyChanged`):** The file's current `SHA256:` fingerprint no longer matches the fingerprint pinned in `~/.local/share/devvm/agent-keys.json`. Run `devvm ssh-agent add <key_file>` again to pin the new key.
    - **`--confirm` fails (`AgentConfirmNeedsAskpass`):** Install an `ssh-askpass` binary on `PATH` or set the `SSH_ASKPASS` environment variable before using `--confirm`.
  </Accordion>

  <Accordion title="devvm desktop fails or browser in desktop cannot reach HTTPS sites">
    - **VM has no desktop (`DesktopNoLayer`):** `/usr/local/bin/devvm-desktop` is missing because the VM was created without the `desktop` layer. Run `devvm layer add desktop` to remake the VM with the layer.
    - **Xvnc failed to start (`DesktopStartFailed`):** Inspect `~/.cache/devvm-desktop.log` inside the VM for `Xvnc` or `startxfce4` errors.
    - **Browser shows ERR_TUNNEL_CONNECTION_FAILED on HTTPS URLs:** The target domain is blocked by the VM's network tier. Open a terminal inside the desktop and run `devvm-why` (or `devvm network log` on the host) to see the blocked domain and the `devvm network allow <host>` command to permit it.
  </Accordion>
</AccordionGroup>

## Related pages

<CardGroup cols={2}>
  <Card title="CLI reference: VM and workspace commands" href="/cli-vm-and-workspace-commands">
    Complete flag and exit-code reference for devvm host, devvm code, and devvm desktop.
  </Card>
  <Card title="CLI reference: network, security, and agent commands" href="/cli-security-network-and-agent-commands">
    Command syntax and subcommands for devvm ssh-agent, devvm network, and devvm secret.
  </Card>
  <Card title="Images, layers, and content keys" href="/image-and-layer-model">
    How the built-in desktop, docker, and nix layers are built, keyed, and added to VMs.
  </Card>
  <Card title="Diagnose host, boot, and SSH failures" href="/host-and-vm-diagnostics">
    Troubleshoot SSH host-key verification errors, moved project folders, and run logs.
  </Card>
</CardGroup>
