Invisible Unicode - Markdown as an Attack Surface
Gate smuggling primitives in CI, not in preview
Markdown can look fine in the rendered preview while the logical file still carries Trojan instructions. In agentic delivery, skills, YAML, specs, and comments are machine context - not just human prose. Supply-chain risk is not only npm; it is unvetted text your agents load before they act.
Here's what this piece covers: the leadership problem of WYSIWYG security, what commit e833777 (2026-07-07) enforced in CI, the threat classes we gate at a high level, the named pattern, and three habits for any repo agents treat as context.
Part of the Field Notes: agentic delivery series - deterministic gates sit beside human merge approval.
1. The leadership problem
Reviewers trust the rendered preview. Attackers exploit the gap between display and logical order - Trojan Source-style confusion, plus invisible codepoints that models still tokenize.
The anti-pattern is trust the rendered preview - WYSIWYG security for agent-readable repos.
Wrong: "Be careful with copy-paste."
Right: "e833777: what we detect, what we block, what CI enforces - a lint-shaped gate, not a lecture."
You cannot ask every reviewer to eyeball zero-width characters on every skill file. You can fail the build.
2. What the git history shows
On 2026-07-07, commit e833777 - Add invisible-unicode CI workflow and align methodology docs - made the gate non-optional:
| Artifact | Role |
|---|---|
.github/workflows/invisible-unicode-lint.yml |
Scan + unit tests on push/PR |
tools/invisible_unicode_lint/ |
Deterministic scanner (Python + TS) |
| Canonical threat note | docs/code-quality/invisible-text-prompt-injection-and-linting.md |
| Standard | docs/standards/agent-context-text-hygiene.md |
The scanner predates the workflow; the commit is the leadership move - CI enforcement, not optional local hygiene.
Core scan loop - codepoints, not models:
export function scanText(
text: string,
rules: Iterable<Rule>,
): Array<[number, number, number, Rule]> {
const violations: Array<[number, number, number, Rule]> = [];
let line = 1, col = 1, cpOffset = 0;
for (const ch of text) {
const code = ch.codePointAt(0)!;
for (const rule of matchRules(code, cpOffset, rules)) {
violations.push([line, col, code, rule]);
}
if (ch === "\n") { line++; col = 1; }
else if (ch !== "\r") { col++; }
cpOffset++;
}
return violations;
}3. What I built
Threat mechanisms we treat as lint-shaped (high level, no exploit cookbook):
| Mechanism | Why it matters |
|---|---|
| Unicode tag letters (U+E0000 block) | Invisible English to models; no legitimate role in most docs |
| Zero-width / format chars | Hidden payloads, token splitting |
| Bidirectional controls | Display order ≠ logical order (Trojan Source class) |
What we ship: deterministic rules from JSON, strict UTF-8 reads, pre-commit + CI, same rules for skills, AGENTS.md, specs, and comments. Complements - does not replace - least-privilege tool access and human review of high-risk changes.
What broke: teams forget non-code files are in the blast radius. False positives stay rare when the rule set stays scoped to smuggling primitives rather than "any unusual Unicode."
4. Named pattern: deterministic Unicode lint
Deterministic Unicode lint - supply-chain hygiene for agent-readable repos; gate in CI, do not trust the preview.
| Control | Who it protects | Decision it supports |
|---|---|---|
| CI scan | Every merge | "May this text become agent context?" |
| Pre-commit | Authors | Fix before push |
| Canonical doc + standard | Security + EM onboarding | Shared vocabulary without FUD |
| Human playbook (MCP/hooks) | Operators | Broader instruction supply chain |
What broke: treating this as a complete security program. It is a cheap filter for high-yield primitives - then least privilege and human merge still matter.
5. What I'd do as a lead
Three habits - steal one this month:
- Enable the gate on any repo agents load as context. Skills and specs first; source comments next.
- Include invisible-text hygiene in security onboarding for agentic teams. Show one failing CI log; skip the Unicode lecture.
- Pair the lint with the human playbook for MCP, hooks, and instruction supply chain. Lint is necessary; it is not sufficient alone.
Further reading
- Field notes: agentic delivery - series intro
- Next: Graphify - monorepo navigation
- agentic-framework anchor: commit
e833777· 2026-07-07 ·tools/invisible_unicode_lint/