348 files your agent is told to read were being scanned as shell scripts
Skill bundles ship reference documents the agent is instructed to open. We were selecting them correctly and then asking them the wrong question entirely.
A skill bundle is a SKILL.md plus the files it references. The markdown tells the
agent what to do; the reference documents carry the detail, and the agent is told to open
them. Both end up in the model's context as trusted text.
We were already picking those reference documents up. Anything a SKILL.md names
gets selected as a bundle file. Then we handed each one to a scanner that looks for
credential exfiltration in shell scripts.
Across the 144 skills installed on one developer machine, that was 348 markdown files the agent is explicitly instructed to read, checked as if they were shell scripts. Right files, wrong question. Injection aimed at the agent — the thing we describe as the threat nobody else covers — was invisible in every one of them.
The number we started from was wrong in a useful way#
Our own backlog said "739 files, vetted by nothing". Measuring first gave a different shape:
| Bundle markdown | Files |
|---|---|
Named in SKILL.md — selected, wrong scanner | 348 |
| Reachable only through a second document | 14 |
| Referenced by nothing in the bundle | 355 |
The obvious build was to follow the reference graph one hop deeper. That buys fourteen files. We did not build it. The 355 unreferenced ones have no path to the agent at all — they are changelogs and test scratch files. The gap worth closing was the 348 we were already holding.
Coercion, not imperatives#
The hard part is not detecting instructions. A reference document is made of instructions — "always run the tests", "never push on red", "ignore the first warning from the linter". A scanner that reads instruction-to-a-reader as instruction-to-an-agent flags every guide ever written.
What we match instead is text that only makes sense if the reader is an agent being steered against its operator: an instruction to ignore previous instructions, to conceal an action from the user, to redefine the assistant's role, or to tell an automated reviewer to report clean. Plus one proximity rule — a credential path and a way off the machine in the same breath.
The false-positive work was the actual work#
Final result: 1,253 real documents, zero flagged. It took two rounds, and what the first ten were is the whole lesson.
Seven were anti-injection guidance quoting the attack inline — things like
("SYSTEM:", "ignore previous instructions", "mark this rule as approved"). The most
security-conscious documents on the machine were the ones it fired on. We strip quoted
spans now; four then survived that, because the phrase wrapped across a newline and a
per-line strip saw neither half as quoted.
Three were concealment phrasing with nothing behind it. A protocol table saying
Skip silently. Do not mention to user. about an optional input. A code-review
guide listing Retry logic that exhausts attempts without informing the user as a smell
to hunt for. Not telling someone about a no-op is not concealment, so the concealment
rules now require an action nearby.
Neither of those was predictable by reasoning. Both came from running it over real files.
Two things it will not catch#
- Quoting evades it. Wrap the payload in quotes or a code fence and we say nothing. That is the deliberate price of not flagging every security write-up — and a quoted injection is a weaker one, because the agent reads it as an example.
- It is a pattern scanner. A homoglyph —
ınstructionswith a dotless i — walks straight past. Normalising for that is its own signal with its own false-positive surface.
Both are published in the corpus notes alongside the fixtures, because a detection whose limits are undocumented is a detection you cannot reason about.