SW-SKL-002: Hidden text: zero-width or bidirectional control characters
Last updated 7 November 2026
Also known as: hidden Unicode characters, ASCII smuggling (tag characters).
Explained in: Prompt injection in agent skills, Hidden Unicode characters in agent skills
| Fact | Value |
|---|---|
| Severity | High |
| Checks | Skill package |
| Malicious indicator | No |
| OWASP Agentic Top 10 | ASI01 |
| OWASP MCP Top 10 | None mapped |
| NIST SP 800-53 | SI-10 (provisional mapping to SP 800-53 base controls) |
| First shipped in | Scanner 0.1.0 |
What it detects
The rule reads every text file of the skill package, not only Markdown, line by line. A byte order mark at the very start of a file is ignored. A line is reported when it holds a hidden character: a zero-width space, joiner or direction mark (U+200B to U+200F), the word joiner and the invisible operators after it (U+2060 to U+2064), a bidirectional embedding, override or isolate control (U+202A to U+202E and U+2066 to U+2069), a line or paragraph separator, a byte order mark inside the text, the combining grapheme joiner, the Hangul fillers, the Mongolian vowel separator, the interlinear annotation marks, any character of the tag block (U+E0000 to U+E007F) or of the variation selectors supplement, and every other character of the Unicode format category.
Variation selectors are judged by their place. Two or more in a row are hidden text, and so is a single one after a character that takes none. The emoji and text presentation selectors after an emoji, a keycap, or an ideograph variant are ordinary and pass, as does a zero-width joiner inside an emoji sequence. The soft hyphen is not reported, because text pasted from a word processor often has it. Because the line looks normal, the evidence is not the line: it is the list of code points found, each with its Unicode name.
Why it matters
A model reads every code point of a file, while a person reviewing the same file sees only what the screen draws. That gap lets a skill carry an instruction that no reviewer can see: tag characters can carry a whole sentence that renders as nothing, and bidirectional controls can reorder the text so that a line reads one way in a review and another way to a parser. The scanner maps this rule to OWASP ASI01, Agent Goal Hijack, and to NIST SP 800-53 control SI-10, Information Input Validation. It is the same hijack of the agent's goal as for visible injection phrases.
Because the characters are invisible, this rule is the one that finds what a human reading the file cannot, which is why it looks at all text files and not only the instructions.
Example
The last line of this skill has three zero-width spaces (U+200B) after the word "short". A Markdown viewer draws nothing for them, so the line looks ordinary; they are in the file below all the same, written there as the characters themselves:
.claude/skills/meeting-summary/SKILL.md
---
name: meeting-summary
description: Summarises meeting notes into a short list of decisions.
license: MIT
version: 1.0.0
---
# Meeting summary
Read the notes file and list the decisions, one line each.
Keep the list short. Add the owner of each decision.The finding sits on that line and its evidence reads U+200B ZERO WIDTH SPACE, not the sentence, since the sentence is unremarkable.
How to fix
Open the file in an editor that draws invisible characters, or retype the line, and delete every character the finding names. When a skill needs one of them for real text, as some languages do with joiners and direction marks, keep it only in that text and say why in the skill's description.
Check the neighbouring lines too: hidden characters are usually added in a run, and a copy from a web page or a chat window is the most common way they arrive.
If it is a false positive
Text in a language that is written with zero-width non-joiners or direction marks, such as Persian or Arabic documentation inside a skill, is flagged by design, because the rule cannot read the language. The code points in the evidence tell you whether they sit inside real words; if they do, accept the finding with a note that names the language.
If the finding is not a problem in your case, accept it with a note: in the dashboard a manager or owner can accept a finding and the note is kept with it; with the scanner on its own, add it to the baseline file with a reason and an expiry date.