mcpbeat Sign in

Windows VM Detection Post Wmic Agent Skill

Detect that a desktop app is running inside a virtual machine on Windows after the classic command-line management query tool was removed in Windows 11 — query the management layer through PowerShell, probe both the manufacturer and the model field, and pick the fail direction deliberately; reach for it when a transparent or undecorated window renders nothing on a VM while the process keeps running, or when a detection probe that worked for years suddenly reports empty on every modern host.

1k tokens
context cost
the whole folder, loaded on every use
1
files
instructions only
0
copies elsewhere
how many repositories repackaged it
100
stars on the repo
on the repository, not the skill itself

Install

one command, takes just this skill from the repository
npx skills add https://github.com/maxrave-dev/kotlin-footguns --skill windows-vm-detection-post-wmic

The instruction itself

3 sections, as written by the author

Detecting a virtual machine on Windows

Some window features do not survive a VM's graphics driver. A transparent, undecorated window is

the usual casualty: it never paints, the process stays alive, and the user sees nothing at all.

The fix is to detect the VM and fall back to a normal decorated window — which makes the *detection*

the load-bearing part.

The system fields that carry a hypervisor's brand live in the computer-system record of Windows'

management layer. Read them, match against known vendor strings.

// adapted — extracted from the window-construction path, inside a `remember { }`
// Guard first: both probes below are Windows-only commands. Without this the
// app spawns a doomed subprocess on every macOS and Linux launch.
val osName = System.getProperty("os.name", "")
if (!osName.contains("Windows", ignoreCase = true)) return@remember false

val probes = listOf(
    listOf(
        "powershell", "-NoProfile", "-Command",
        "(Get-CimInstance Win32_ComputerSystem | " +
            "Select-Object Manufacturer,Model | " +
            "Format-List | Out-String).Trim()",
    ),
    listOf("wmic", "computersystem", "get", "manufacturer,model"), // older hosts only
)
val sysInfo = probes.asSequence()
    .mapNotNull { cmd ->
        runCatching {
            val p = ProcessBuilder(cmd).redirectErrorStream(true).start()
            val out = p.inputStream.bufferedReader().readText()
            if (p.waitFor() == 0 && out.isNotBlank()) out else null
        }.getOrNull()
    }.firstOrNull().orEmpty()

val vmTokens = listOf("Parallels", "VirtualBox", "VMware", "QEMU", "KVM", "Xen", "Hyper-V")
val isVM = vmTokens.any { sysInfo.contains(it, ignoreCase = true) } ||
    System.getProperty("app.window.no-transparent", "false").toBooleanStrictOrNull() == true

Then: Window(undecorated = !isVM, transparent = !isVM, …).

Traps

The classic command-line query tool is gone. It was deprecated in Windows 10 21H1 and is not

installed by default on Windows 11 — a version-scoped fact, so keep it as the *fallback* probe

rather than deleting it, since older hosts still have it and may not have a usable PowerShell path.

On a modern host the old tool answers "not recognized", the probe reads back empty, and the code

concludes "real hardware". This is exactly how a detection that shipped working degrades into a

detection that has silently never run.

Probe BOTH the manufacturer and the model field. Which one carries the brand differs per

hypervisor and per architecture — one product puts its company name in the manufacturer field and

its product name in the model field; another puts a company name you would never guess in the

manufacturer field and the recognisable brand only in the model. Reading one field is how a

detection passes on your laptop's VM and fails on a user's.

Decide the fail direction on purpose, and know what it costs. Detection failure here resolves

to "not a VM", which keeps the enhanced window. That is the right default for a hardened or

unusual host that simply refuses to answer — it should not lose features for being quiet. But it

also means *a broken probe is indistinguishable from real hardware*, and its symptom is the worst

one available: an invisible window. If your fallback path is the cheap one instead, invert it.

Either way, write down which way it fails and why.

Ship a manual override. No token list is exhaustive and no probe is guaranteed to answer. A

system property (or an environment variable, or a settings toggle) that forces the fallback path

turns "unusable, file a bug and wait for a release" into "add one flag". Note that the override in

the excerpt only forces the *safe* direction — there is deliberately no way to force transparency

on, because that is the state that renders nothing.

Run the probe once and remember it. It spawns a process. Doing that on every recomposition, or

on every window resize, is a visible stall. Compute it once at window construction.

**Redirect the error stream into the output you read, and require a zero exit *and* non-blank

output.** A probe that writes its complaint to standard error and exits zero otherwise looks like a

successful empty answer.

Do not extend the token list by guessing. Vendor strings are not documented as an API and

change between product generations.

Verifying it

The token list is data, not knowledge — regenerate it rather than trusting this file. On any host

you care about, run the query itself and read what it prints:

Get-CimInstance Win32_ComputerSystem | Select-Object Manufacturer,Model | Format-List

Two checks that catch the failure this skill exists for:

  • Prove the probe ran. Log the raw captured text, not just the boolean. An empty string with

isVM = false and a real string with isVM = false are entirely different situations and the

boolean cannot tell them apart.

  • Prove the fallback works on a real guest, not on a resized window on your own machine. The

render failure comes from the guest's graphics driver; nothing on the host reproduces it.

How to use it

Copy the folder

Take maxrave-dev/windows-vm-detection-post-wmic from the repository into ~/.claude/skills for personal use, or into .claude/skills inside a project.

Check the name does not clash

The agent identifies a skill by the name field in its header. Two skills with the same name cannot sit side by side — one of them will be ignored.