Opvizor Blog

VM-0001 Has a 91-Day Snapshot: Anonymising vSphere Data Before It Reaches Claude

Written by Dennis | Aug 6, 2026, 7:00:00 AM

A follow-up to Teaching Claude Code to Speak vSphere: A govc Skill for VMware Admins.

The last post ended on a comfortable note: the skill's safety rules held, every command was read-only, and Claude reconstructed a seven-month-old host outage from rotated vmware.log files without touching a thing. Several people wrote back about the same sentence, though — the one where I mentioned running against a local model if cloud inference is a non-starter.

Because that framing was too binary. Most teams don't want a different model. They want the same model and a smaller blast radius for what it sees.

Here is the uncomfortable part I glossed over. A single govc vm.info -json across a mid-size estate puts every VM name, every guest hostname, every IP and MAC address, every annotation, and whatever someone typed into a snapshot description four years ago straight into the conversation — and out to whichever provider is serving the model. Nobody decided to do that. It's just what the command returns. ACME-PROD-SQL01, pre-upgrade before Contoso migration INC-88213, 10.20.30.41. Your customer names are in your inventory, and your inventory is now in a transcript.

So the repo grew a second optional layer: govc-safe, a pseudonymising proxy that sits in the data path and makes sure the normal path never puts a real identifier in front of the model.

The Idea in One Line

govc-safe holds the credentials, runs the real command, and rewrites every identifier to a stable token before anything is printed.

$ govc-safe find / -type m
/DC-01/vm/VM-0001
/DC-01/vm/VM-0002

VM-0001 is not a placeholder. It is a stable token: the same VM is VM-0001 in every command and every session, which means the model can still correlate. If VM-0001 shows up in a snapshot audit on Monday and again in a capacity report on Friday, it is the same machine — and Claude can say so without ever knowing what it is called.

Tokens also go back in. govc-safe vm.power -off VM-0017 resolves the token to the real object before calling vCenter. From the model's point of view nothing has changed: it plans, it queries, it acts. It just does all of it in token space.

How It Fits Together

Nine steps, four of which are worth explaining.

Learning the estate (step 6). Real identifiers do not sit politely in fields called name. They turn up inside composite strings — [LocalDS_0] MyVM/MyVM.vmx — so walking JSON keys is not enough. Before each run the wrapper enumerates the inventory with find -l -i /, mapping every managed object reference to its display name, then makes a second pass over snapshots (which find does not enumerate, and whose names are among the worst offenders in practice). Everything it learned becomes one substitution pass that fires wherever a name appears, in any shape.

Tokens are keyed on the MoRef wherever govc reports one, not on the name. Rename a VM and it keeps its token — which is the behaviour you want, because a rename is not a new machine.

Substituting (step 7). On the JSON path the wrapper walks the parsed document and rewrites values only. Keys and structure come back exactly as govc emitted them, so jq recipes written for plain govc keep working. Managed object references survive as tokens — "self": {"type": "VirtualMachine", "value": "VM-0001"} — which gives the model a join key for correlating two outputs without ever seeing a name.

Free text is a different problem, and it does not have a good solution. Annotations, snapshot descriptions and event messages are arbitrary prose; there is nothing structural to pseudonymise them against. So on the JSON path they are dropped, not rewritten: [redacted: free text]. That is a real capability loss and the skill tells Claude to say so rather than report "this VM has no annotation".

The allowlist (step 3). govc-safe is not a passthrough. Unknown verbs and unknown flags are refused, so a future govc flag cannot quietly become an exfiltration channel. guest.*, host.esxcli, logs (the bundle verb — logs.ls is permitted), datastore.tail, permissions.ls, sso.* and about.cert are out because their output cannot be redacted safely. -trace, -debug, -dump and -xml are out because they write raw SOAP or Go dumps past the redactor entirely — -trace in particular puts a live session cookie on stderr.

Refusals come back as ordinary tool errors with a reason, so Claude adapts instead of stalling. The companion skill says it plainly: a refusal is not a bug and not a permissions problem — do not work around it.

Rehydration (step 9). This is the part that makes the whole thing bearable to use. A MessageDisplay hook rewrites Claude's replies as they render on your screen. The model writes:

VM-0001 is powered on with a 91-day snapshot SNAP-01 on DS-01

and your terminal shows:

ACME-PROD-SQL01 is powered on with a 91-day snapshot pre-upgrade INC-88213 on LocalDS_0

Display-only is the entire trick. The transcript and the model both keep the tokens; the real names are substituted locally, after the response comes back, and never travel anywhere. /verbose shows the untouched text if you want to confirm exactly what left the machine. For a cleartext copy on disk there is govc-safe rehydrate report.html, which is for you — the hook explicitly denies it to the agent, because running it would pull every real name into the transcript and defeat the point.

What It Looks Like on a Real Estate

The repo now ships three generated health checks: the same nine-check report against vcsim with and without the wrapper, and one collected read-only through the wrapper against a production environment. That last one is the honest one, so here is what it actually said.

Two datacenters, two clusters, one host, 44 VMs, four datastores. vCenter endpoint: not exposed. Two critical findings — an expired vCenter appliance root password, flagged by an unacknowledged red alarm with a yellow precursor that had signalled it beforehand; and four VMs with snapshots older than seven days, the oldest at 1358 days. DS-04 fullest at 72%. HOST-05 up 220 days. And two of nine checks not completed, stated as such rather than quietly skipped.

Nothing about that analysis needed a real name. It needed a count, an age, a percentage, and the ability to say which four — and VM-0045 (1358 d), VM-0034 (1214 d), VM-0007 (109 d), VM-0012 (9 d) does that job perfectly. On screen those read as the real machines. In the file, and in the transcript, they are tokens.

What the Environment Taught Us, Again

A VM named config. Redaction originally ran over the serialised JSON text, so the name alternation matched inside keys as readily as inside values, and a VM called config turned the config object itself into VM-0001. Short names like log, name, host and db exist in every grown environment. The fix was structural: walk the tree, rewrite values, leave keys alone.

Property paths look exactly like hostnames. runtime.connectionState and web-01.corp.local are both dotted identifiers, so the FQDN sweep rewrote every property and counter name to an FQDN-nnn token. The document still parsed and still had the right numbers in it — it just no longer said what any of them were. The same sweep ate vCenter's own alarm constants (alarm.HostMemoryUsageAlarm), which needed a second, narrower exemption.

Alarm labels. An alarm can be named anything an admin typed, including after a customer, and it was passing through verbatim while the description next to it was being dropped. Blanking it would have left [redacted] (red), which tells an administrator nothing and cannot be diffed between runs. It is tokenised instead — ALARM-02 (red) counts, correlates and diffs like any other object.

And one actual leak. datastore.ls in plain form returns bare directory entries with no [DS-01] prefix and no dir/ in front, so the path-tokenising rules never matched. The names that escaped were folders belonging to VMs that had been renamed — the folder keeps the old name, and the old name is not in the inventory map. datastore.ls is now forced to -json, and it is why the test suite matters more than the design does.

Verifying It, in Both Directions

./wrapper/test-redaction.sh    # does anything leak?      (no install needed, runs against vcsim)
./wrapper/test-deployment.sh   # is my setup correct?     (after setup.sh)

The redaction suite runs the read verbs the reports actually depend on against vcsim — including snapshots seeded with deliberately identifying names and a VM named after a JSON key — and fails if any real identifier, IP, MAC or UUID survives.

It also grades the opposite direction. A wrapper that deleted everything would score a perfect pass, so the suite asserts that keys, structure and MoRefs come through intact. Over-redaction is not a leak, but it is still a broken tool.

The deployment suite checks what the engine test cannot: that govc-safe is on your PATH, that your shell is not exporting GOVC_* (an export GOVC_PASSWORD=... in your .zshrc hands the agent working credentials and bypasses the wrapper entirely), that the creds file is 0600, that plain govc cannot authenticate from a clean environment, and that the hook actually rewrites govc into govc-safe and denies rehydrate.

Enabling It

./wrapper/setup.sh          # govc-safe + the hook to ~/.local/bin, govc-private to ~/.claude/skills

Per-user, no root. It moves your vCenter credentials into ~/.config/govc-safe/creds (mode 0600), installs the companion skill, and wires three hooks plus a Bash(*GOVC_PASSWORD*) deny rule into ~/.claude/settings.json.

Back up settings.json yourself first. The installer merges rather than overwrites, preserves your existing hooks and matcher groups, converges on re-run, and takes its own timestamped backup — but that backup sits next to the file it protects and is written by the same code you are trusting. cp ~/.claude/settings.json ~/settings.json.mine costs nothing. --no-settings installs everything and touches nothing if you'd rather wire it by hand.

The govc/ skill itself needs no changes. The PreToolUse hook rewrites every invocation in the command, both sides of a pipeline included, so the skill from part 1 keeps working exactly as written and there is no second command name to learn.

Linux and macOS. On Windows, run it from WSL or Git Bash — the Python is portable but no native PowerShell install has been tested.

What You Give Up

One capability, and it is the one part 1 showed off: log forensics. Reading a VM's vmware.log off the datastore is how the plain skill answered "what happened to this VM last week" once vCenter's event retention ran out. It needs datastore.tail or datastore.download, and both are refused here — a log is unbounded free text carrying guest hostnames, in-guest paths and usernames that appear nowhere in the inventory, so there is nothing to tokenise them against. The skill's instruction is to say so plainly and name the step, not to quietly answer a smaller question. You run that one yourself.

And structure still leaks by design. Counts, cluster shape, guest-OS versions and ESXi build numbers survive pseudonymisation, because removing them removes the point of the reports. If build numbers are themselves sensitive in your environment — they map to published CVEs — that is worth knowing up front.

What This Is, and What It Isn't

It is an anonymisation tool, not a security boundary. govc-safe runs as the same user you do, and the hook matches command text, which loses to enough creativity. An agent that deliberately set out to bypass it could.

What it eliminates is the failure mode that actually happens: a routine vm.info -json dumping every VM name, IP and annotation into a transcript because nobody thought about it. That one is gone.

If you need a hard boundary against a hostile agent, this is the wrong layer — run Claude Code as a separate unix user that genuinely cannot read the credentials. And none of this replaces a least-privilege vCenter role, which remains the only thing that prevents unwanted changes. The layers compose: the skill supplies judgment, the policy hook supplies deterministic enforcement, the wrapper supplies anonymity, and vSphere RBAC supplies the floor that holds when everything else is bypassed.

The wrapper is optional. Everything in part 1 works without it. But if the reason you haven't pointed an agent at vCenter yet is that you didn't want your inventory in someone else's logs — that objection now has an answer, and it's about forty lines of setup.

Everything is on GitHub at vchaindz/claude-vsphere-skill — the skill in govc/, the policy hook in guard/, the wrapper in wrapper/. Point it at vcsim first, run wrapper/test-redaction.sh before you trust it, and tell me what leaks.