# Configure coding agents and credentials

> Run Claude Code, Codex, and opencode inside VMs, import and save agent settings, complete OAuth or API-key logins, and bind host secrets to allowed hosts.

- 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/agent.go`
- `internal/cli/agentlogin.go`
- `internal/vm/agents.go`
- `internal/vm/agentimport.go`
- `internal/vm/agentlogin.go`
- `internal/vm/agentsave.go`
- `internal/agentlogin/restore.go`
- `internal/proxy/secrets.go`

---

---
title: "Configure coding agents and credentials"
description: "Run Claude Code, Codex, and opencode inside VMs, import and save agent settings, complete OAuth or API-key logins, and bind host secrets to allowed hosts."
---

`devvm` isolates coding agents (`claude`, `codex`, and `opencode`) inside per-project virtual machines while keeping all real credentials—OAuth tokens, refresh tokens, API keys, MCP server headers, and GitHub tokens—exclusively on the host. Each VM receives read-only mounts of non-secret agent directories, redacted copies of agent configuration files, and per-VM salted placeholders in place of tokens. When a guest process makes an HTTPS request to a bound host on port `443`, the host-side proxy (`devvm _proxy`) terminates TLS using a per-VM certificate authority, substitutes the placeholder with the live credential from the host store or macOS Keychain, and forwards the request upstream.

## Supported coding agents and guest environment

Agents are packaged as read-only EROFS shared tool images mounted into VMs alongside language toolchains. Because the shared tool mounts are immutable, `devvm` disables each agent's built-in self-updater and telemetry during guest synchronization.

| Agent ID | Binary | Version track & guest overrides | Built-in policy (`shared-tools/<agent>.policy`) |
| --- | --- | --- | --- |
| `claude` | `claude` | Latest release; sets `DISABLE_AUTOUPDATER=1` and disables telemetry | `api.anthropic.com`, `claude.ai`, `downloads.claude.ai`, `platform.claude.com`, `mcp-proxy.anthropic.com` |
| `codex` | `codex` | Latest release; sets `cli_auth_credentials_store = "file"` in `~/.codex/config.toml` and disables analytics/metrics unless explicitly configured | `api.openai.com`, `chatgpt.com`, `auth.openai.com` |
| `opencode` | `opencode` | Latest `2.x` release; sets `OPENCODE_DISABLE_AUTOUPDATE=1` | `opencode.ai`, `models.opencode.ai` |

<Note>
`opencode` connects to whichever model provider you configure (`anthropic`, `openai`, `openrouter`, or custom providers). On the `allowlist` network tier, allow your model provider's API host explicitly (for example, `devvm network allow api.anthropic.com`), or bind a provider API key with `devvm agent login opencode --api-key --provider <provider>`, which automatically adds that provider's host on port `443` to `secrets.policy`.
</Note>

### Adding agents to new and existing VMs

Configure default agents for new VMs or attach agents to an existing project VM using the CLI:

```bash
# Configure default agents selected for all new VMs
devvm setup --agents claude,codex

# Create a new VM with specific agents enabled
devvm --agents claude,opencode

# Interactively select or add agents to an existing VM
devvm tools
devvm tools add claude codex opencode
```

### In-VM agent guidance

On every launch, `syncer.stage` installs `/usr/share/doc/devvm/README.md` (owned by `root`, read-only) and exposes the `devvm-help` command inside the guest. This guide documents the sandbox boundaries, network tiers, and host commands (`devvm network allow`, `devvm agent login`) that an agent should ask the user to run on the host. When a VM runs with a proxy address, guest setup also maintains a `<!-- devvm-proxy -->` block inside `~/.claude/CLAUDE.md` and `~/.codex/AGENTS.md` instructing agents to run `devvm-why` whenever a connection fails or returns `403`.

## Host agent store and VM synchronization

`devvm` never mounts or reads `~/.claude`, `~/.claude.json`, `~/.codex`, `~/.config/opencode`, or `~/.local/share/opencode` directly from your host home directory when launching a VM. Instead, all VMs read from `devvm`'s isolated host store, ensuring VMs receive identical configurations whether or not the agents are installed on the host machine, and preventing host session transcripts from leaking into guests.

```mermaid
flowchart TD
  subgraph HostHome["Host Home Directory (~)"]
    HClaude["~/.claude & ~/.claude.json"]
    HCodex["~/.codex"]
    HOpencode["~/.config/opencode & ~/.local/share/opencode"]
  end

  subgraph HostStore["Host Store & Secret Backend"]
    SettingsStore["~/.config/devvm/agents/&lt;agent&gt;/<br/>(Settings files & skill/plugin folders)"]
    LoginsStore["~/.local/share/devvm/logins/&lt;agent&gt;/<br/>(OAuth credentials & API keys, mode 0600)"]
    SecretStore["macOS Keychain / ~/.local/share/devvm/secrets/<br/>(claude, github, proxy)"]
  end

  subgraph HostProxy["Host Proxy Process (devvm _proxy)"]
    Bindings["vms/&lt;hash&gt;/run/secrets.json<br/>(BindingList: hosts, header, placeholder)"]
    Capturer["OAuth Token Capturer<br/>(auth.openai.com, platform.claude.com)"]
  end

  subgraph GuestVM["Guest VM (~)"]
    ROLinks["Symlinks in ~ to read-only mounts<br/>(skills, plugins, prompts, commands)"]
    RedactedCopies["Redacted copies & salted placeholders<br/>(settings.json, config.toml, auth.json, env)"]
    GuestHistory["Per-VM session history & SQLite DBs"]
  end

  HostHome -- "devvm agent import<br/>(excludes history)" --> SettingsStore
  HostHome -- "devvm agent import<br/>(excludes history)" --> LoginsStore
  SettingsStore -- "Read-only virtiofs / 9p mount" --> ROLinks
  SettingsStore -- "agentlogin.Redact on launch" --> RedactedCopies
  LoginsStore -- "File.VMCopy (salted placeholders)" --> RedactedCopies
  SecretStore -- "Lookup on demand (IO.Secret)" --> HostProxy
  LoginsStore -- "Lookup & OAuth refresh (File.Get)" --> HostProxy
  RedactedCopies -- "devvm agent save<br/>(diff + agentlogin.Restore)" --> SettingsStore
  GuestVM -- "POST OAuth token exchange" --> Capturer
  Capturer -- "Save real tokens" --> LoginsStore
  Capturer -- "Return salted placeholders" --> GuestVM
```

### Store directory layout

`devvm` separates non-secret agent settings from login files and host secrets across XDG config and data directories:

:::files
~/.config/devvm/
└── agents/
    ├── claude/
    │   └── .claude/
    │       ├── settings.json
    │       ├── CLAUDE.md
    │       ├── skills/
    │       ├── agents/
    │       ├── commands/
    │       ├── output-styles/
    │       └── plugins/
    ├── codex/
    │   └── .codex/
    │       ├── config.toml
    │       ├── AGENTS.md
    │       ├── prompts/
    │       └── skills/
    └── opencode/
        └── .config/opencode/
            ├── opencode.json
            ├── AGENTS.md
            └── <subfolders>/
~/.local/share/devvm/
├── logins/
│   ├── claude/
│   │   ├── .claude.json
│   │   └── .claude/.credentials.json
│   ├── codex/
│   │   └── .codex/auth.json
│   └── opencode/
│       └── .local/share/opencode/auth.json
├── secrets/
│   ├── claude
│   ├── github
│   └── proxy
└── vms/<hash>/
    └── run/
        ├── secrets.json
        ├── secrets.policy
        └── ca/
            ├── ca.pem
            ├── ca.key
            ├── next.pem
            └── next.key
:::

### Mounted directories vs. copied files

On every VM launch, `Launch.AgentMounts` and `syncer.stage` provision each enabled agent's files using two mechanisms:

| Agent | Read-only host mounts (symlinked into guest `~`) | Copied into guest `~` on every launch (secrets redacted/replaced) |
| --- | --- | --- |
| `claude` | `.claude/skills`, `.claude/agents`, `.claude/commands`, `.claude/output-styles`, `.claude/plugins` | `.claude/settings.json`, `.claude/CLAUDE.md`, and `.claude.json` (merged from `logins/claude/.claude.json`) |
| `codex` | `.codex/prompts`, `.codex/skills` | `.codex/config.toml`, `.codex/AGENTS.md`, and `.codex/auth.json` (from `logins/codex/.codex/auth.json`) |
| `opencode` | All subdirectories of `.config/opencode/` except `node_modules` | All regular files in `.config/opencode/` (except `*.lock`, `package-lock.json`, `.gitignore`, `.DS_Store`, and withheld non-settings/non-Markdown files), plus `.local/share/opencode/auth.json` |

#### Read-only folder mounts

- **Host-enforced immutability:** Settings folders that exist in `~/.config/devvm/agents/<agent>/` are mounted read-only into the guest at their exact host store path and symlinked into the VM's home directory (`~`). Because the mount path in the guest matches the host store path, absolute paths recorded in Claude Code plugin metadata continue to resolve.
- **Unredacted content:** Mounted directories contain executable scripts, plugins, and skill definitions, so `devvm` mounts them as-is without scrubbing secrets. Do not store `.env` files, private keys, or embedded git credentials inside skill or plugin folders.
- **Comma restriction:** Because Lima splits mount options on commas, `agentMounts` skips any directory whose path contains `,` and emits a warning (`agent_dir_comma`).
- **Dynamic mount updates:** If you add or remove a settings folder in the store after a VM is created, `updateAgentDirs` detects the difference on the next launch. Stopped VMs pick up the new mounts immediately on start; running VMs prompt to restart so the mount change can take effect.

#### Launch-time copies and secret redaction

Files copied into the guest on launch are parsed and scrubbed in memory (`syncer.stageSecrets`) before being streamed over `tar` to `guest/sync.sh`:

1. **Settings file parsing and scrubbing (`agentlogin.Redact`):**
   - Applies to `.claude/settings.json`, `.codex/config.toml`, and `.json`, `.jsonc`, or `.toml` files in `.config/opencode/`.
   - Every file is decoded completely (`jsonc.Strip` + `json.Decoder` or `toml.Decode`). If a settings file fails to parse, `devvm` refuses to copy it into the VM and warns (`config_not_copied`).
   - Any string under a secret-matching key (`*key*`, `*token*`, `*secret*`, `*passw*`, `Authorization`, `Proxy-Authorization`, `Cookie`, excluding `keybind`/`keymap` tables) is replaced with `devvm-redacted`, unless the value is a variable reference (`OPENAI_API_KEY`, `{env:VAR}`, `{file:PATH}`, `$VAR`, `${VAR}`) or a file path (`/`, `~/`, `./`).
   - All string values inside `env`, `environment`, `headers`, `http_headers`, and Codex's `shell_environment_policy.set` tables are replaced with `devvm-redacted`, except booleans (`true`/`false`), up to 5-digit numbers, scrubbed non-credential URLs, and allowed top-level Claude Code configuration variables (`ANTHROPIC_MODEL`, `ANTHROPIC_SMALL_FAST_MODEL`, `ANTHROPIC_DEFAULT_*_MODEL`, `ANTHROPIC_CUSTOM_MODEL_OPTION*`, `CLAUDE_CODE_*`, `DISABLE_*`, `MAX_*`, and `*_TIMEOUT_MS` whose names do not match credential keywords).
   - Known token prefixes (`sk-`, `sk_`, `pk_`, `rk_`, `ghp_`, `gho_`, `ghs_`, `ghu_`, `github_pat_`, `glpat-`, `xox[abpr]-`, `AKIA`, `ASIA`, `AIza`, `ya29.`, `eyJ`), `Bearer`/`Basic` tokens in shell commands or hooks, secret-named CLI flags (`--api-key VAL`, `--token=VAL`), and URL userinfo or secret query parameters are scrubbed to `devvm-redacted`.
   - Non-settings, non-Markdown files in `.config/opencode/` (such as `.env` files) are withheld completely (`withheld`).
2. **Claude Code `.claude.json` merging and MCP server bindings:**
   - Only allowed top-level keys (`claudeTopKeys`, such as `mcpServers`, `oauthAccount`, `theme`, `editorMode`, and onboarding flags) are kept. `primaryApiKey`, `customApiKeyResponses`, and per-project `.projects` state are stripped.
   - For each server in `mcpServers`, `env` values are replaced with `devvm-redacted`, and `command`, `headersHelper`, `args`, and `url` are scrubbed.
   - If an MCP server specifies an `https://` URL on default port `443` with a valid DNS hostname, each entry in its `headers` map is replaced with `devvm-proxy-managed-claude-mcp-<12-hex-sha256>-<salt>` and registered as a `login` binding in the proxy's `secrets.json`. Plain HTTP MCP server headers are replaced with `devvm-redacted`.

## Importing and saving agent settings

Use `devvm agent import` to seed the host store from your existing host configurations, and `devvm agent save` to persist configuration changes made inside a VM back to the host store.

### Importing from the host home directory

```bash
# Import settings and logins for all configured agents in ~
devvm agent import

# Import only specific agents
devvm agent import claude codex

# Overwrite existing store entries without interactive confirmation
devvm agent import --force
```

`vm.AgentImport.Run` enforces the following rules when reading from `~`:

- **Symlink containment:** Each agent's home folder (`~/.claude`, `~/.codex`, `~/.config/opencode`) is opened via `os.OpenRoot`. Symlinks that resolve to regular files inside that root are copied as regular files (`0600` or `0700` if owner-executable). Symlinks pointing outside the agent root, symlinks to directories, and symlinked login files are skipped with a warning (`agent_import_link_left_out`).
- **Claude Code plugins:** When importing `.json` files inside `.claude/plugins`, `renamePrefix` rewrites host paths (`~/.claude/plugins/...`) to their corresponding store paths (`~/.config/devvm/agents/claude/.claude/plugins/...`) so mounted plugins resolve inside VMs.
- **Claude Code credentials:** On macOS, Claude Code stores OAuth credentials in the system Keychain rather than `~/.claude/.credentials.json`; `devvm agent import` does not read the macOS Keychain for Claude Code. From `~/.claude.json`, it imports only `claudeTopKeys` via `agentlogin.ClaudeCopy`.
- **opencode 2 SQLite export:** When the host `opencode --version` reports major version `2` or higher, `devvm agent import` executes `opencode auth export` in your home directory and converts active credentials via `agentlogin.OpencodeFromExport` into the store's `auth.json` format. Active API keys (`"type": "key"`) and OpenAI Codex OAuth logins (`chatgpt-browser`, `chatgpt-headless`, or legacy imported entries) are saved; unsupported OAuth methods (such as `chatgpt-token-sharing`) are skipped with a warning (`agent_import_opencode_skipped`).

### Saving settings from a VM to the host store

```bash
# Inspect diffs and save settings for all agents in the current folder's VM
devvm agent save

# Inspect diffs and save settings only for Claude Code
devvm agent save claude
```

Because files inside a VM may have been modified by an untrusted or compromised agent, `vm.AgentSave` validates every byte read from the guest before writing to `~/.config/devvm/agents/`:

<Steps>
  <Step title="Stream extraction and resource caps">
    `devvm` starts the VM if stopped and runs `agentSaveScript` to stream agent settings over `tar` ( plus `.devvm-save/claude.json` filtered by `jq` to `ClaudeTopKeys`). `readSent` enforces hard limits: at most `128 MiB` total stream size (`saveMaxBytes`), `16 MiB` per file (`saveMaxFile`), `20,000` total entries (`saveMaxEntries`), and at most `8` relative symlink hops within the same settings folder (`saveLinkHops`). Any absolute path, `..` traversal, duplicate entry, or path containing non-graphic Unicode characters (`!unicode.IsGraphic`) aborts the entire save immediately (`agent_save_unsafe`).
  </Step>
  <Step title="Placeholder restoration and context verification">
    Guest settings files and `.claude.json` contain `devvm-redacted` and `devvm-proxy-managed-*` placeholders. `agentlogin.Restore` parses the VM's file, the redacted copy originally given to the VM, and the host store's original file. It restores the host store's real secret at a JSON/TOML path **only** if:
    - The VM's file has the exact same placeholder string at that path as the copy originally given to the VM, and
    - The immediate parent table/list and grandparent table/list (below the file's top level) are `reflect.DeepEqual` to what the VM was given.

    If a guest modifies the URL or arguments of an MCP server or model provider while keeping the placeholder, `Restore` returns a `*PlaceholderError`, and `devvm agent save` skips the entire file with a warning (`agent_save_placeholder`) so a VM cannot redirect a host secret to a different endpoint. Any unparsed file or non-settings file (such as a skill script) containing a placeholder is also skipped.
  </Step>
  <Step title="Sanitized diff review and terminal confirmation">
    `showPlans` prints added (`A`), modified (`M`), and deleted (`D`) folder entries and a unified line diff (capped at `20,000` lines, `1 MiB`, or `10,000` lines per file). Both sides of settings file diffs are passed through `agentlogin.Redact` so host secrets are never printed to the terminal, and non-printable/bidi characters are escaped as `\u{...}`. `devvm agent save` then prompts for confirmation (`y/N`) and refuses to run with `--no-input` or without a TTY.
  </Step>
</Steps>

## Authenticating coding agents

`devvm agent login` runs an agent's login flow inside the current folder's VM (or saves an API key directly on the host) and stores the resulting credentials in `~/.local/share/devvm/logins/<agent>/` (or the host secret store for `claude --setup-token`). The host machine does not need `claude`, `codex`, or `opencode` installed.

### Command syntax and flags

```bash
devvm agent login AGENT [flags]
```

<ParamField body="AGENT" type="string" required>
  Target coding agent to authenticate: `claude`, `codex`, or `opencode`. The current folder's VM must include the agent in its shared tools.
</ParamField>

<ParamField body="--setup-token" type="boolean">
  Valid only with `claude`. Runs `claude setup-token` inside the VM to mint a 1-year `user:inference` token and saves it as the `claude` host secret instead of an OAuth login in `.claude/.credentials.json`.
</ParamField>

<ParamField body="--device-code" type="boolean">
  Valid with `codex` and `opencode` (mutually exclusive with `--api-key`). Uses headless device-code authentication (`codex login --device-auth` or `opencode auth login openai --method chatgpt-headless`) so no localhost port forwarding is opened.
</ParamField>

<ParamField body="--api-key" type="boolean">
  Valid with `codex` and `opencode` (mutually exclusive with `--device-code`). Prompts for an API key on the host without echoing (or reads one line from standard input with `--stdin`) and writes it directly to the host store without running the agent's login binary in the VM.
</ParamField>

<ParamField body="--provider" type="string">
  Required with `devvm agent login opencode --api-key` (invalid otherwise). Specifies the model provider whose API key is saved and bound by the proxy: `anthropic`, `openai`, or `openrouter`. This keeps BYOK workflows provider-neutral across supported upstream APIs.
</ParamField>

<ParamField body="--stdin" type="boolean">
  Valid only with `--api-key`. Reads the API key from a single line of standard input without prompting.
</ParamField>

### In-VM OAuth token capture and proxy injection flow

Every VM's proxy configuration registers the agents' OAuth token endpoints in `Config.Captures`. When an agent logs in inside a VM, `devvm _proxy` intercepts the token exchange on port `443`, captures the upstream response, writes the real tokens to the host store under an `flock` lock, and returns a sanitized response containing only salted placeholders to the VM.

```mermaid
sequenceDiagram
  autonumber
  participant Guest as Agent in Guest VM
  participant Proxy as Host Proxy (devvm _proxy)
  participant Store as Host Store (internal/secrets & agentlogin)
  participant Auth as OAuth / API Upstream (:443)

  Note over Guest,Auth: Phase 1: In-VM OAuth Login Capture
  Guest->>Proxy: POST /oauth/token or /v1/oauth/token (TLS with VM CA)
  Proxy->>Store: Capturer.Request(Exchange)
  Store-->>Proxy: Verified request body
  Proxy->>Auth: Forward POST over upstream TLS
  Auth-->>Proxy: 200 OK {access_token, refresh_token, id_token}
  Proxy->>Store: Capturer.Response(Exchange, reply)
  Store->>Store: Lock & write real tokens to logins/<agent>/<file><br/>Regenerate vms/<hash>/run/secrets.json
  Store-->>Proxy: Sanitized reply {access_token: placeholder-<salt>, refresh_token: "devvm-proxy-managed", expires_in: ~2099}
  Proxy-->>Guest: 200 OK (Sanitized reply, stripped headers)

  Note over Guest,Auth: Phase 2: Outbound API Request & Token Refresh
  Guest->>Proxy: POST https://api.anthropic.com or chatgpt.com<br/>Authorization: Bearer placeholder-<salt>
  Proxy->>Store: Lookup(Binding) -> Store.Get
  alt Access token within 10m of expiry
    Store->>Auth: POST refresh_token grant (via company proxy if set)
    Auth-->>Store: New access_token & refresh_token
    Store->>Store: Atomic writeRefreshed to logins/<agent>/<file>
  end
  Store-->>Proxy: Real access_token / API key (cached 30s for login, 5m for secret)
  Proxy->>Auth: Forward request with real Authorization / x-api-key header
  Auth-->>Proxy: API response
  Proxy-->>Guest: API response
```

### Agent-specific login procedures

<Tabs>
  <Tab title="Claude Code">
    `devvm` supports two mutually exclusive authentication modes for Claude Code—a Claude subscription OAuth login or a long-lived setup token—and retains whichever credential was saved most recently:

    ```bash
    # Option 1: Log in to a Claude subscription (OAuth with automatic host refresh)
    devvm agent login claude

    # Option 2: Mint a 1-year setup token via claude setup-token
    devvm agent login claude --setup-token

    # Option 3: Save an existing sk-ant-oat01-... setup token directly on the host
    devvm secret set claude
    ```

    - **Execution and code paste:** `devvm agent login claude` runs `env -u CLAUDE_CODE_OAUTH_TOKEN claude auth login --claudeai` (or `claude setup-token`) inside the VM. Open the printed URL in any browser, authorize, and paste the callback code into the terminal. No localhost ports are forwarded.
    - **Token endpoint capture:** The proxy intercepts `POST` requests to `platform.claude.com/v1/oauth/token` and `console.anthropic.com/v1/oauth/token`.
    - **Scope enforcement:** `claudeScopesOK` requires `user:inference` on new logins and rejects any login or refresh containing `org:create_api_key` with HTTP `502`. This blocks Anthropic Console logins (which only mint API keys) and prevents a guest VM from calling `api.anthropic.com/api/oauth/claude_cli/create_api_key` to generate a persistent API key using your OAuth token.
    - **Single active login & transition:** Capturing a subscription OAuth login writes `~/.local/share/devvm/logins/claude/.claude/.credentials.json` (`claudeAiOauth`) and deletes any saved `claude` secret. Conversely, capturing a `setup-token` request (identified by `expires_in` in the authorization code request) or running `devvm secret set claude` saves the `claude` secret and removes `.credentials.json`. Both operations synchronize on `~/.local/share/devvm/logins/claude/lock`.
    - **Guest files and placeholders:** A VM receives either `CLAUDE_CODE_OAUTH_TOKEN=sk-ant-oat01-devvm-proxy-managed-<salt>` in `~/.config/devvm/env` **or** `~/.claude/.credentials.json` with `"accessToken": "sk-ant-oat01-devvm-proxy-managed-oauth-<salt>"`, `"refreshToken": "devvm-proxy-managed"`, and `"expiresAt": 4070908800000` (year 2099), never both. Both bindings (`claude` and `claude-oauth`) are written to `secrets.json` and resolve to whichever credential `devvm` currently holds, so running VMs continue working across credential switches without a restart. If `CLAUDE_CODE_OAUTH_TOKEN` is set in the host environment where `devvm` runs, it overrides both stored credentials.
  </Tab>
  <Tab title="Codex">
    `codex` supports ChatGPT browser OAuth, headless device-code OAuth, and OpenAI API keys:

    ```bash
    # Browser OAuth on this machine (forwards localhost:1455 and 1457 during login)
    devvm agent login codex

    # Device-code OAuth on any machine (no port forwarding)
    devvm agent login codex --device-code

    # Save an OpenAI API key from prompt or stdin
    devvm agent login codex --api-key
    printf '%s\n' "$OPENAI_API_KEY" | devvm agent login codex --api-key --stdin
    ```

    - **Loopback callback forwarding:** Without `--device-code`, `AgentLogin.login` opens a temporary SSH loopback forward (`reach.SSH.ForwardLoopback`) from host `127.0.0.1:1455` (and fallback port `1457` when available) to the VM while `codex -c cli_auth_credentials_store="file" login` runs. If host port `1455` is already bound, `devvm` aborts and instructs you to pass `--device-code`. Pressing `Ctrl+C` closes the port forward and kills the in-VM login PID recorded in `~/.cache/devvm-login.pid`.
    - **Token capture and API-key exchange:** The proxy intercepts `POST https://auth.openai.com/oauth/token`. When Codex completes the `authorization_code` grant, `codexCapture` saves `id_token`, `access_token`, `refresh_token`, and `account_id` to `~/.local/share/devvm/logins/codex/.codex/auth.json` and returns JWT-shaped placeholders (`alg: "none"`, `exp: 4070908800`, signature `devvm-placeholder`) preserving non-sensitive account claims (`email`, `chatgpt_account_id`, `https://api.openai.com/auth`, `https://api.openai.com/profile`). When Codex immediately follows up with a `urn:ietf:params:oauth:grant-type:token-exchange` request (`requested_token=openai-api-key`) carrying the `id_token` placeholder, `CaptureRequest` swaps in the store's real `id_token` upstream, captures the returned API key into `OPENAI_API_KEY`, and gives the VM `sk-devvm-proxy-managed-codex-<salt>`.
    - **Bindings:** `codexBindings` binds `codex-key` (`Authorization` on `api.openai.com`) and `codex` (`Authorization` on `chatgpt.com` and `api.openai.com`).
  </Tab>
  <Tab title="opencode">
    `opencode` supports OpenAI OAuth (via its Codex login methods) and provider-scoped API keys for `anthropic`, `openai`, and `openrouter`:

    ```bash
    # Browser OAuth to OpenAI using opencode's Codex browser method
    devvm agent login opencode

    # Device-code OAuth to OpenAI using opencode's Codex headless method
    devvm agent login opencode --device-code

    # Save an API key for Anthropic, OpenAI, or OpenRouter
    devvm agent login opencode --api-key --provider anthropic
    devvm agent login opencode --api-key --provider openai
    devvm agent login opencode --api-key --provider openrouter
    ```

    - **Supported OpenAI OAuth methods:** `devvm agent login opencode` invokes `opencode auth login openai --method chatgpt-browser` (or `chatgpt-headless` with `--device-code`), which exchanges tokens at `auth.openai.com/oauth/token`. Do not select `opencode`'s interactive "Sign in with ChatGPT" (`chatgpt-token-sharing`) inside a VM: that flow requests tokens from `auth.openai.com/api/accounts/oauth/token` and verifies OpenAI's JWT signature inside the guest, so the proxy's `guard` filter blocks its token response with `502`.
    - **User-Agent disambiguation:** Because Codex and `opencode` share `auth.openai.com/oauth/token`, `agentlogin.Pick` inspects the HTTP `User-Agent` header in VMs that have both agents installed: requests starting with `Bun/` or `opencode/` are routed to `opencode`'s login store, and all others to Codex's.
    - **Provider bindings:** `opencodeBindings` creates proxy bindings for API keys under `anthropic` (`x-api-key` on `api.anthropic.com`), `openai` (`Authorization` on `api.openai.com`), and `openrouter` (`Authorization` on `openrouter.ai`), plus OAuth logins under `openai` (`Authorization` on `chatgpt.com`) and `github-copilot` (`Authorization` on `api.githubcopilot.com`, when `enterpriseUrl` is empty). API keys for other providers (such as local model servers) and `"type": "wellknown"` entries are left intact in the VM's copy with a warning (`opencode_login_kept`); unbound OAuth logins (such as Copilot Enterprise) have their `access` and `refresh` tokens replaced with `devvm-redacted` (`opencode_login_withheld`).
    - **SQLite credential synchronization (`guest/opencode.sh`):** `opencode` 2 imports `~/.local/share/opencode/auth.json` only once when creating `~/.local/share/opencode/opencode.db`. On subsequent VM launches—and immediately after `devvm agent login opencode --api-key`—`devvm` generates `opencode auth export` entries containing placeholders under deterministic IDs (`cred_devvm_<provider>`) via `agentlogin.OpencodeImport` and pipes them to `guest/opencode.sh`. Once `opencode.db` exists, `opencode.sh` compares the payload against `~/.local/state/devvm/opencode-import.json` and verifies all `cred_devvm_*` IDs exist in `opencode auth export`; when changed or missing, it logs out previous `cred_devvm_*` entries, imports each new entry via `opencode auth import`, and activates it via `opencode auth switch`. Credentials added manually inside the VM under non-`devvm` IDs are preserved.
  </Tab>
</Tabs>

## Managing host secrets and proxy bindings

In addition to agent login files, `devvm` manages three named secrets in `internal/secrets`:

| Secret name | Target hosts (`:443` only) | Injected HTTP header | Guest environment variable | Placeholder format | Binding scope |
| --- | --- | --- | --- | --- | --- |
| `claude` | `api.anthropic.com` | `Authorization` | `CLAUDE_CODE_OAUTH_TOKEN` | `sk-ant-oat01-devvm-proxy-managed-<salt>` | Automatically bound to every VM with the `claude` tool; can also be bound via `devvm secret use claude` |
| `github` | `api.github.com`, `github.com`, `codeload.github.com`, `raw.githubusercontent.com` | `Authorization` | `GH_TOKEN` (plus git HTTPS credential helper when `DEVVM_GITHUB=1`) | `devvm-proxy-managed-github-<salt>` | Bound per-VM via `devvm secret use github` |
| `proxy` | Host-only (`HostOnly: true`) | `Proxy-Authorization` (to upstream corporate proxy) | None (never exposed to VMs) | None | Used by host `devvm` and `devvm _proxy` when `config.toml` `[proxy]` specifies a username |

### Secret CLI operations

```bash
# List known secrets, whether each is saved, and which VMs use it
devvm secret
devvm secret --json

# Save a secret interactively or via stdin
devvm secret set github
gh auth token | devvm secret set github --stdin
devvm secret set claude
devvm secret set proxy

# Bind or unbind a secret for the current folder's VM
devvm secret use github
devvm secret unuse github

# Delete a saved secret from the host
devvm secret rm github
devvm secret rm github --force
```

<RequestExample>
```bash
devvm secret --json
```
</RequestExample>

<ResponseExample>
```json
{
  "version": 1,
  "secrets": [
    {
      "name": "claude",
      "saved": true,
      "used_by": [
        "devvm-some-project-71a99b1076d92c"
      ]
    },
    {
      "name": "github",
      "saved": true,
      "used_by": [
        "devvm-some-project-71a99b1076d92c"
      ]
    },
    {
      "name": "proxy",
      "saved": false,
      "used_by": []
    }
  ]
}
```
</ResponseExample>

### Storage, validation, and runtime injection

- **Host storage backend:** On macOS (`platform.HasKeychain`), `secrets.Store` stores `claude`, `github`, and `proxy` in the login Keychain under service `devvm` (overridable via `DEVVM_SECRET_SERVICE` matching `^[A-Za-z0-9._-]+$`) using `security -i` with the secret passed over standard input. On Linux, secrets are stored in `~/.local/share/devvm/secrets/<name>` with directory mode `0700` and file mode `0600`.
- **Input validation (`secrets.Token`):** All whitespace is stripped from `claude` and `github` tokens (while `proxy` passwords only trim leading/trailing whitespace). Tokens containing quotes (`"`), backslashes (`\`), or ASCII control characters (`< 0x20` or `0x7f`) are rejected (`token_quotes`). A `claude` secret must start with `sk-ant-oat01-` (`not_setup_token`).
- **Immediate proxy reload:** Running `devvm secret set`, `rm`, `use`, or `unuse` calls `Proxy.RefreshSecrets`, which rewrites `~/.local/share/devvm/vms/<hash>/run/secrets.json` and `secrets.policy`. Because `devvm _proxy` watches `secrets.json` on every request (`watched[[]Binding]`) and clears its in-memory value cache whenever the file changes, secret additions and revocations apply immediately to open connections without restarting the VM.
- **Header and Basic auth replacement (`secrets.inject`):** When `devvm _proxy` intercepts a TLS connection on port `443` to a bound host, it checks that the HTTP `Host` header matches the `CONNECT` tunnel's hostname (returning `421 Misdirected Request` otherwise). It then replaces the binding's placeholder inside the configured header—including inside base64-encoded `Authorization: Basic ...` headers used by `git` over HTTPS—and records the secret name in the `secrets` array of `network.jsonl`.
- **Lookup serialization and caching:** `secrets.value` serializes lookups under `valueMu` with a `20s` timeout (`lookupTimeout`). `KindSecret` values are cached in memory for `5 minutes` (`valueTTL`); `KindLogin` values are cached for `30 seconds` (`loginTTL`). When an OAuth login (`codex`, `opencode`, or `claude-oauth`) is within `10 minutes` of expiration (`refreshMargin`), `File.Get` acquires an exclusive `flock` on the login directory and refreshes the token upstream (routing through the corporate proxy and trusting `~/.config/devvm/ca` if configured).
- **Blocked GitHub credential-minting calls (`refuseMinting`):** Even when `github` is bound to a VM, `devvm _proxy` inspects every HTTPS request to `api.github.com` and blocks requests that would mint new persistent credentials capable of outliving `devvm secret unuse`:
  - `POST /user/keys`, `POST /user/ssh_signing_keys`, `POST /user/gpg_keys` (account keys)
  - `POST /repos/{owner}/{repo}/keys`, `POST /repositories/{id}/keys` (repository deploy keys)
  - `POST /.../{repos,repositories,orgs,organizations,enterprises}/.../actions/runners/{registration-token,generate-jitconfig}` (self-hosted Actions runners)
  - `POST` or `PATCH` to `/.../{repos,repositories,orgs,organizations}/.../hooks...` (webhooks)

## Exporting and importing agent history

Session transcripts and databases remain inside each VM and are never placed in `~/.config/devvm/agents/` or `~/.local/share/devvm/logins/`. When you delete a VM with `devvm rm` (unless `--no-export` is passed) or run `devvm history export`, `devvm` archives the VM's agent history to a `.tar.gz` file on the host.

```bash
# List saved exports in ~/.local/share/devvm/exports (oldest first)
devvm history
devvm history --json

# Export the current VM's agent history to ~/.local/share/devvm/exports or a custom path
devvm history export
devvm history export ~/backups/project-history.tar.gz

# Import the newest export for this folder (or a specific archive) into the current VM
devvm history import
devvm history import ~/backups/project-history.tar.gz
```

### Sanitization and safety boundaries in history archives

- **Included and excluded paths:** `historyExport` archives `.claude`, `.claude.json`, `.codex`, `.local/share/opencode`, and `.local/state/opencode`, capped at `2 GiB`. It explicitly excludes `.claude/.credentials.json`, `.codex/auth.json`, `.local/share/opencode/auth.json`, `.local/share/opencode/account.json`, `.local/share/opencode/mcp-auth.json`, `.local/share/opencode/bin`, and raw `opencode*.db*` files (`-wal`, `-shm`, `-journal`).
- **opencode SQLite credential scrubbing (`devvm_opencode_clean`):** Because `opencode` 2 stores session history and credentials in the same SQLite database (`~/.local/share/opencode/opencode.db` in WAL mode), both `historyExport` and `historyImport` run `devvm_opencode_clean` inside the guest using `sqlite3 -bail -cmd 'PRAGMA trusted_schema = OFF'`. The function snapshots the database via `VACUUM INTO 'clean.db'`, drops all triggers, deletes all rows from any table whose name or column names match credential patterns (`opencodeLogins`: `credential`, `account`, `control_account`, `account_state`, `session_share`, or columns like `access_token`, `refresh_token`, `api_key`, `secret`), deletes `wellknown:config:%` rows from `kv`, removes the `20260805200742_import_legacy_credentials` migration entry so `opencode` re-imports the VM's `auth.json` placeholders on next launch, and runs `PRAGMA journal_mode = DELETE; PRAGMA secure_delete = ON; VACUUM;` to purge deleted pages before verifying zero rows remain.
- **Untrusted archive unpacking (`historyImport`):** `devvm` streams the archive into the guest and never unpacks it on the host. Inside the VM, GNU `tar` lists all archive entries first and aborts with `devvm-history-unsafe` if any entry falls outside the five allowed history paths, starts with `/`, or contains `..`. It extracts into a temporary directory with `--no-same-owner --no-same-permissions` and excludes `importSkips()` (all login files, `.claude.json`, and all store-mounted/copied agent paths), deletes any symlink targeting an absolute path or `..`, cleans any `opencode*.db` files via `devvm_opencode_clean`, and finally copies the remaining files into `~` with `--skip-old-files` so existing VM files are never overwritten.

## Troubleshooting and failure modes

<AccordionGroup>
  <Accordion title="Port 1455 is already in use during devvm agent login">
    Browser OAuth for `codex` and `opencode` forwards host `127.0.0.1:1455` to the VM so the browser redirect can reach the agent's local callback server. If another process on your host is listening on port `1455`, `devvm agent login` fails with `agent_login_port_taken`. Run `devvm agent login codex --device-code` or `devvm agent login opencode --device-code` to authenticate with a one-time device code without port forwarding.
  </Accordion>
  <Accordion title="Claude login or opencode ChatGPT login returns 502 Bad Gateway">
    `devvm _proxy` returns `502` (`devvm couldn't read the login's token request or reply, so it went no further`) in three situations:
    1. You authenticated Claude Code with an Anthropic Console (API billing) account that lacks the `user:inference` scope or includes `org:create_api_key`. Log in with a Claude subscription account or use `devvm agent login claude --setup-token`.
    2. Inside `opencode`, you selected `Sign in with ChatGPT` (`chatgpt-token-sharing`), which uses `/api/accounts/oauth/token`. The proxy guard blocks token responses from uncaptured paths so raw tokens never enter the VM. Use `devvm agent login opencode` on the host (which selects `chatgpt-browser` or `chatgpt-headless`).
    3. You ran `claude /login` inside a VM whose image does not include the `claude` shared tool. Add the tool first with `devvm tools add claude`.
  </Accordion>
  <Accordion title="Agent requests fail after a VM runs for more than 30 days without a launch">
    Each VM has a dedicated ECDSA P-256 CA (`vms/<hash>/run/ca/ca.pem`) and a pre-installed successor CA (`next.pem`), each valid for 30 days. If a VM runs continuously past `next.pem`'s expiration without `devvm` being run in its project folder, `caEndedRefusal` rejects `CONNECT` tunnels to bound secret hosts on port `443` with `403` and logs `devvm's CA has ended: open the VM again (devvm in its folder) for a new one`. Run `devvm` in the project directory on the host to rotate the VM's CA certificates and restart its proxy.
  </Accordion>
  <Accordion title="devvm agent save skips a settings file with a placeholder warning">
    When `devvm agent save` warns that a file was left out because of a placeholder (`agent_save_placeholder`), the agent or user inside the VM modified the table containing a `devvm-redacted` or `devvm-proxy-managed-*` placeholder (or its parent table), or placed a placeholder into a non-settings file. Edit the file directly in `~/.config/devvm/agents/<agent>/` or `~/.local/share/devvm/logins/<agent>/` on the host instead.
  </Accordion>
  <Accordion title="Agent stops authenticating after OAuth refresh failure">
    When an OAuth refresh token expires or is revoked upstream, `File.Get` logs `claude_refresh_failed`, `codex_refresh_failed`, or `opencode_refresh_failed` to `~/.local/share/devvm/vms/<hash>/logs/proxy.log` andwithholds the header rather than sending a stale token. Re-authenticate by running `devvm agent login <agent>` in the project folder.
  </Accordion>
</AccordionGroup>

## Related pages

<CardGroup cols={2}>
  <Card title="Network tiers and host proxy" href="/network-tiers-and-proxy">
    Traffic confinement across allowlist, internet, none, and open tiers, TLS SNI inspection, and per-VM CA rotation.
  </Card>
  <Card title="CLI reference: network, security, and agent commands" href="/cli-security-network-and-agent-commands">
    Complete syntax, subcommands, and flags for devvm agent, devvm secret, devvm network, and devvm git-protect.
  </Card>
  <Card title="Manage toolchains and custom layers" href="/toolchains-and-custom-layers">
    Attach shared toolchains and coding agents to VMs with devvm tools and build custom image layers.
  </Card>
  <Card title="Manage disk retention, garbage collection, and upgrades" href="/disk-retention-and-upgrades">
    Configure automatic retention policies and garbage collection for agent history exports and shared tool images.
  </Card>
</CardGroup>
