[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M77K7TEQTNFHSVB4NDFXT4R8":3,"skill-same:01M77K7TEQTNFHSVB4NDFXT4R8":44},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":14,"repositoryHtmlUrl":27,"repositoryHomepageUrl":6,"ownerHtmlUrl":28,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":29,"install":30,"resources":43},"01M77K7TEQTNFHSVB4NDFXT4R8","claude-vm-config-global",null,"Interactively create the global claude-vm config PAIR at ~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml + config-boot.yml from the resolved defaults (cpus 2, mem 4096, bundled-tinyproxy proxy default, egress.allow incl. api.anthropic.com, claude.version). Idempotent — detects existing files and offers to merge or leave rather than clobber.","TheVoskamps\u002Fclaude-plugins-marketplace","TheVoskamps","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F235012489?v=4","01M77K5ZCP12QCJPQ85WVJSHTS","Irene and Edwin Voskamp","01M77K5ZDS7DB0PA1W8FM7G2QD",0,"GPL-3.0","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fclaude-vm\u002Fskills\u002Fclaude-vm-config-global\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: claude-vm-config-global\ndescription: Interactively create the global claude-vm config PAIR at ~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml + config-boot.yml from the resolved defaults (cpus 2, mem 4096, bundled-tinyproxy proxy default, egress.allow incl. api.anthropic.com, claude.version). Idempotent — detects existing files and offers to merge or leave rather than clobber.\n---\n\n# claude-vm-config-global\n\nYou are running the `\u002Fclaude-vm-config-global` skill. Your job is to\ncreate the **global** claude-vm config **pair** at\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` and\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml` (expand `~` to the user's home\ndirectory) from the resolved defaults.\n\nSince issue #179, claude-vm's config is split into a **bake** file and a\n**boot** file per tier (four files total, all optional):\n\n- **`config-bake.yml`** — keys that change **bytes in the guest `.raw`\n  image**: `packages:` (a flat list of apt packages baked into the\n  image), `apt_sources:` (third-party apt repos rendered into the\n  image), and `image.root_headroom_mb`. The image cache key + filename\n  is a **whole-file, raw-byte hash** of the bake files, so editing a\n  bake file (including only its trailing newline) rebuilds the image.\n- **`config-boot.yml`** — keys applied at **run time** (launcher\u002FVM\n  wiring): `cpus`, `mem`, `proxy.*`, `egress.allow`, `mounts`,\n  `repo.*`, `packages:` here means **installed at boot** (a flat list),\n  `update_at_boot`, `add_apt_uris_to_allowlist`, `claude.*`, and\n  `github.*`. Boot files never affect image\n  identity.\n\nThis pair is the machine-wide layer of claude-vm's four-file config (the\nper-repo pair at `\u003Crepo>\u002F.claude-vm\u002Fconfig-{bake,boot}.yml`, written by\n`\u002Fclaude-vm-config-repo`, overrides it). The config surface, layering\nsemantics, and key meanings are documented in the sibling `claude-vm`\nskill (`skills\u002Fclaude-vm\u002FSKILL.md`) and the annotated\n`payload\u002Fconfig-bake.example.yml` \u002F `payload\u002Fconfig-boot.example.yml`.\nThis skill writes the global layer with the resolved defaults rather\nthan the examples' illustrative placeholders.\n\n**Migration:** if a legacy single-file `~\u002F.config\u002Fclaude-vm\u002Fconfig.yml`\nexists, the launcher aborts with a migration message (it will not\nsilently read it). When this skill detects that legacy file, offer to\nmigrate: split its keys into the bake\u002Fboot pair per the placement rule\nabove, write the pair, and tell the user to remove or rename the legacy\n`config.yml` afterward.\n\n## Idempotent — detect and offer, never clobber\n\nThis skill is **idempotent**. Before writing anything it checks whether\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` and\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml` already exist (each independently):\n\n- **If a file does not exist**: this is the create path for that file.\n  Propose the full default file, get approval, write it.\n- **If a file already exists**: **do not clobber it.** Read it, show the\n  user what is there, and offer these choices:\n  1. **Leave** the existing file untouched (the default, safe choice).\n  2. **Merge** the resolved defaults in for any keys the existing file\n     is missing, preserving every key the user already set.\n\n  Never overwrite a key the user already set on the \"merge\" path, and\n  never delete a key. A merge only *fills gaps*. If the existing file\n  already has every key the defaults provide, report that it is already\n  complete and leave it untouched.\n\nWriting either file requires explicit user approval in every case.\n\n## Resolved defaults\n\nThese are the values this skill writes, drawn from the parent issue's\nverified analysis. They deliberately **override** the illustrative\nvalues in `payload\u002Fconfig-bake.example.yml` \u002F\n`payload\u002Fconfig-boot.example.yml`. The **file** column names which of\nthe two global files each key is written into (its bake\u002Fboot placement):\n\n| key | file | default | rationale |\n| ----- | ------ | --------- | ----------- |\n| `cpus` | boot | `2` | RAM-bound sizing; vCPUs time-slice, 2 covers git\u002Fbuild\u002Ftest bursts |\n| `mem` | boot | `4096` | the real ceiling — RAM is committed, ~8–12 VMs fit at 4 GB on a 64 GB host |\n| `proxy.cmd` | boot | omitted (bundled tinyproxy launcher is the launcher-side default) | tinyproxy is the chosen forward proxy; the launcher runs the bundled `payload\u002Fproxy\u002Ftinyproxy-launch.sh` when `proxy.cmd` is unset |\n| `proxy.port` | boot | `3128` | matches the launcher default |\n| `proxy.host_alias` | boot | `192.168.127.254` | the gvproxy host alias the guest reaches the proxy on |\n| `egress.allow` | boot | `api.anthropic.com`, `github.com`, `claude.ai`, `downloads.claude.ai` | `api.anthropic.com` is required for Remote Control; the rest cover git + claude install\u002Ffetch |\n| `claude.version` | boot | `stable` | which `claude` binary the host-side verified cache fetches |\n| `claude.renderer` | boot | omitted (claude's own default) | terminal renderer on the interactive console: `classic` \\| `fullscreen` \\| unset |\n| `claude.remote_control` | boot | omitted (`false`) | opt-in Remote Control: `true` adds `--remote-control` + a date-stamped `--name` default; `false`\u002Funset passes CLI args through |\n| `packages:` (boot file) | boot | `[]` | apt packages installed AT BOOT (a flat list; through the proxy, before claude starts) |\n| `update_at_boot` | boot | `true` | apt-get update && upgrade at boot |\n| `add_apt_uris_to_allowlist` | boot | `auto` | add derived egress URIs to the proxy allowlist only when boot-time package work needs them (`auto`), or always (`always`) |\n| `apt_sources` (boot file) | boot | `[]` | third-party apt repos a boot-time install pulls from (union+dedup with the bake file's) |\n| `claude.permission_mode` | boot | `bypassPermissions` | in-guest Claude's permission mode (`bypassPermissions` \\| `default` only; any other value aborts the launch) |\n| `claude.plugins.install_at_boot` | boot | `[]` | `plugin@marketplace` refs the guest installs at boot, blocking, before claude starts |\n| `claude.plugins.update_at_boot` | boot | `true` | refresh the marketplaces and update the installed plugins at boot — the freshness path for BAKED plugins |\n| `claude.plugins.add_marketplace_uris_to_allowlist` | boot | `auto` | marketplace-URI analogue of `add_apt_uris_to_allowlist` |\n| `claude.plugins.enabled` | boot | omitted | optional map (plugin ref → boolean) mirroring settings.json's `enabledPlugins`; overrides the default-enabled state per plugin (`false` = installed-but-disabled) |\n| `claude.marketplaces` (boot file) | boot | `[]` | marketplaces only an `install_at_boot` ref needs (union+dedup with the bake file's, by `name`); the url only has to be reachable from the GUEST — the build pre-registers it best-effort and a failure there only warns |\n| `github.auth` | boot | `none` | whether the guest is seeded with a GitHub auth token derived from the host |\n| `packages:` (bake file) | bake | `[]` | apt packages BAKED into the guest image (a flat list; present with no network at boot) |\n| `apt_sources` (bake file) | bake | `[]` | third-party apt repos rendered into the image at build time |\n| `claude.marketplaces` (bake file) | bake | `[]` | marketplaces registered INTO the image at build time (union+dedup with the boot file's, by `name`); registering one declared HERE is a build precondition — a failed add aborts the build, where a boot-declared one only warns |\n| `claude.plugins.bake` | bake | `[]` | `plugin@marketplace` refs INSTALLED into the image at build time; a BAKE key because they change the image's bytes |\n| `image.root_headroom_mb` | bake | `1024` | extra MiB of FREE SPACE in the guest root filesystem above its base content, so a live session (boot-time apt working set + ordinary growth) does not hit ENOSPC |\n| `env.set` (bake file) | bake | omitted | guest environment variables written INTO the image, as explicit literals. NON-SECRET only — a bake value is committed config AND image bytes, and persists across every run |\n| `env.set` (boot file) | boot | omitted | same shape, applied per launch and shredded on exit; overrides the bake value for the same name |\n| `env.copy` | boot | omitted | NAMES read from the environment claude-vm is launched from and forwarded to the guest. BOOT-ONLY — a hard abort in a bake file. This is how a third-party API key reaches the guest without ever being written to config |\n| `env.files` | boot | omitted | host `.env` files parsed at launch and forwarded the same way. BOOT-ONLY — a hard abort in a bake file |\n\nNotes on the forward-looking keys:\n\n- **`proxy.cmd` is omitted from the written config.** tinyproxy is the\n  chosen forward proxy, and the launcher already defaults to the bundled\n  `payload\u002Fproxy\u002Ftinyproxy-launch.sh` when `proxy.cmd` is unset (it reads\n  the egress allowlist from `$CLAUDE_VM_EGRESS_ALLOWLIST` and binds\n  `$CLAUDE_VM_PROXY_PORT`). So the resolved global config leaves\n  `proxy.cmd` unset rather than pinning a brittle invocation. Write a\n  `proxy.cmd` only if the user wants to override the bundled launcher;\n  any override must read `$CLAUDE_VM_EGRESS_ALLOWLIST` rather than a\n  hand-maintained list baked into the command.\n- **`claude.version: stable`** selects which `claude` binary the\n  host-side GPG-verified cache fetches. It is consumed by\n  `payload\u002Flib\u002Fclaude-cache.sh`: the host resolves the channel\u002Fpin to a\n  concrete version, downloads that version's GPG-signed manifest,\n  verifies the signature against the operator's pinned key,\n  checksum-verifies the binary, caches it keyed on the version, and\n  shares it into the guest, where the image's fstab mounts it `ro`.\n  `stable` (default) tracks the conservative stable channel; `latest`\n  tracks the latest channel; a dotted version like `2.1.172` pins one\n  concrete release with no channel resolution.\n- **`api.anthropic.com` must stay in `egress.allow`.** Remote Control is\n  outbound-HTTPS-only and connects to the Anthropic API on 443; dropping\n  this host breaks every in-guest Remote Control session. Treat it as\n  load-bearing, not optional.\n- **`claude.renderer` is omitted by default.** It selects the in-guest\n  claude's terminal renderer on the interactive console (`classic` →\n  `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`; `fullscreen` →\n  `CLAUDE_CODE_NO_FLICKER=1`); unset passes nothing so claude uses its\n  own default. Both renderers work over the byte-pipe console, so leaving\n  it unset is a fine default — write a value only if the user asks for\n  one. An unrecognized value aborts the launch.\n- **`claude.remote_control` is omitted by default (`false`).** It is an\n  opt-in boolean: when `true`, the launcher injects `--remote-control`\n  into the in-guest claude invocation (unless the CLI args already carry\n  it) and, when Remote Control is in effect but no `--name` was given,\n  appends a date-stamped `--name` (format like `Jul10-14:30`) so the run\n  is named. When `false`\u002Funset, claude runs without Remote Control and the\n  user's CLI args pass through unchanged. Accepts `true`\u002F`false` (or leave\n  unset); any other value aborts the launch. Ask the user whether they\n  want Remote Control on by default; write the key only when they opt in.\n- **The bake file's `packages:`\u002F`apt_sources:` and the boot file's\n  `packages:` are written empty (`[]`) by default.** Ask the user whether they want\n  any apt packages baked into the image, installed at boot, or any\n  third-party apt repos; write entries only if they name specific\n  packages\u002Frepos. Leaving these empty is the safe default — the bake\n  file's entries are baked into the image (issue #105) and the boot\n  file's are installed at boot through the proxy (issue #106).\n- **`claude.permissions.allow` \u002F `.ask` \u002F `.deny` and\n  `claude.marketplaces` \u002F `claude.plugins.bake` \u002F\n  `.plugins.install_at_boot` are written empty by default.** Same\n  reasoning: ask whether the user wants specific permission rules,\n  marketplaces, or plugins baked\u002Finstalled; write entries only on\n  request. `claude.permission_mode` and `claude.permissions.*` are\n  rendered into the guest `settings.json` (issue #104). `claude.plugins.bake`\n  refs are installed **into the image** by the build's plugin bake step\n  and `.install_at_boot` refs by the guest boot launcher's plugin phase\n  (issue #107); both are also rendered into `settings.json`'s\n  `enabledPlugins`.\n- **Placement is enforced for `claude.plugins`.** `bake` belongs in\n  `config-bake.yml` (baked plugins change the image's bytes, so they must\n  live where the whole-file image-identity hash sees them);\n  `install_at_boot`, `update_at_boot`,\n  `add_marketplace_uris_to_allowlist`, and `enabled` belong in\n  `config-boot.yml`. A sub-key written into the wrong file **aborts the\n  launch** with a message naming the file that carries it, rather than\n  parsing and being silently ignored. It is a *presence* test, so a\n  misplaced sub-key written empty (a valueless `bake:`, or\n  `bake: []` \u002F `\"\"` \u002F `{}`) aborts exactly like a populated one — never\n  write a placeholder sub-key into the file it does not belong in.\n  `claude.marketplaces` is the exception: allowed\n  in both files, unioned and deduped by `name`, with the same name under\n  two differing urls aborting the launch. Give every entry a `name:` —\n  the name is what a `plugin@marketplace` ref resolves against and what\n  registration is checked by, so an entry with a url and no name aborts\n  the launch too (issue #226). Which file it lands in still\n  decides its build-time failure policy — a bake-file entry must register\n  during the image build or the build aborts, so its url has to be\n  reachable from the build container, while a boot-file entry is\n  pre-registered best-effort and a url the build cannot reach only warns\n  (issue #226).\n- **Prefer an explicit `https:\u002F\u002F` marketplace url.** The launcher derives\n  the guest's marketplace egress hosts from the url; the `owner\u002Frepo`\n  GitHub shorthand yields no derivable host, so it needs `github.com`\n  kept in `egress.allow` by hand.\n- **`claude.plugins.enabled` is omitted by default.** It is an optional\n  map of plugin ref → boolean that mirrors `settings.json`'s own\n  `enabledPlugins` vocabulary and is rendered straight into the guest\n  `settings.json` (issue #104). Every ref in `claude.plugins.bake` \u002F\n  `.install_at_boot` is enabled by default, so write an `enabled` entry\n  only when the user wants to override that — e.g. `false` to ship a\n  plugin installed-but-disabled (toggling a debug plugin like\n  `show-loaded-rules` around a specific issue). Keys must name an\n  installed ref and values must be boolean; a typo aborts the launch.\n- **The whole `env:` block is omitted by default.** Ask the user whether\n  the in-guest claude needs any environment variables — a plain setting\n  (`FOO_ENDPOINT`) or a third-party API key for a tool, MCP server or\n  model provider other than Anthropic\u002FGitHub (`OPENROUTER_API_KEY`,\n  `TAVILY_API_KEY`). Then place each by **how its value is obtained**,\n  which is what decides the tier:\n  - a **literal** the user dictates → `env.set`, in either file. Only\n    write it into `config-bake.yml` when it should persist in the image\n    for every session; a bake value is committed config *and* image\n    bytes, so **never** put a secret there.\n  - a value that already lives in the user's **shell environment** →\n    `env.copy`, listing the NAME only. Never ask for the value and never\n    write it anywhere; the launcher reads it at launch. `env.copy` in a\n    bake file is a hard abort.\n  - a value that already lives in a host **`.env` file** → `env.files`,\n    listing the path. Also boot-only, also a hard abort in a bake file.\n\n  Precedence, lowest to highest:\n  `bake env.set` \u003C `boot env.files` \u003C `boot env.copy` \u003C `boot env.set`.\n\n  **Pair every `env.copy` with `egress.allow`.** Forwarding a key does\n  not make the service reachable — the guest is egress-confined, so the\n  provider's host must be in `egress.allow` (`openrouter.ai` for\n  `OPENROUTER_API_KEY`, `api.tavily.com` for `TAVILY_API_KEY`, …) or the\n  proxy blocks the call and the failure is baffling. Nothing is derived\n  automatically: a variable name implies no hostname the way an apt\n  source's URI does. Add both or neither.\n\n  **These entries are written verbatim, and the launcher rejects several\n  shapes**, so check before writing: an `env.copy` name that is not\n  currently exported in the user's shell **aborts the launch** (confirm\n  with the user that they export it — never print or capture the value);\n  an `env.files` path that is not on the host aborts; a name outside\n  `[A-Za-z_][A-Za-z0-9_]*` aborts; an `env.set` value that is a map or a\n  list aborts, and so does a key left valueless (write `NAME: \"\"` for the\n  empty string); and a name claude-vm's own launcher composes — every name\n  in `run.env` (the proxy vars, `CLAUDE_ARGS`, the `*_TAG` mount vars,\n  `IS_SANDBOX`, `DISABLE_AUTOUPDATER`, the renderer `CLAUDE_CODE_*` vars,\n  the terminal geometry, the `CLAUDE_VM_*_UPDATE_AT_BOOT` flags) plus\n  `CLAUDE_VM_LAST_CLAUDE_STATUS` — aborts, because the guest sources\n  `run.env` before the env files, so such an entry would overwrite the\n  launcher's own value and break the boot;\n  `CLAUDE_VM_LAST_CLAUDE_STATUS` is the exception in the other direction\n  (exported long after both files, so an entry for it would be silently\n  overwritten instead) and is refused at load for that reason.\n- **`github.auth` defaults to `none`.** Ask the user whether they want\n  the guest seeded with a GitHub auth token derived from the host\n  (`host-token`); `none` is the safe default since the consumer that\n  seeds the token lands in a sibling slice under #39.\n\n## Steps\n\nFollow these in order. Do not write the file until the user has\nexplicitly approved the proposed content.\n\n### Step 1: Resolve the target paths\n\nExpand `~` to the user's home directory. There are TWO target files:\n\n```text\n~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml\n~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml\n```\n\nRespect `XDG_CONFIG_HOME` if set: the config dir is\n`${XDG_CONFIG_HOME:-$HOME\u002F.config}\u002Fclaude-vm`, matching the launcher's\n`CLAUDE_VM_GLOBAL_CONFIG_DIR` default in `payload\u002Flib\u002Fconfig.sh`. The\nparent directory may not exist yet; the `Write` tool creates it.\n\nAlso check for a **legacy** `${XDG_CONFIG_HOME:-$HOME\u002F.config}\u002Fclaude-vm\u002Fconfig.yml`.\nIf it exists, tell the user the launcher no longer reads it and offer to\nmigrate its keys into the bake\u002Fboot pair (splitting by the placement\nrule); after writing the pair, remind them to remove or rename the\nlegacy file.\n\n### Step 2: Detect existing config files\n\nCheck each of the two target files independently.\n\n- **Absent** → create path for that file. The proposed content is the\n  full default file from Step 3.\n- **Present** → idempotent path for that file. Read its full contents,\n  parse the YAML, and show the user what is already there. Then ask (via\n  `AskUserQuestion`) whether to:\n  - **Leave it untouched** (recommended default), or\n  - **Merge** the resolved defaults into any keys it is missing.\n\n  On \"leave\", skip that file. On \"merge\", compute the gap-fill (Step 3,\n  merge variant) and continue to Step 4 for that file.\n\n### Step 3: Compose the proposed content\n\n**Create variant** (no existing file). The full default **`config-bake.yml`**\n(image-bytes keys) is:\n\n```yaml\n# claude-vm global BAKE config (machine-wide defaults).\n#\n# Keys here change BYTES in the guest .raw image, so the image cache key +\n# filename is a WHOLE-FILE, RAW-BYTE hash of this file. Editing it (including\n# only its trailing newline) rebuilds the image. The per-repo bake layer at\n# \u003Crepo>\u002F.claude-vm\u002Fconfig-bake.yml overrides scalars and unions lists. See the\n# claude-vm skill (skills\u002Fclaude-vm\u002FSKILL.md) and payload\u002Fconfig-bake.example.yml.\n#\n# NO SECRETS HERE.\n\n# apt packages baked into the guest image (flat list of names; present with no\n# network at boot). Write entries only if the user names specific packages.\npackages: []\n\n# Third-party apt repos rendered into the image at build time:\n# {name, repo, key_url}. Also allowed in the boot file (union, dedup by name).\n# Write entries only on request.\napt_sources: []\n\n# Extra MiB of FREE SPACE in the guest root filesystem above its base content,\n# so a live session does not hit ENOSPC (default 1024). A BAKE key: it sizes\n# the .raw, so changing it rebuilds the image.\nimage:\n  root_headroom_mb: 1024\n\n# Plugin marketplaces registered INTO the image, and the plugin@marketplace refs\n# installed into it, at build time. BAKE keys: baked plugins change the image's\n# bytes. `name` must match the marketplace's OWN manifest name; prefer an\n# explicit https:\u002F\u002F url so the launcher can derive its egress host. Write\n# entries only on request.\nclaude:\n  marketplaces: []\n  plugins:\n    bake: []\n\n# Guest environment variables written INTO the image (explicit literals only).\n# Omitted unless the user names specific variables. NO SECRETS: a value here is\n# committed config AND image bytes, and persists across every run of the image.\n# env.copy \u002F env.files are BOOT-ONLY and abort the launch if written here.\n# env:\n#   set:\n#     FOO_ENDPOINT: https:\u002F\u002Ffoo.internal\u002Fv2\n```\n\nThe full default **`config-boot.yml`** (run-time keys) is:\n\n```yaml\n# claude-vm global BOOT config (machine-wide defaults).\n#\n# Keys here are applied at run time (launcher\u002FVM wiring) and NEVER affect the\n# image identity -- editing this file triggers no rebuild. The per-repo boot\n# layer at \u003Crepo>\u002F.claude-vm\u002Fconfig-boot.yml overrides scalars and unions lists.\n# See the claude-vm skill (skills\u002Fclaude-vm\u002FSKILL.md) and\n# payload\u002Fconfig-boot.example.yml.\n#\n# NO SECRETS HERE. The host claude.ai OAuth credential is mounted into the guest\n# at runtime by the launcher; it is never read from or written to this file.\n\ncpus: 2\nmem: 4096\n\nproxy:\n  # proxy.cmd is intentionally OMITTED: tinyproxy is the chosen forward proxy,\n  # and the launcher defaults to the bundled tinyproxy launcher\n  # (payload\u002Fproxy\u002Ftinyproxy-launch.sh) when proxy.cmd is unset. It reads the\n  # egress allowlist from $CLAUDE_VM_EGRESS_ALLOWLIST and binds\n  # $CLAUDE_VM_PROXY_PORT. Set proxy.cmd only to override that default.\n  port: 3128\n  host_alias: 192.168.127.254\n\negress:\n  allow:                # outbound hosts permitted through the proxy\n    - api.anthropic.com # REQUIRED for Remote Control (outbound 443)\n    - github.com\n    - claude.ai\n    - downloads.claude.ai\n\n# apt packages installed AT BOOT (flat list; through the proxy, before claude\n# starts). Write entries only on request.\npackages: []\nupdate_at_boot: true    # apt-get update && upgrade at boot (default true)\nadd_apt_uris_to_allowlist: auto   # auto (default) | always\n# apt_sources: third-party apt repos a boot-time install pulls from (union+dedup\n# with the bake file's). Write only on request.\napt_sources: []\n\nclaude:\n  version: stable       # which claude binary the host-side GPG-verified cache\n                        # fetches: stable (default) | latest | \u003Cpinned>\n  # renderer: classic   # classic (no alt-screen) | fullscreen | unset. Omitted.\n  # remote_control: false  # opt-in Remote Control. Omitted by default.\n  # permission_mode + permissions.* render into the guest settings.json's\n  # permissions (issue #104); the host's own ~\u002F.claude\u002Fsettings.json is never read.\n  permission_mode: bypassPermissions   # bypassPermissions (default) | default\n  permissions:\n    allow: []           # write entries only if the user names specific rules\n    ask: []\n    deny: []\n  marketplaces: []      # {name, url} entries a boot-time install needs (union+\n                        # dedup by name with the bake file's). One declared HERE\n                        # only has to be reachable from the GUEST -- the build\n                        # pre-registers it as an optimization and a url it cannot\n                        # reach only warns. Write on request.\n  plugins:\n    # bake: is a BAKE key -- it lives in config-bake.yml. Writing it here aborts\n    # the launch, on a PRESENCE test: even an empty `bake:` placeholder aborts.\n    install_at_boot: []  # plugin@marketplace refs installed at boot, blocking,\n                         # before claude starts. Write only on request.\n    update_at_boot: true # refresh the marketplaces and update the installed\n                         # plugins at boot; the freshness path for BAKED plugins\n    add_marketplace_uris_to_allowlist: auto   # auto (default) | always\n    # enabled: OPTIONAL map, plugin ref -> boolean, mirroring settings.json's\n    # enabledPlugins. Every bake\u002Finstall_at_boot ref defaults enabled; write an\n    # entry only to override (false = installed-but-disabled). Write on request.\n    # enabled:\n    #   show-loaded-rules@thevoskamps: false\n\n# Guest GitHub auth.\ngithub:\n  auth: none             # none (default) | host-token\n\n# Guest environment variables. Omitted unless the user names specific ones.\n# `set` holds NON-SECRET literals (they are committed config); `copy` names\n# host environment variables read at launch, and `files` names host .env files\n# parsed at launch -- neither ever writes a secret into this file. Pair every\n# `copy` entry with the provider's host in egress.allow above, or the\n# egress-confined guest cannot reach the service the key is for.\n# env:\n#   set:\n#     FOO_ENDPOINT: https:\u002F\u002Ffoo.internal\u002Fv2\n#   copy:\n#     - OPENROUTER_API_KEY\n#   files:\n#     - ~\u002F.config\u002Ffoo\u002Fprod.env\n```\n\n> On `proxy.cmd`: the bundled tinyproxy launcher\n> (`payload\u002Fproxy\u002Ftinyproxy-launch.sh`) is the launcher-side default, so\n> the resolved global boot config leaves `proxy.cmd` unset. If the user\n> wants their own forward proxy, capture their `proxy.cmd` verbatim into\n> the boot file — the only hard requirement is that it honor the\n> launcher-provided allowlist (`$CLAUDE_VM_EGRESS_ALLOWLIST`) rather than\n> a baked-in one.\n\n**Merge variant** (existing file): start from the existing parsed YAML\nand add **only** the keys from that file's default that are absent. Keep\nevery existing key and value verbatim, including any the user\ncustomized and any this skill does not recognize. For list keys\n(bake file: `packages`, `apt_sources`, `claude.marketplaces`,\n`claude.plugins.bake`; boot file: `egress.allow`,\n`packages`, `apt_sources`, `claude.permissions.allow`\u002F`.ask`\u002F`.deny`,\n`claude.marketplaces`, `claude.plugins.install_at_boot`, `env.copy`,\n`env.files`), union\nthe default entries in (do not drop the user's extras, do not\nduplicate). Render the merged result preserving the user's existing\ncomments where practical.\n\n### Step 4: Show the proposed file(s) and get approval\n\nRender each **full proposed file** exactly as it will land on disk. For\na merge variant, also show a short summary: which keys this run adds,\nwhich it preserves untouched.\n\nThen ask explicitly, per file:\n\n> Write `~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` (and\u002For `config-boot.yml`)\n> as shown? (y to proceed, or tell me what to change)\n\nWait for explicit approval. If the user asks for changes, adjust and\nre-render here.\n\n### Step 5: Write the file(s)\n\nOn approval, use the `Write` tool to write each full file in one call.\nThe parent directory is created if needed.\n\n### Step 6: Verify and summarize\n\nAfter each `Write`, re-read the file and confirm it parses as YAML and\ncontains the expected keys:\n\n- **config-bake.yml**: `packages` (list), `apt_sources` (list),\n  `image.root_headroom_mb`, `claude.marketplaces` (list),\n  `claude.plugins.bake` (list).\n- **config-boot.yml**: `cpus: 2`, `mem: 4096`, `proxy.port`,\n  `egress.allow` including\n  `api.anthropic.com`, `claude.version`, `claude.permission_mode`,\n  `update_at_boot`, `github.auth`. `proxy.cmd` is intentionally absent —\n  the launcher defaults to the bundled tinyproxy launcher when unset.\n\nThis is content verification — `Write` already errors if the bytes did\nnot land; the re-read confirms the *intended content*.\n\nReport back:\n\n- The absolute path(s) written (or \"left untouched\" \u002F \"already complete\").\n- For a merge, which keys were added vs. preserved.\n- A reminder that the per-repo pair\n  (`\u003Crepo>\u002F.claude-vm\u002Fconfig-{bake,boot}.yml`, written by\n  `\u002Fclaude-vm-config-repo`) overrides scalars and unions lists, that\n  editing a bake file rebuilds the image while editing a boot file does\n  not, and that no secret is ever written here.\n\n## Hard constraints\n\n- **Idempotent.** Never clobber an existing config. Detect it, and offer\n  leave (default) or gap-fill merge. A merge fills gaps only — it never\n  overwrites or deletes a key the user set.\n- **`api.anthropic.com` stays in `egress.allow`.** It is required for\n  Remote Control. Never drop it from the defaults.\n- **No secrets in this file.** The host OAuth credential is mounted at\n  runtime, never written to config.\n- **Never write without explicit approval** in Step 4.\n- **Write exactly the two global files**:\n  `~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` and\n  `~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml`. This skill touches no repo,\n  edits no `.gitignore`, and runs no git commands.\n- **Respect the bake\u002Fboot placement rule.** Image-bytes keys\n  (`packages` baked in, `apt_sources`, `image.root_headroom_mb`,\n  `claude.plugins.bake`, `env.set` when it should persist in the image)\n  go in `config-bake.yml`; run-time keys go in `config-boot.yml`. A\n  misplaced key loudly does nothing rather than silently poisoning the\n  image cache, and a misplaced `claude.plugins` sub-key aborts the launch\n  outright — as do `env.copy` and `env.files` in a bake file, on a\n  *presence* test, so even an empty `env.copy:` there aborts.\n- **Never write a secret value into `env.set`.** A third-party API key\n  goes in `env.copy` (name only) or `env.files` (path only). Do not ask\n  the user to paste a key, and do not read one out of their environment\n  to echo it back — `env.copy` exists precisely so the value never\n  reaches the config or this conversation.\n","# claude-vm-config-global\n\nYou are running the `\u002Fclaude-vm-config-global` skill. Your job is to\ncreate the **global** claude-vm config **pair** at\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` and\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml` (expand `~` to the user's home\ndirectory) from the resolved defaults.\n\nSince issue #179, claude-vm's config is split into a **bake** file and a\n**boot** file per tier (four files total, all optional):\n\n- **`config-bake.yml`** — keys that change **bytes in the guest `.raw`\n  image**: `packages:` (a flat list of apt packages baked into the\n  image), `apt_sources:` (third-party apt repos rendered into the\n  image), and `image.root_headroom_mb`. The image cache key + filename\n  is a **whole-file, raw-byte hash** of the bake files, so editing a\n  bake file (including only its trailing newline) rebuilds the image.\n- **`config-boot.yml`** — keys applied at **run time** (launcher\u002FVM\n  wiring): `cpus`, `mem`, `proxy.*`, `egress.allow`, `mounts`,\n  `repo.*`, `packages:` here means **installed at boot** (a flat list),\n  `update_at_boot`, `add_apt_uris_to_allowlist`, `claude.*`, and\n  `github.*`. Boot files never affect image\n  identity.\n\nThis pair is the machine-wide layer of claude-vm's four-file config (the\nper-repo pair at `\u003Crepo>\u002F.claude-vm\u002Fconfig-{bake,boot}.yml`, written by\n`\u002Fclaude-vm-config-repo`, overrides it). The config surface, layering\nsemantics, and key meanings are documented in the sibling `claude-vm`\nskill (`skills\u002Fclaude-vm\u002FSKILL.md`) and the annotated\n`payload\u002Fconfig-bake.example.yml` \u002F `payload\u002Fconfig-boot.example.yml`.\nThis skill writes the global layer with the resolved defaults rather\nthan the examples' illustrative placeholders.\n\n**Migration:** if a legacy single-file `~\u002F.config\u002Fclaude-vm\u002Fconfig.yml`\nexists, the launcher aborts with a migration message (it will not\nsilently read it). When this skill detects that legacy file, offer to\nmigrate: split its keys into the bake\u002Fboot pair per the placement rule\nabove, write the pair, and tell the user to remove or rename the legacy\n`config.yml` afterward.\n\n## Idempotent — detect and offer, never clobber\n\nThis skill is **idempotent**. Before writing anything it checks whether\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` and\n`~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml` already exist (each independently):\n\n- **If a file does not exist**: this is the create path for that file.\n  Propose the full default file, get approval, write it.\n- **If a file already exists**: **do not clobber it.** Read it, show the\n  user what is there, and offer these choices:\n  1. **Leave** the existing file untouched (the default, safe choice).\n  2. **Merge** the resolved defaults in for any keys the existing file\n     is missing, preserving every key the user already set.\n\n  Never overwrite a key the user already set on the \"merge\" path, and\n  never delete a key. A merge only *fills gaps*. If the existing file\n  already has every key the defaults provide, report that it is already\n  complete and leave it untouched.\n\nWriting either file requires explicit user approval in every case.\n\n## Resolved defaults\n\nThese are the values this skill writes, drawn from the parent issue's\nverified analysis. They deliberately **override** the illustrative\nvalues in `payload\u002Fconfig-bake.example.yml` \u002F\n`payload\u002Fconfig-boot.example.yml`. The **file** column names which of\nthe two global files each key is written into (its bake\u002Fboot placement):\n\n| key | file | default | rationale |\n| ----- | ------ | --------- | ----------- |\n| `cpus` | boot | `2` | RAM-bound sizing; vCPUs time-slice, 2 covers git\u002Fbuild\u002Ftest bursts |\n| `mem` | boot | `4096` | the real ceiling — RAM is committed, ~8–12 VMs fit at 4 GB on a 64 GB host |\n| `proxy.cmd` | boot | omitted (bundled tinyproxy launcher is the launcher-side default) | tinyproxy is the chosen forward proxy; the launcher runs the bundled `payload\u002Fproxy\u002Ftinyproxy-launch.sh` when `proxy.cmd` is unset |\n| `proxy.port` | boot | `3128` | matches the launcher default |\n| `proxy.host_alias` | boot | `192.168.127.254` | the gvproxy host alias the guest reaches the proxy on |\n| `egress.allow` | boot | `api.anthropic.com`, `github.com`, `claude.ai`, `downloads.claude.ai` | `api.anthropic.com` is required for Remote Control; the rest cover git + claude install\u002Ffetch |\n| `claude.version` | boot | `stable` | which `claude` binary the host-side verified cache fetches |\n| `claude.renderer` | boot | omitted (claude's own default) | terminal renderer on the interactive console: `classic` \\| `fullscreen` \\| unset |\n| `claude.remote_control` | boot | omitted (`false`) | opt-in Remote Control: `true` adds `--remote-control` + a date-stamped `--name` default; `false`\u002Funset passes CLI args through |\n| `packages:` (boot file) | boot | `[]` | apt packages installed AT BOOT (a flat list; through the proxy, before claude starts) |\n| `update_at_boot` | boot | `true` | apt-get update && upgrade at boot |\n| `add_apt_uris_to_allowlist` | boot | `auto` | add derived egress URIs to the proxy allowlist only when boot-time package work needs them (`auto`), or always (`always`) |\n| `apt_sources` (boot file) | boot | `[]` | third-party apt repos a boot-time install pulls from (union+dedup with the bake file's) |\n| `claude.permission_mode` | boot | `bypassPermissions` | in-guest Claude's permission mode (`bypassPermissions` \\| `default` only; any other value aborts the launch) |\n| `claude.plugins.install_at_boot` | boot | `[]` | `plugin@marketplace` refs the guest installs at boot, blocking, before claude starts |\n| `claude.plugins.update_at_boot` | boot | `true` | refresh the marketplaces and update the installed plugins at boot — the freshness path for BAKED plugins |\n| `claude.plugins.add_marketplace_uris_to_allowlist` | boot | `auto` | marketplace-URI analogue of `add_apt_uris_to_allowlist` |\n| `claude.plugins.enabled` | boot | omitted | optional map (plugin ref → boolean) mirroring settings.json's `enabledPlugins`; overrides the default-enabled state per plugin (`false` = installed-but-disabled) |\n| `claude.marketplaces` (boot file) | boot | `[]` | marketplaces only an `install_at_boot` ref needs (union+dedup with the bake file's, by `name`); the url only has to be reachable from the GUEST — the build pre-registers it best-effort and a failure there only warns |\n| `github.auth` | boot | `none` | whether the guest is seeded with a GitHub auth token derived from the host |\n| `packages:` (bake file) | bake | `[]` | apt packages BAKED into the guest image (a flat list; present with no network at boot) |\n| `apt_sources` (bake file) | bake | `[]` | third-party apt repos rendered into the image at build time |\n| `claude.marketplaces` (bake file) | bake | `[]` | marketplaces registered INTO the image at build time (union+dedup with the boot file's, by `name`); registering one declared HERE is a build precondition — a failed add aborts the build, where a boot-declared one only warns |\n| `claude.plugins.bake` | bake | `[]` | `plugin@marketplace` refs INSTALLED into the image at build time; a BAKE key because they change the image's bytes |\n| `image.root_headroom_mb` | bake | `1024` | extra MiB of FREE SPACE in the guest root filesystem above its base content, so a live session (boot-time apt working set + ordinary growth) does not hit ENOSPC |\n| `env.set` (bake file) | bake | omitted | guest environment variables written INTO the image, as explicit literals. NON-SECRET only — a bake value is committed config AND image bytes, and persists across every run |\n| `env.set` (boot file) | boot | omitted | same shape, applied per launch and shredded on exit; overrides the bake value for the same name |\n| `env.copy` | boot | omitted | NAMES read from the environment claude-vm is launched from and forwarded to the guest. BOOT-ONLY — a hard abort in a bake file. This is how a third-party API key reaches the guest without ever being written to config |\n| `env.files` | boot | omitted | host `.env` files parsed at launch and forwarded the same way. BOOT-ONLY — a hard abort in a bake file |\n\nNotes on the forward-looking keys:\n\n- **`proxy.cmd` is omitted from the written config.** tinyproxy is the\n  chosen forward proxy, and the launcher already defaults to the bundled\n  `payload\u002Fproxy\u002Ftinyproxy-launch.sh` when `proxy.cmd` is unset (it reads\n  the egress allowlist from `$CLAUDE_VM_EGRESS_ALLOWLIST` and binds\n  `$CLAUDE_VM_PROXY_PORT`). So the resolved global config leaves\n  `proxy.cmd` unset rather than pinning a brittle invocation. Write a\n  `proxy.cmd` only if the user wants to override the bundled launcher;\n  any override must read `$CLAUDE_VM_EGRESS_ALLOWLIST` rather than a\n  hand-maintained list baked into the command.\n- **`claude.version: stable`** selects which `claude` binary the\n  host-side GPG-verified cache fetches. It is consumed by\n  `payload\u002Flib\u002Fclaude-cache.sh`: the host resolves the channel\u002Fpin to a\n  concrete version, downloads that version's GPG-signed manifest,\n  verifies the signature against the operator's pinned key,\n  checksum-verifies the binary, caches it keyed on the version, and\n  shares it into the guest, where the image's fstab mounts it `ro`.\n  `stable` (default) tracks the conservative stable channel; `latest`\n  tracks the latest channel; a dotted version like `2.1.172` pins one\n  concrete release with no channel resolution.\n- **`api.anthropic.com` must stay in `egress.allow`.** Remote Control is\n  outbound-HTTPS-only and connects to the Anthropic API on 443; dropping\n  this host breaks every in-guest Remote Control session. Treat it as\n  load-bearing, not optional.\n- **`claude.renderer` is omitted by default.** It selects the in-guest\n  claude's terminal renderer on the interactive console (`classic` →\n  `CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN=1`; `fullscreen` →\n  `CLAUDE_CODE_NO_FLICKER=1`); unset passes nothing so claude uses its\n  own default. Both renderers work over the byte-pipe console, so leaving\n  it unset is a fine default — write a value only if the user asks for\n  one. An unrecognized value aborts the launch.\n- **`claude.remote_control` is omitted by default (`false`).** It is an\n  opt-in boolean: when `true`, the launcher injects `--remote-control`\n  into the in-guest claude invocation (unless the CLI args already carry\n  it) and, when Remote Control is in effect but no `--name` was given,\n  appends a date-stamped `--name` (format like `Jul10-14:30`) so the run\n  is named. When `false`\u002Funset, claude runs without Remote Control and the\n  user's CLI args pass through unchanged. Accepts `true`\u002F`false` (or leave\n  unset); any other value aborts the launch. Ask the user whether they\n  want Remote Control on by default; write the key only when they opt in.\n- **The bake file's `packages:`\u002F`apt_sources:` and the boot file's\n  `packages:` are written empty (`[]`) by default.** Ask the user whether they want\n  any apt packages baked into the image, installed at boot, or any\n  third-party apt repos; write entries only if they name specific\n  packages\u002Frepos. Leaving these empty is the safe default — the bake\n  file's entries are baked into the image (issue #105) and the boot\n  file's are installed at boot through the proxy (issue #106).\n- **`claude.permissions.allow` \u002F `.ask` \u002F `.deny` and\n  `claude.marketplaces` \u002F `claude.plugins.bake` \u002F\n  `.plugins.install_at_boot` are written empty by default.** Same\n  reasoning: ask whether the user wants specific permission rules,\n  marketplaces, or plugins baked\u002Finstalled; write entries only on\n  request. `claude.permission_mode` and `claude.permissions.*` are\n  rendered into the guest `settings.json` (issue #104). `claude.plugins.bake`\n  refs are installed **into the image** by the build's plugin bake step\n  and `.install_at_boot` refs by the guest boot launcher's plugin phase\n  (issue #107); both are also rendered into `settings.json`'s\n  `enabledPlugins`.\n- **Placement is enforced for `claude.plugins`.** `bake` belongs in\n  `config-bake.yml` (baked plugins change the image's bytes, so they must\n  live where the whole-file image-identity hash sees them);\n  `install_at_boot`, `update_at_boot`,\n  `add_marketplace_uris_to_allowlist`, and `enabled` belong in\n  `config-boot.yml`. A sub-key written into the wrong file **aborts the\n  launch** with a message naming the file that carries it, rather than\n  parsing and being silently ignored. It is a *presence* test, so a\n  misplaced sub-key written empty (a valueless `bake:`, or\n  `bake: []` \u002F `\"\"` \u002F `{}`) aborts exactly like a populated one — never\n  write a placeholder sub-key into the file it does not belong in.\n  `claude.marketplaces` is the exception: allowed\n  in both files, unioned and deduped by `name`, with the same name under\n  two differing urls aborting the launch. Give every entry a `name:` —\n  the name is what a `plugin@marketplace` ref resolves against and what\n  registration is checked by, so an entry with a url and no name aborts\n  the launch too (issue #226). Which file it lands in still\n  decides its build-time failure policy — a bake-file entry must register\n  during the image build or the build aborts, so its url has to be\n  reachable from the build container, while a boot-file entry is\n  pre-registered best-effort and a url the build cannot reach only warns\n  (issue #226).\n- **Prefer an explicit `https:\u002F\u002F` marketplace url.** The launcher derives\n  the guest's marketplace egress hosts from the url; the `owner\u002Frepo`\n  GitHub shorthand yields no derivable host, so it needs `github.com`\n  kept in `egress.allow` by hand.\n- **`claude.plugins.enabled` is omitted by default.** It is an optional\n  map of plugin ref → boolean that mirrors `settings.json`'s own\n  `enabledPlugins` vocabulary and is rendered straight into the guest\n  `settings.json` (issue #104). Every ref in `claude.plugins.bake` \u002F\n  `.install_at_boot` is enabled by default, so write an `enabled` entry\n  only when the user wants to override that — e.g. `false` to ship a\n  plugin installed-but-disabled (toggling a debug plugin like\n  `show-loaded-rules` around a specific issue). Keys must name an\n  installed ref and values must be boolean; a typo aborts the launch.\n- **The whole `env:` block is omitted by default.** Ask the user whether\n  the in-guest claude needs any environment variables — a plain setting\n  (`FOO_ENDPOINT`) or a third-party API key for a tool, MCP server or\n  model provider other than Anthropic\u002FGitHub (`OPENROUTER_API_KEY`,\n  `TAVILY_API_KEY`). Then place each by **how its value is obtained**,\n  which is what decides the tier:\n  - a **literal** the user dictates → `env.set`, in either file. Only\n    write it into `config-bake.yml` when it should persist in the image\n    for every session; a bake value is committed config *and* image\n    bytes, so **never** put a secret there.\n  - a value that already lives in the user's **shell environment** →\n    `env.copy`, listing the NAME only. Never ask for the value and never\n    write it anywhere; the launcher reads it at launch. `env.copy` in a\n    bake file is a hard abort.\n  - a value that already lives in a host **`.env` file** → `env.files`,\n    listing the path. Also boot-only, also a hard abort in a bake file.\n\n  Precedence, lowest to highest:\n  `bake env.set` \u003C `boot env.files` \u003C `boot env.copy` \u003C `boot env.set`.\n\n  **Pair every `env.copy` with `egress.allow`.** Forwarding a key does\n  not make the service reachable — the guest is egress-confined, so the\n  provider's host must be in `egress.allow` (`openrouter.ai` for\n  `OPENROUTER_API_KEY`, `api.tavily.com` for `TAVILY_API_KEY`, …) or the\n  proxy blocks the call and the failure is baffling. Nothing is derived\n  automatically: a variable name implies no hostname the way an apt\n  source's URI does. Add both or neither.\n\n  **These entries are written verbatim, and the launcher rejects several\n  shapes**, so check before writing: an `env.copy` name that is not\n  currently exported in the user's shell **aborts the launch** (confirm\n  with the user that they export it — never print or capture the value);\n  an `env.files` path that is not on the host aborts; a name outside\n  `[A-Za-z_][A-Za-z0-9_]*` aborts; an `env.set` value that is a map or a\n  list aborts, and so does a key left valueless (write `NAME: \"\"` for the\n  empty string); and a name claude-vm's own launcher composes — every name\n  in `run.env` (the proxy vars, `CLAUDE_ARGS`, the `*_TAG` mount vars,\n  `IS_SANDBOX`, `DISABLE_AUTOUPDATER`, the renderer `CLAUDE_CODE_*` vars,\n  the terminal geometry, the `CLAUDE_VM_*_UPDATE_AT_BOOT` flags) plus\n  `CLAUDE_VM_LAST_CLAUDE_STATUS` — aborts, because the guest sources\n  `run.env` before the env files, so such an entry would overwrite the\n  launcher's own value and break the boot;\n  `CLAUDE_VM_LAST_CLAUDE_STATUS` is the exception in the other direction\n  (exported long after both files, so an entry for it would be silently\n  overwritten instead) and is refused at load for that reason.\n- **`github.auth` defaults to `none`.** Ask the user whether they want\n  the guest seeded with a GitHub auth token derived from the host\n  (`host-token`); `none` is the safe default since the consumer that\n  seeds the token lands in a sibling slice under #39.\n\n## Steps\n\nFollow these in order. Do not write the file until the user has\nexplicitly approved the proposed content.\n\n### Step 1: Resolve the target paths\n\nExpand `~` to the user's home directory. There are TWO target files:\n\n```text\n~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml\n~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml\n```\n\nRespect `XDG_CONFIG_HOME` if set: the config dir is\n`${XDG_CONFIG_HOME:-$HOME\u002F.config}\u002Fclaude-vm`, matching the launcher's\n`CLAUDE_VM_GLOBAL_CONFIG_DIR` default in `payload\u002Flib\u002Fconfig.sh`. The\nparent directory may not exist yet; the `Write` tool creates it.\n\nAlso check for a **legacy** `${XDG_CONFIG_HOME:-$HOME\u002F.config}\u002Fclaude-vm\u002Fconfig.yml`.\nIf it exists, tell the user the launcher no longer reads it and offer to\nmigrate its keys into the bake\u002Fboot pair (splitting by the placement\nrule); after writing the pair, remind them to remove or rename the\nlegacy file.\n\n### Step 2: Detect existing config files\n\nCheck each of the two target files independently.\n\n- **Absent** → create path for that file. The proposed content is the\n  full default file from Step 3.\n- **Present** → idempotent path for that file. Read its full contents,\n  parse the YAML, and show the user what is already there. Then ask (via\n  `AskUserQuestion`) whether to:\n  - **Leave it untouched** (recommended default), or\n  - **Merge** the resolved defaults into any keys it is missing.\n\n  On \"leave\", skip that file. On \"merge\", compute the gap-fill (Step 3,\n  merge variant) and continue to Step 4 for that file.\n\n### Step 3: Compose the proposed content\n\n**Create variant** (no existing file). The full default **`config-bake.yml`**\n(image-bytes keys) is:\n\n```yaml\n# claude-vm global BAKE config (machine-wide defaults).\n#\n# Keys here change BYTES in the guest .raw image, so the image cache key +\n# filename is a WHOLE-FILE, RAW-BYTE hash of this file. Editing it (including\n# only its trailing newline) rebuilds the image. The per-repo bake layer at\n# \u003Crepo>\u002F.claude-vm\u002Fconfig-bake.yml overrides scalars and unions lists. See the\n# claude-vm skill (skills\u002Fclaude-vm\u002FSKILL.md) and payload\u002Fconfig-bake.example.yml.\n#\n# NO SECRETS HERE.\n\n# apt packages baked into the guest image (flat list of names; present with no\n# network at boot). Write entries only if the user names specific packages.\npackages: []\n\n# Third-party apt repos rendered into the image at build time:\n# {name, repo, key_url}. Also allowed in the boot file (union, dedup by name).\n# Write entries only on request.\napt_sources: []\n\n# Extra MiB of FREE SPACE in the guest root filesystem above its base content,\n# so a live session does not hit ENOSPC (default 1024). A BAKE key: it sizes\n# the .raw, so changing it rebuilds the image.\nimage:\n  root_headroom_mb: 1024\n\n# Plugin marketplaces registered INTO the image, and the plugin@marketplace refs\n# installed into it, at build time. BAKE keys: baked plugins change the image's\n# bytes. `name` must match the marketplace's OWN manifest name; prefer an\n# explicit https:\u002F\u002F url so the launcher can derive its egress host. Write\n# entries only on request.\nclaude:\n  marketplaces: []\n  plugins:\n    bake: []\n\n# Guest environment variables written INTO the image (explicit literals only).\n# Omitted unless the user names specific variables. NO SECRETS: a value here is\n# committed config AND image bytes, and persists across every run of the image.\n# env.copy \u002F env.files are BOOT-ONLY and abort the launch if written here.\n# env:\n#   set:\n#     FOO_ENDPOINT: https:\u002F\u002Ffoo.internal\u002Fv2\n```\n\nThe full default **`config-boot.yml`** (run-time keys) is:\n\n```yaml\n# claude-vm global BOOT config (machine-wide defaults).\n#\n# Keys here are applied at run time (launcher\u002FVM wiring) and NEVER affect the\n# image identity -- editing this file triggers no rebuild. The per-repo boot\n# layer at \u003Crepo>\u002F.claude-vm\u002Fconfig-boot.yml overrides scalars and unions lists.\n# See the claude-vm skill (skills\u002Fclaude-vm\u002FSKILL.md) and\n# payload\u002Fconfig-boot.example.yml.\n#\n# NO SECRETS HERE. The host claude.ai OAuth credential is mounted into the guest\n# at runtime by the launcher; it is never read from or written to this file.\n\ncpus: 2\nmem: 4096\n\nproxy:\n  # proxy.cmd is intentionally OMITTED: tinyproxy is the chosen forward proxy,\n  # and the launcher defaults to the bundled tinyproxy launcher\n  # (payload\u002Fproxy\u002Ftinyproxy-launch.sh) when proxy.cmd is unset. It reads the\n  # egress allowlist from $CLAUDE_VM_EGRESS_ALLOWLIST and binds\n  # $CLAUDE_VM_PROXY_PORT. Set proxy.cmd only to override that default.\n  port: 3128\n  host_alias: 192.168.127.254\n\negress:\n  allow:                # outbound hosts permitted through the proxy\n    - api.anthropic.com # REQUIRED for Remote Control (outbound 443)\n    - github.com\n    - claude.ai\n    - downloads.claude.ai\n\n# apt packages installed AT BOOT (flat list; through the proxy, before claude\n# starts). Write entries only on request.\npackages: []\nupdate_at_boot: true    # apt-get update && upgrade at boot (default true)\nadd_apt_uris_to_allowlist: auto   # auto (default) | always\n# apt_sources: third-party apt repos a boot-time install pulls from (union+dedup\n# with the bake file's). Write only on request.\napt_sources: []\n\nclaude:\n  version: stable       # which claude binary the host-side GPG-verified cache\n                        # fetches: stable (default) | latest | \u003Cpinned>\n  # renderer: classic   # classic (no alt-screen) | fullscreen | unset. Omitted.\n  # remote_control: false  # opt-in Remote Control. Omitted by default.\n  # permission_mode + permissions.* render into the guest settings.json's\n  # permissions (issue #104); the host's own ~\u002F.claude\u002Fsettings.json is never read.\n  permission_mode: bypassPermissions   # bypassPermissions (default) | default\n  permissions:\n    allow: []           # write entries only if the user names specific rules\n    ask: []\n    deny: []\n  marketplaces: []      # {name, url} entries a boot-time install needs (union+\n                        # dedup by name with the bake file's). One declared HERE\n                        # only has to be reachable from the GUEST -- the build\n                        # pre-registers it as an optimization and a url it cannot\n                        # reach only warns. Write on request.\n  plugins:\n    # bake: is a BAKE key -- it lives in config-bake.yml. Writing it here aborts\n    # the launch, on a PRESENCE test: even an empty `bake:` placeholder aborts.\n    install_at_boot: []  # plugin@marketplace refs installed at boot, blocking,\n                         # before claude starts. Write only on request.\n    update_at_boot: true # refresh the marketplaces and update the installed\n                         # plugins at boot; the freshness path for BAKED plugins\n    add_marketplace_uris_to_allowlist: auto   # auto (default) | always\n    # enabled: OPTIONAL map, plugin ref -> boolean, mirroring settings.json's\n    # enabledPlugins. Every bake\u002Finstall_at_boot ref defaults enabled; write an\n    # entry only to override (false = installed-but-disabled). Write on request.\n    # enabled:\n    #   show-loaded-rules@thevoskamps: false\n\n# Guest GitHub auth.\ngithub:\n  auth: none             # none (default) | host-token\n\n# Guest environment variables. Omitted unless the user names specific ones.\n# `set` holds NON-SECRET literals (they are committed config); `copy` names\n# host environment variables read at launch, and `files` names host .env files\n# parsed at launch -- neither ever writes a secret into this file. Pair every\n# `copy` entry with the provider's host in egress.allow above, or the\n# egress-confined guest cannot reach the service the key is for.\n# env:\n#   set:\n#     FOO_ENDPOINT: https:\u002F\u002Ffoo.internal\u002Fv2\n#   copy:\n#     - OPENROUTER_API_KEY\n#   files:\n#     - ~\u002F.config\u002Ffoo\u002Fprod.env\n```\n\n> On `proxy.cmd`: the bundled tinyproxy launcher\n> (`payload\u002Fproxy\u002Ftinyproxy-launch.sh`) is the launcher-side default, so\n> the resolved global boot config leaves `proxy.cmd` unset. If the user\n> wants their own forward proxy, capture their `proxy.cmd` verbatim into\n> the boot file — the only hard requirement is that it honor the\n> launcher-provided allowlist (`$CLAUDE_VM_EGRESS_ALLOWLIST`) rather than\n> a baked-in one.\n\n**Merge variant** (existing file): start from the existing parsed YAML\nand add **only** the keys from that file's default that are absent. Keep\nevery existing key and value verbatim, including any the user\ncustomized and any this skill does not recognize. For list keys\n(bake file: `packages`, `apt_sources`, `claude.marketplaces`,\n`claude.plugins.bake`; boot file: `egress.allow`,\n`packages`, `apt_sources`, `claude.permissions.allow`\u002F`.ask`\u002F`.deny`,\n`claude.marketplaces`, `claude.plugins.install_at_boot`, `env.copy`,\n`env.files`), union\nthe default entries in (do not drop the user's extras, do not\nduplicate). Render the merged result preserving the user's existing\ncomments where practical.\n\n### Step 4: Show the proposed file(s) and get approval\n\nRender each **full proposed file** exactly as it will land on disk. For\na merge variant, also show a short summary: which keys this run adds,\nwhich it preserves untouched.\n\nThen ask explicitly, per file:\n\n> Write `~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` (and\u002For `config-boot.yml`)\n> as shown? (y to proceed, or tell me what to change)\n\nWait for explicit approval. If the user asks for changes, adjust and\nre-render here.\n\n### Step 5: Write the file(s)\n\nOn approval, use the `Write` tool to write each full file in one call.\nThe parent directory is created if needed.\n\n### Step 6: Verify and summarize\n\nAfter each `Write`, re-read the file and confirm it parses as YAML and\ncontains the expected keys:\n\n- **config-bake.yml**: `packages` (list), `apt_sources` (list),\n  `image.root_headroom_mb`, `claude.marketplaces` (list),\n  `claude.plugins.bake` (list).\n- **config-boot.yml**: `cpus: 2`, `mem: 4096`, `proxy.port`,\n  `egress.allow` including\n  `api.anthropic.com`, `claude.version`, `claude.permission_mode`,\n  `update_at_boot`, `github.auth`. `proxy.cmd` is intentionally absent —\n  the launcher defaults to the bundled tinyproxy launcher when unset.\n\nThis is content verification — `Write` already errors if the bytes did\nnot land; the re-read confirms the *intended content*.\n\nReport back:\n\n- The absolute path(s) written (or \"left untouched\" \u002F \"already complete\").\n- For a merge, which keys were added vs. preserved.\n- A reminder that the per-repo pair\n  (`\u003Crepo>\u002F.claude-vm\u002Fconfig-{bake,boot}.yml`, written by\n  `\u002Fclaude-vm-config-repo`) overrides scalars and unions lists, that\n  editing a bake file rebuilds the image while editing a boot file does\n  not, and that no secret is ever written here.\n\n## Hard constraints\n\n- **Idempotent.** Never clobber an existing config. Detect it, and offer\n  leave (default) or gap-fill merge. A merge fills gaps only — it never\n  overwrites or deletes a key the user set.\n- **`api.anthropic.com` stays in `egress.allow`.** It is required for\n  Remote Control. Never drop it from the defaults.\n- **No secrets in this file.** The host OAuth credential is mounted at\n  runtime, never written to config.\n- **Never write without explicit approval** in Step 4.\n- **Write exactly the two global files**:\n  `~\u002F.config\u002Fclaude-vm\u002Fconfig-bake.yml` and\n  `~\u002F.config\u002Fclaude-vm\u002Fconfig-boot.yml`. This skill touches no repo,\n  edits no `.gitignore`, and runs no git commands.\n- **Respect the bake\u002Fboot placement rule.** Image-bytes keys\n  (`packages` baked in, `apt_sources`, `image.root_headroom_mb`,\n  `claude.plugins.bake`, `env.set` when it should persist in the image)\n  go in `config-bake.yml`; run-time keys go in `config-boot.yml`. A\n  misplaced key loudly does nothing rather than silently poisoning the\n  image cache, and a misplaced `claude.plugins` sub-key aborts the launch\n  outright — as do `env.copy` and `env.files` in a bake file, on a\n  *presence* test, so even an empty `env.copy:` there aborts.\n- **Never write a secret value into `env.set`.** A third-party API key\n  goes in `env.copy` (name only) or `env.files` (path only). Do not ask\n  the user to paste a key, and do not read one out of their environment\n  to echo it back — `env.copy` exists precisely so the value never\n  reaches the config or this conversation.\n",{"name":5,"description":7},"main","4b187366f06afb374243166f6a0f5fc1514fa579","plugins\u002Fclaude-vm\u002Fskills\u002Fclaude-vm-config-global","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace","https:\u002F\u002Fgithub.com\u002FTheVoskamps","2026-11-14 13:16:57",{"source":8,"skillName":5,"commands":31,"prompt":41,"zipAvailable":42},[32,35,38],{"tool":33,"command":34},"npx","npx skills add TheVoskamps\u002Fclaude-plugins-marketplace --skill claude-vm-config-global",{"tool":36,"command":37},"bunx","bunx skills add TheVoskamps\u002Fclaude-plugins-marketplace --skill claude-vm-config-global",{"tool":39,"command":40},"pnpm","pnpm dlx skills add TheVoskamps\u002Fclaude-plugins-marketplace --skill claude-vm-config-global","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fclaude-vm\u002Fskills\u002Fclaude-vm-config-global\u002FSKILL.md",true,[],[45,52,59,66,73,80],{"skillNo":46,"name":47,"nameZh":6,"description":48,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":49,"categories":50,"keywords":51,"publishedAt":19},"01M77KH3GAXPPXNQS96KDEEZBS","mask-inappropriate-language","Replace inappropriate language (profanity, slurs, strong insults) in provided text or a file with asterisks, character-for-character, preserving everything else.","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fwriting-tools\u002Fskills\u002Fmask-inappropriate-language\u002FSKILL.md",[],[],{"skillNo":53,"name":54,"nameZh":6,"description":55,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":56,"categories":57,"keywords":58,"publishedAt":19},"01M77KGY5TZDQT8QR23Y0R8P7A","theorem-agents-interface","The interface between the sdlc reviewer and the theorem agents it spawns — what each double-dash brief parameter means, and what each consequence class means. Preloaded into every theorem agent (sdlc:theorem-generator, the disprover, and the verifier) via its skills frontmatter, and read by name by the sdlc:theorem-based-pr-reviewer agent for what its briefs' parameters mean; not invoked from the user's slash menu.","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fsdlc\u002Fskills\u002Ftheorem-agents-interface\u002FSKILL.md",[],[],{"skillNo":60,"name":61,"nameZh":6,"description":62,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":63,"categories":64,"keywords":65,"publishedAt":19},"01M77KGT5HW0XAE500WSSM0PZY","sdlc-config-user","Interactively create or merge-update the repo user sdlc config, `\u003Crepo-root>\u002F.sdlc\u002Fuser-config.yml` (tier 3 — this user, this repo, never committed), and add it to the repo's tracked `.gitignore`.","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fsdlc\u002Fskills\u002Fsdlc-config-user\u002FSKILL.md",[],[],{"skillNo":67,"name":68,"nameZh":6,"description":69,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":19},"01M77KGQGQVB0NYZX0WJRJPDEK","sdlc-config-repo","Interactively create or merge-update the shared repo sdlc config, `\u003Crepo-root>\u002F.sdlc\u002Frepo-config.yml` (tier 2 — tracked, every user of this repo).","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fsdlc\u002Fskills\u002Fsdlc-config-repo\u002FSKILL.md",[],[],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":77,"categories":78,"keywords":79,"publishedAt":19},"01M77KGKPEN7C5RCEDSEP3Z1XB","sdlc-config-global","Interactively create or merge-update the global user sdlc config, `$XDG_CONFIG_HOME\u002Fsdlc\u002Fuser-config.yml` (tier 1 — this user, every repo on this machine).","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fsdlc\u002Fskills\u002Fsdlc-config-global\u002FSKILL.md",[],[],{"skillNo":81,"name":82,"nameZh":6,"description":83,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":84,"categories":85,"keywords":86,"publishedAt":19},"01M77KGFX95J2XEQBBD49QHTXK","pr-read-cli-interface","The contract for sdlc's PR-read CLIs — sdlc-pr-round, sdlc-pr-adjustments, sdlc-fixer-brief and sdlc-records-chain — their arguments, exit codes and output grammar. Preloaded into sdlc:theorem-based-pr-reviewer, sdlc:issue-fixer, sdlc:docs-writer and sdlc:pr-finalizer via their skills frontmatter; not invoked from the user's slash menu.","https:\u002F\u002Fgithub.com\u002FTheVoskamps\u002Fclaude-plugins-marketplace\u002Fblob\u002Fmain\u002Fplugins\u002Fsdlc\u002Fskills\u002Fpr-read-cli-interface\u002FSKILL.md",[],[]]