Hidden Unicode in agent rules

Check V14 · agent family · automatic · one of the 17 VS Code Marketplace checks

The question this check asks: do the agent rule files packed inside this extension — CLAUDE.md, AGENTS.md, .cursorrules, an .mcp.json, the .claude/ settings — contain characters that render as nothing?

Why it matters

V14 is the same detection function as GitHub's C19 and Hugging Face's H15, run on files read out of the .vsix package rather than a git tree or the Hub's file list. The technique it looks for is the Rules File Backdoor (Pillar Security, March 2025): instructions placed in an agent's rules file using codepoints that occupy no visible space — zero-width characters, bidirectional overrides, the Unicode Tags block — so that a person skimming the file sees ordinary guidance and the agent reading it does what the hidden text says. Nothing about that mechanism cares how the file arrived. A CLAUDE.md that came out of an extension package is read by an agent exactly as one that came out of a clone.

The package context changes one thing, and V13 accounts for it: an installed extension's directory is not a workspace an agent opens, so the presence of an agent file inside a package is a note here where C18 treats it as HIGH. Five of the 100 most-installed extensions ship one — prettier its repository's CLAUDE.md and .claude/settings.json, Microsoft's cmake-tools and makefile-tools an .mcp.json, vscode-icons and markdown-preview-enhanced an AGENTS.md — and none of them is a fault. The contents are a different matter: hidden text does not become harmless because the file was zipped, and whoever opens the extension's folder to read its source, copies the rules file into a project, or starts an agent in that directory gets the instruction whole. So V13 is downgraded and V14 keeps its CRITICAL, wherever the file sits.

Two Marketplace incidents used invisible Unicode, and this check would have seen neither — both need saying plainly. GlassWorm's loader hid in invisible Unicode variation selectors (Koi Security, October 2025; the Eclipse Foundation's Open VSX security update of 27 October 2025) — in JavaScript, which this tier does not read; V14 scans the agent rule files and nothing else. Secure Annex's "GlassWorm continued" of 30 November 2025 counted 19 Microsoft-Marketplace identifiers carrying the same Unicode trick beside Rust implants — still in code, still outside this check's input. And Checkmarx's juanblan281.solid281 (30 January 2026) hid three zero-width characters inside its display name — "solidity" with invisible characters between the letters — a listing field, not a file in the package, so not this check's input either. The display name is compared by V4 against known names for equality; no check today scans it for invisible codepoints. The same family of technique aimed at a different surface is a gap this page states rather than a catch it claims.

How RepoGates scores it

V14 reads the agent files V13 names — CLAUDE.md, AGENTS.md, .cursorrules, .clinerules, .windsurfrules, .github/copilot-instructions.md, .cursor/rules/*.mdc, .mcp.json, .cursor/mcp.json and the JSON files under .claude/ — out of the package itself. The file list comes from two ranged GETs against the .vsix's central directory; each matched entry is fetched by a third, capped at eight files of up to 64 KB, and the package is never downloaded whole. Every file that decodes is scanned for codepoints in U+200B–U+200F, U+202A–U+202E, U+2060–U+2064, U+2066–U+2069, U+FEFF and U+E0000–U+E007F — the range C19 and H15 scan for, from the same function. Any hit is CRITICAL, costs 45 points, and makes the verdict BLOCK: any CRIT finding drives the score to 0 regardless of what the rest of the roster found. The finding names the file, the count and up to three of the codepoints' Unicode names, never the decoded text. The default personal profile sets this check's action to block — the one content check of the three whose action is not warn. The scoring table's evidence note:

"Rules File Backdoor; zero FPs observed"

V14 sits in two of the four dimension verdicts — Open in an editor and Hand to an AI agent — and each takes the worst of its checks, so one hit turns both to BLOCK while the Clone and Install dimensions read on their own evidence. When the package listing cannot be fetched, V13 through V16 are reported as skipped, not passed; when it can and no agent file is in it, there is nothing to hide instructions in and the check passes.

False positives we know about

None observed on the package contents this check has scanned. In the 18 September 2026 measurement of the 100 most-installed extensions — 99 PASS, 1 REVIEW, 0 BLOCK, every listing read — V14 fired zero times. The five agent files that measurement found were read in full and none carried an invisible codepoint; the one REVIEW came from V5 on abusaidm.html-snippets, an established listing with no verified domain and no source, not from anything in a file. The GitHub-side control figures C19 cites are GitHub numbers and are not restated here. The one false positive C19 expects to meet eventually — a byte-order mark, U+FEFF, at the top of a file saved by a Windows editor — applies here without change, since it is a property of the file's bytes, not of where the file was packaged.

What this check does not cover

It scans the agent rule files on the list, and only those. The extension's JavaScript is not read on this tier — the GlassWorm loader is the standing example of invisible Unicode in exactly the place this check does not look. Listing fields are not files: a display name, description or README carrying zero-width characters (the solid281 shape) passes this check untouched. A rules file larger than 64 KB is listed by V13 but its contents are not read, so V14, V15 and V16 do not run on it; the same is true of the ninth and later agent files in a package. It looks for invisible characters — homoglyphs from another script are a different technique and are not caught, and neither is an instruction written in plain, readable prose. And it reads what the package on the Marketplace holds at the time of the request, not your agent's own configuration outside it.

RepoGates assesses a Marketplace extension on request — through the API, the MCP server (platform="vsx") and the preflight for code --install-extension. It does not see an extension installed from the editor's own UI, or one that auto-updates; the board on marketplace pages is not built yet; Open VSX (where VSCodium, Cursor and Windsurf install from) is a different registry and is not assessed. The extension's code bundle is not read on this tier: a PASS means the provenance and the declared surface are clean, never that the JavaScript was inspected.

Check it yourself

Without installing anything: the listing page's Resources column carries a Download Extension link, and a .vsix is a zip with the extension's own files under extension/. List the agent files it carries, then scan any it has:

unzip -l EXTENSION.vsix | grep -E 'extension/(CLAUDE\.md|AGENTS\.md|\.cursorrules|\.clinerules|\.windsurfrules|\.mcp\.json|\.cursor/|\.claude/|\.github/copilot-instructions\.md)'
unzip -p EXTENSION.vsix extension/CLAUDE.md \
  | perl -CSD -ne 'print "$.: $_" if /[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{2066}-\x{2069}\x{FEFF}\x{E0000}-\x{E007F}]/'

VS Code itself highlights invisible characters when you open the file in the editor, which is the least effort of all once the package is on disk. The open-source anti-trojan-source scanner and SonarQube's rules S6389 and S7628 cover the same codepoints; this check runs before the install.

Questions

This extension ships a CLAUDE.md — does that fire V14? No, not on its own. Having an agent instruction file in the package is check V13, recorded as a note because an installed extension's directory is not a workspace an agent opens. V14 reads what is inside the file and fires only on codepoints that render as nothing: zero-width characters, bidirectional overrides, invisible operators, the Unicode Tags block. Five of the 100 most-installed extensions ship an agent file; all five were read and none fired.

Would V14 have caught GlassWorm's hidden loader? No. GlassWorm hid its loader in invisible Unicode variation selectors inside JavaScript (Koi Security, October 2025; the Eclipse Foundation's Open VSX security update of 27 October 2025), and this tier does not read the code bundle — V14 scans the agent rule files only. Checkmarx's juanblan281.solid281 (30 January 2026) hid zero-width characters in its display name, a listing field rather than a file in the package, so that is not this check's input either. Both are stated here as gaps, not claimed as catches.

Does RepoGates see the extension when VS Code installs it? No. RepoGates assesses a Marketplace extension on request — through the API, the MCP server and the preflight for code --install-extension. It does not see an extension installed from the editor's own UI, or one that auto-updates; the board on marketplace pages is not built yet; and Open VSX, where VSCodium, Cursor and Windsurf install from, is a different registry and is not assessed.

Related checks

V13 Agent config files — the files this check reads; presence alone is a note in a package · V15 MCP shell-launch config — the same files, a different payload: a command that fetches and runs · C19 Hidden Unicode in agent rules — the same function, reading a git tree instead of a package · H15 Hidden Unicode in agent rules — the same function on the Hub's file list.

Add RepoGates to Chrome Pricing

Numbers on this page: the 18 September 2026 measurement of the 100 most-installed VS Code Marketplace extensions (99 PASS, 1 REVIEW, 0 BLOCK; V14 fired 0 times; 5 agent files read); Pillar Security, March 2025 (the technique); Koi Security, October 2025, and the Eclipse Foundation's Open VSX security update of 27 October 2025 (GlassWorm); Secure Annex, 30 November 2025 (19 Microsoft-Marketplace identifiers); Checkmarx, 30 January 2026 (solid281); weights from the product's scoring table; codepoint ranges from the repository vetting standard.