---
title: "VM-0001 Has a 91-Day Snapshot: Anonymising vSphere Data Before It Reaches Claude"
description: A follow-up to the govc skill for Claude Code — a pseudonymising wrapper that replaces every VM name, IP, MAC and UUID with a stable token before the model ever sees it, and rehydrates them on your screen.
---

[Opvizor Blog](https://opvizor.com/blog)

# [VM-0001 Has a 91-Day Snapshot: Anonymising vSphere Data Before It Reaches Claude](https://opvizor.com/blog/vm-0001-has-a-91-day-snapshot-anonymising-vsphere-data-before-it-reaches-claude)

 Written by [Dennis](https://opvizor.com/blog/author/dennis) | Aug 6, 2026, 7:00:00 AM

*A follow-up to [Teaching Claude Code to Speak vSphere: A govc Skill for VMware Admins](https://opvizor.com/blog/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](https://github.com/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.*

[View full post](https://opvizor.com/blog/vm-0001-has-a-91-day-snapshot-anonymising-vsphere-data-before-it-reaches-claude)

```json
{
  "@context" : "http://schema.org",
  "@type" : "BlogPosting",
  "author" : {
    "@type" : "Person",
    "name" : "Dennis"
  },
  "dateModified" : "2026-08-06T10:07:40.455Z",
  "datePublished" : "2026-08-06T07:00:00Z",
  "headline" : "VM-0001 Has a 91-Day Snapshot: Anonymising vSphere Data Before It Reaches Claude",
  "image" : {
    "@type" : "ImageObject",
    "height" : 1272,
    "url" : "https://26616011.fs1.hubspotusercontent-eu1.net/hubfs/26616011/govc-safe-flow.png",
    "width" : 1968
  },
  "mainEntityOfPage" : "https://opvizor.com/blog/vm-0001-has-a-91-day-snapshot-anonymising-vsphere-data-before-it-reaches-claude",
  "publisher" : {
    "@type" : "Organization",
    "logo" : {
      "@type" : "ImageObject",
      "height" : 60,
      "url" : "/hs/hsstatic/content_shared_assets/static-1.4092/img/default-amp-logo.png",
      "width" : 60
    },
    "name" : "Opvizor Blog"
  }
}
```