Agent VM .claude Share Set¶
This guide explains which parts of the host's ~/.claude directory are
shared into agent VMs, how conversation resume survives a VM rebuild, and
why plugins and the session roster are deliberately kept VM-local. The
wiring lives in src/vergil_tooling/lib/lima.py (create_vm,
link_claude_dirs, copy_claude_config); the vergil-vm template only
declares the static projects mount.
The share set below describes local (Lima) VMs. Cloud VMs cannot share the host store live, so they receive canonical agent data as a read-only cached projection instead — see Cloud VMs: read-only memory projection.
The share set¶
| Subdir | How it is shared | Writable | Why |
|---|---|---|---|
projects/ |
virtiofs mount + symlink | yes | Durable conversation transcripts; must survive rebuilds. |
skills/ |
virtiofs mount + symlink | no | Read-only reference. |
sessions/ |
not shared (VM-local) | n/a | Disposable per-VM roster; sharing breaks atomic writes (EXDEV). |
plugins/ |
not shared (VM-local) | n/a | Installed/refreshed in-VM from GitHub marketplaces. |
copy_claude_config additionally copies CLAUDE.md and settings.json
into each VM on every start, rebuild and session, on cloud create, and on
every vrg-vm update (which requires the host settings.json and fails,
naming it, when it is missing). The guest CLAUDE.md is a read-only copy of
the host's on every transport: copy_claude_config is the sole owner of its
file mode and runs chmod a-w on it after each copy, and removes the guest
copy when the host has none. The mode makes an accidental write fail loudly;
it is not a security boundary, because the guest user owns the file and can
change its mode. settings.json stays writable, because provisioning and the
Claude runtime both write it.
Why resume survives a rebuild¶
Conversation transcripts are written as append-only *.jsonl files under
~/.claude/projects/<slug>/. That directory is a symlink onto the
path-preserved host mount, so the transcripts live on the host and are
untouched when a VM is destroyed and recreated. Resuming a conversation
reads those transcripts — not the sessions/ roster — so a rebuild
never loses history.
Why sessions/ is VM-local¶
~/.claude/sessions/ holds a small roster of pid -> session files that
Claude writes atomically: it writes a temp file in the VM-local tmpdir and
then rename()s it onto the target. Renaming across filesystems (VM-local
ext4 to the virtiofs host mount) fails with EXDEV, so the write would
silently fail and no roster file would ever appear. The roster is also
per-machine (pids only mean anything on the owning host), so there is no
value in sharing it. Session detection reads each VM's local roster
in-guest over limactl shell. See vergil-tooling #1301 and vergil-vm #73.
A host
~/.claude/sessionsmount used to exist (mounts[3]) but the VM never read it once the roster was made VM-local; it was removed as dead weight (#1603).
Why plugins are VM-local, not shared¶
Plugins are declared in the host settings.json (enabledPlugins and
extraKnownMarketplaces), which is copied into each VM. The marketplaces
are GitHub repositories, so each VM installs the enabled plugins itself
on first launch and keeps them current with an in-VM refresh — the same
model used for vergil-tooling. The host's materialized ~/.claude/plugins
checkout is never shared: doing so would cross the macOS/Linux boundary
(fragile if any plugin ships a binary) and hit the same EXDEV write
problem as the roster. Instead, update_plugins runs the shared refresh in
lib/claude_marketplace.py, the same one vrg-host plugins runs on the
host. It reads the guest settings.json; a missing, unreadable or invalid
file is a failure, never "nothing declared". It first registers each
extraKnownMarketplaces entry with claude plugin marketplace add, keeping
any ref the entry declares (owner/repo@ref for a GitHub source,
url#ref for a git URL): Claude Code refuses an add whose source does not
match the declared entry. Once the settings declare a new ref, this re-add
re-clones the marketplace at that ref, which is how a VM switches ref. It
then refreshes each marketplace it added by name (claude plugin
marketplace update <name>), including claude-plugins-official when it
added that one for an enabled plugin. Any other marketplace registered in the
VM is never updated, so a stale leftover cannot fail the refresh; a plugin
enabled from one is still updated, from that marketplace's catalog as it
stands. It then updates every install of an
enabled plugin to its latest version, each at its own scope (claude plugin
update <id> --scope <user|project> — there is no bulk-update form), all
inside the VM. One plugin can be installed at user scope and again at project
scope for each project; claude plugin list --json lists each install
separately, and each one is updated. The CLI has no project flag, and takes
the project from the working directory, so a project install is updated from
inside its projectPath. If that directory no longer exists in the VM, the
install is reported as a failure that names its scope and project. An enabled
plugin with no install at any scope is installed at user scope. Last, it
verifies that each declared GitHub marketplace is registered, as claude
plugin marketplace list --json reports it, at the declared repo and ref; a
mismatch (including no registered ref when the settings declare one) or a
missing registration is a failure. Every failure
is named and raised, and a failed claude command is reported with its
stdout and stderr as well as its exit status. The refresh is driven by vrg-vm update, which first
copies the host's current settings.json into the running VM, and by a
warn-mode stage on VM start/rebuild. vrg-vm update --all skips stopped VMs
(they pick up the settings on their next start), and one VM's plugin failure
does not stop the rest from updating.
Cloud VMs: read-only memory projection¶
The share set above works because local (Lima) VMs path-preserve the
project mount and symlink ~/.claude/{projects,skills} onto the host's
live directories, so a memory write in a local-VM session flows straight
through to the durable host disk. Cloud VMs — off-platform x86 boxes
backed by the ephemeral /vergil data disk — cannot do that. Their disk
is nuked and rebuilt freely, and the checkout lives at a different path,
so a memory write there would either be lost on the next rebuild or land
under a slug no other environment reads. The corrosive part is that the
loss is silent: the harness advertises the memory directory as
persistent, an agent writes memory believing it durable, and the write
quietly evaporates.
To keep the physical host the single source of truth, a cloud VM receives
canonical agent data — the per-repo memory/ subtree, including
MEMORY.md — as a read-only cached projection that never flows back
automatically. (The global CLAUDE.md is not part of the projection:
copy_claude_config copies it and makes it read-only on every transport,
as described above.) See epic
vergil-project/.github#156
and its
spec
for the full rationale, trust model, and failure modes.
The three platforms¶
A single platform-awareness signal distinguishes three states along two axes — sandbox (in a VM) vs. host (not a VM), and cloud vs. local:
| Platform | Sandbox? | Credentials | Canonical store |
|---|---|---|---|
physical-host |
no | human | live and writable (source of truth) |
local-vm |
yes | agent (GitHub App) | shared live via path-preserved mounts (durable) |
cloud-vm |
yes | agent (GitHub App) | read-only copy on ephemeral disk |
physical-host names the property that matters — physical, not a VM —
not the operating system; it is macOS today only because that is the
hardware in use. The read-only memory control (the memory/ subtree only)
activates only on cloud-vm: local-vm writes already reach the live host store, and
physical-host is the store.
The platform resolver (vrg-whoami --platform)¶
vrg-whoami --platform reports the platform as a single token, parallel
to the existing --mode; vrg-whoami --explain reports the resolving
signal and warns on disagreement. The resolver is empirical — it
derives the platform from heuristics (a /vergil mount ⇒ in a VM; a
reachable cloud metadata endpoint ⇒ cloud; host OS and resolved identity
as corroboration), with no written marker file.
It fails closed for the memory control: if a VM signal is present but
the box cannot be positively confirmed as local-vm, it is treated as
cloud-vm (locked), never as physical-host. Fail-open — silently
deciding "host" and re-enabling futile writes — is the exact failure this
model exists to kill, and is prohibited.
Read-only projection, refreshed at session start¶
Projection is coupled to vrg-vm cloud-session start, not build: a
cloud VM is not used until a session opens, so the first session on a
freshly built box performs the initial projection and every subsequent
session refreshes it. Before opening the session, vrg-vm:
- aligns the guest working directory to the host project path (via a
symlink onto the
/vergilcheckout) so Claude derives the same memory slug as the host and finds the projected memory; - copies that repo's
memory/+MEMORY.mdfrom the host over the established transport idiom (the samecat > <file>mechanismcopy_claude_configuses — there is norsyncover the transport); - re-applies surgical read-only permissions — locking
memory/andMEMORY.mdwhile leaving the session-transcript.jsonlfiles writable (a blanketchmodofprojects/<slug>/would break session logging); - verifies that the host path resolves in the guest and the slug memory directory is non-empty, failing loudly otherwise — a broken indirection would otherwise degrade silently to empty memory.
Steps 2–4 run only when the invocation names a session (--label,
--resume, --fresh) or overrides the command (-- bash). A bare
vrg-vm session <workspace> only lists the workspace's sessions and
exits, so it skips the projection; the reconnect that names a session
performs it.
The read-only permissions are the hard backstop: a write attempt in the
cloud fails with EACCES — loudly, so a futile write can no longer
succeed-then-vanish.
The policy clause (soft control)¶
The hard read-only permissions are paired with a soft control: a
condition-gated clause in the canonical ~/.claude/CLAUDE.md that stops
an agent from attempting a cloud memory write in the first place. Its
substance: when vrg-whoami --platform reports cloud-vm, memory and
copied config are a read-only cache; never write memory — the write
would fail (EACCES) and would be futile anyway — and when memory needs
to change, file it via /vergil:intake-capture for implementation on
the host. Because that file is the developer's personal, global config,
the clause is authored once on the host and rides the copied file into
every environment; its condition is simply almost never true outside
cloud-vm. Keeping the clause part of the canonical file also keeps
CLAUDE.md byte-identical across environments, so it stays lockable.
Issue-driven writeback¶
Canonical data never flows back automatically. When a cloud session
determines that memory or config must change, it files an issue through
the existing /vergil:intake-capture flow, producing an intake issue
under the org's intake epic in .github. The change is later implemented on the
physical host against the live store and re-projected on the next cloud
session or rebuild. Given how infrequently this data changes, this
issue-driven, human-executed writeback is sufficient — no automated
two-way sync.
See also: Identity Architecture.