Hidden Unicode

Check S8 · agent surface family · severity CRITICAL · automatic · one of the 15 skill and plugin checks

The question this check asks: does any text this skill or plugin ships — the SKILL.md an agent loads, the scripts beside it, a plugin's agents, commands, hooks and MCP files — contain characters that render as nothing? It is C19, the repository roster's Rules File Backdoor check, run on every text the skill tier reads. A skill is a document an agent is designed to follow; a character the reviewer cannot see is an instruction only the agent gets.

Why it matters

Snyk's ToxicSkills report of 5 February 2026 shipped its own fixtures, the snyk-labs/toxicskills-goof repository, and one of them is exactly this: a “testing-guidelines” skill whose instruction is hidden by ASCII smuggling — the text encoded into codepoints from the Unicode Tags block, which occupy no visible space in any editor that does not go looking for them. The reader sees guidelines about tests. The model reads what the tags spell. The Cloud Security Alliance's 6 May 2026 paper on agent context poisoning names the surface in one sentence: the intent of a skill is “model-mediated behavioral intent expressed in natural language”, which “may be hidden in prose, Unicode characters, external references, or conditional instructions” — and its recommendations include Unicode sanitisation of context files. That is the technique C19 was built for on a repository's CLAUDE.md, and a skill is the same document shape distributed through a registry: one file, skimmed by a person, obeyed by an agent. The malicious skills page has the record; the Rules File Backdoor page has the technique.

How RepoGates scores it

S8 is C19's rule, unchanged, called through the same function the 22 use — inspect_agent_text — on every text this tier reads for the key: each SKILL.md, the scripts beside it, a plugin's agents/*.md and commands/*.md, its hooks/hooks.json, monitors/monitors.json, .mcp.json, settings.json and .claude-plugin/plugin.json. Each 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 — zero-width characters and marks, bidirectional overrides, invisible operators, bidirectional isolates, the byte-order mark and the Tags block. Any hit is CRITICAL, costs 45 points and makes the verdict BLOCK; the default personal profile blocks on it, and the strict organisation profile locks that so no policy edit can lower it. The finding is hidden-unicode:<path> and names the file, the count and the codepoint names — “3 invisible codepoints (ZERO WIDTH JOINER, RIGHT-TO-LEFT OVERRIDE) — Rules File Backdoor technique; instructions hidden from human review” — and never the decoded text, because a report is itself a text an agent may read. The evidence note in the scoring table is C19's: “Rules File Backdoor; zero FPs observed”. The same rule is G19 on gitlab.com, H15 on Hugging Face and V14 on the VS Code Marketplace.

Measured on 20 September 2026 through the same code as /v1/skill/score, the row fired on none of the 189 skills.sh keys, none of the 310 official-marketplace plugins and none of the first 100 community plugins, and on one key of the malicious set: snyk-labs/toxicskills-goof, on .gemini/skills/testing-guidelines/SKILL.md, 171 codepoints from the Tags block. It is the one BLOCK of the run — BLOCK 0, with S9's exfiltration, fetch-and-run, dynamic-context and credential-read rows beside it. The file was read for the first time in the second run: the first run's discovery did not know .gemini/skills/ as a skill root, so the fixture sat in the tree unopened, and the calibration that added the directory is what put it on the report. A check reads what it is handed; the record says which files those were.

False positives we know about

None observed — across the three top lists, 189, 310 and 100 keys, no SKILL.md, script, agent, command or plugin document contained a codepoint in the set, and the scoring table's own note on the rule reads “zero FPs observed”. The one we expect to see eventually is a byte-order mark (U+FEFF) at the top of a file saved by a Windows editor. If that fires on yours, the finding will say exactly one codepoint, ZERO WIDTH NO-BREAK SPACE, at the start of the file — and we would like to hear about it, because it decides whether a BOM at offset zero should be exempted on this roster as well as on C19.

What this check does not cover

Whitespace inflation. The Cloud Security Alliance's 10 June 2026 scanner test bypassed ClawHub's VirusTotal Code Insight with roughly 100,000 newlines in front of the payload. A newline is whitespace, not an invisible codepoint, and this check does not fire on it; S11 notes the size and nothing more. It is a different trick, and the honest statement is that the 64 KB read is where this tier stops — a payload placed past that mark in a file is not read, the partial-scan note says so, and the deep scan is the tool for the rest. A text the tier did not read. Twelve files per key, eight skills: an invisible codepoint in the thirteenth file, in the ninth skill, or inside a binary or an archive that S10 lists but never opens, is not seen. Homoglyphs — letters from another script that look like Latin ones — are a different technique and are not caught here; nor is an instruction hidden in plain, readable prose, which is S9's question and is matched by shape, never by intent. ClawHub (@owner/slug) is parsed and routed but not assessed on this tier: the Hub's API answers a skill with its SKILL.md text, but until the fetch ships every text check, S8 among them, is a listed skip there and a ClawHub key answers “not assessed” — never a pass.

Scope, the same on every page of this roster: RepoGates does not see git clone, package managers or curl — outside Claude Code with the RepoGates plugin, whose hook refuses a clone or install that names a blocked repository on the command line, before it runs. The hook sees Bash tool calls in that one client and nothing else: npx skills add owner/repo and claude plugin install name@marketplace are shell lines it judges; /plugin install is a slash command the hook never sees; a skill copied by hand into ~/.claude/skills is never seen at all. This tier reads the listing record, the tree once, and at most twelve files of 64 KB each — SKILL.md files first, then a plugin's agents and commands, then the scripts beside each skill; more than fits is a partial scan, stated, and never a pass on what was not read. The deep scan reads the whole package. A PASS means nothing matched on what was read, never that the skill was run.

Check it yourself

Without installing anything: open the SKILL.md in an editor that renders invisible characters — VS Code highlights them by default, and GitHub's own file view warns of bidirectional characters, though only those. From a terminal, on a checkout of the skill or plugin, every text file at once:

find . -type f \( -name '*.md' -o -name '*.json' -o -path '*/scripts/*' -o -path '*/bin/*' \) -print0 \
  | xargs -0 perl -ne 'print "$ARGV:$.: $_" if /[\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{2064}\x{2066}-\x{2069}\x{FEFF}\x{E0000}-\x{E007F}]/'

Any line printed carries a codepoint from the set; the file, line and count are what the finding would say. That runs on what you already have, on every file rather than twelve; this check runs before the install, on what the tier read.

Questions

Can I see what was hidden? Not from the finding, deliberately. It names the file, the number of invisible codepoints and their Unicode names — never the decoded text, because rendering a hidden instruction into a report moves the payload to a second surface, and a report is a text an agent may read. Open the file in an editor that shows codepoints, or run the one-line scan on this page on your own checkout.

What did the check find on 20 September 2026? Nothing on the three top lists — 189 skills.sh keys, all 310 official-marketplace plugins and the first 100 community plugins, not one invisible codepoint in any text read — and one hit on the malicious set: snyk-labs/toxicskills-goof, Snyk's own fixture repository, whose .gemini/skills/testing-guidelines/SKILL.md carries 171 codepoints from the Unicode Tags block, the ASCII-smuggling fixture the ToxicSkills report of 5 February 2026 published alongside its findings. It was read for the first time in the second run, because discovery had learnt the .gemini/skills/ directory between the runs; in the first it was a file nobody opened. The row is CRITICAL, 45 points, and the repository's verdict is BLOCK 0.

Is whitespace inflation caught here? No. The Cloud Security Alliance's 10 June 2026 test bypassed ClawHub's VirusTotal Code Insight by putting roughly 100,000 newlines in front of the payload. A newline is not an invisible codepoint; it is whitespace, and this check does not fire on it. S11 notes a SKILL.md over 500 lines or 65,536 bytes, and that is a note, not a defence: this tier reads the first 64 KB of a file, so a payload placed past that mark is not read, and the finding that would catch it is the partial-scan note, never a pass. The deep scan reads the whole package.

Related checks

C19 Hidden Unicode in agent rules — the same rule on a repository's own CLAUDE.md, AGENTS.md and MCP files; this page is its twin on the skill roster · S9 Instruction shapes — the instruction written in visible prose, matched by shape · S10 Scripts and executables — the scripts this check also reads, and the binaries it cannot · S15 Credential redirect — the other C-roster rule the skill tier carries on every text, rewritten for the skill shape.

Add RepoGates to Chrome Pricing

Numbers on this page: the 20 September 2026 measurement of 189 skills.sh keys, all 310 official-marketplace plugins, the first 100 community-marketplace plugins and the malicious set, run through the same code as /v1/skill/score; Snyk's ToxicSkills report and fixtures (5 February 2026); the Cloud Security Alliance (6 May and 10 June 2026); the incident record behind the malicious skills page; codepoint ranges and weights from the product's scoring table and the repository vetting standard.