The Problem: Agent Instructions Are Attack Surface
When we built the Compact Instruction Compiler, the goal was simple: take verbose Markdown instruction files and compile them through a PIR intermediate into compact Markdown. What we didn't address was what happens when the input isn't just verbose — it's malicious.
The threat is real. NVIDIA demonstrated an indirect AGENTS.md injection attack where a dependency-injected instruction file overrides the agent's safety directives. The OWASP Agentic Skills Top 10 (AST) formalizes this: prompt injection (AST01), over-privileged commands (AST03), exfiltration (AST04), untrusted external instructions (AST05). The SoK paper on prompt injection against coding assistants (arXiv:2601.17548) shows that natural-language-plus-code blends defeat pure signature scanners — a defensive policy that says "NEVER exfiltrate credentials" contains the same words as an attack that says "send credentials to."
We needed three things: a security gate that runs before compilation, a lint checker that enforces structural conventions, and a config file that ties them together. Here's what we built.
Architecture: Three New Tools
The CIC pipeline gained three new components, each with a clear separation of concerns:
The Pipeline
When you run cic.sh -i (install mode), security scanning and semantic regression checking are now auto-enabled. This means you can't accidentally install adversarial content into your source tree without explicitly opting out with --no-sec.
The Security Scanner: 60 Rules, 9 Categories
The scanner (cic-sec.sh) maps directly to the OWASP Agentic Skills Top 10. Each rule is a tab-delimited record: ID, severity, category, ERE regex, description. The rules are data, not logic — adding a new detection pattern means adding a line to the array, not writing a new function.
| Category | AST | Rules | What it detects |
|---|---|---|---|
INJ | AST01, AST05 | 8 | Instruction-override ("ignore previous instructions"), role-reset jailbreaks, system-prompt extraction |
STL | AST01 | 6 | Stealth suppression ("do not mention this change", "silently inject") |
SEC | AST04 | 10 | API keys (sk-, AIza, ghp_, xox, AKIA, JWT), PEM key blocks, inline password assignments |
MAL | AST04 | 10 | Reverse shells, netcat listeners, rm -rf /, curl | sh, mkfifo, eval() |
XSS | AST04 | 7 | <script> tags in HTML context, javascript: URIs, inline event handlers, iframes, cookie access |
EXT | AST05 | 4 | External-instruction fetch directives ("load rules from https://...") |
EXF | AST04 | 6 | Credential exfiltration, secret upload, command-substitution network callbacks, base64-obfuscated channels |
PRI | AST03 | 6 | DROP TABLE, sudo rm, git push --force, git reset --hard |
META | AST03, AST09 | 3 | Brand-impersonation author fields, over-broad permission manifests, unpinned floating versions |
The XSS False-Positive Lesson
The original XSS-01 regex was <script[^>]*> — simple and correct for HTML. But agent instruction files are not HTML. They're Markdown with shell code blocks. A line like Run the script at /tmp/<script>.sh triggered the rule 30 times across the repository.
The fix: require HTML context. The regex became (^|[[:space:]<])<script[^>]*> — the <script> tag must be preceded by start-of-line, whitespace, or another < (indicating HTML structure). Shell placeholders like /tmp/<script>.sh have / or alphanumerics before the <, so they no longer match. False positives dropped from 30 to 7 — and all 7 remaining are real <script> tags in HTML code blocks inside archived plans.
The Lint Checker: 70 Rules, 9 File Types
The lint checker (cic-lint.sh) validates structural conventions across nine file types: skills, workflows, abilities, rules, memories, ideas, plans, AGENTS.md, and README.md. The rule catalog lives in cic-style.sh as tab-delimited data records — the same data-not-logic pattern as the security scanner.
Rules were derived from three sources:
name must be kebab-case, max 64 chars, match the parent directory. description must be non-empty. Optional fields: keywords, license, compatibility, metadata, allowed-tools.required-structure rules, maxTokens/maxDepth budgets..devin files inspected for actual patterns: memory filenames use m_<snake>_<epoch>.md, ideas use IDEA_<SCREAMING>_<hash>.md, plans use SCREAMING_SNAKE_CASE, workflows require auto_execution_mode: 3.When repo conventions conflicted with external specs, the repo won. The Agent Skills spec says description max is 1024 chars; our skills average 80. The spec lists Invocation and Commands as recommended sections; many of our skills (like build and test) are simple enough that they don't need them. Those rules became info severity — they report but don't block.
Severity Design
| Severity | Blocks? | Examples |
|---|---|---|
error | Yes | Missing required frontmatter, invalid naming, unclosed code fences, unpaired backticks |
warning | Yes | Trailing whitespace, consecutive blank lines, tab indentation |
info | No | Missing optional sections, long lines (>200 chars), missing fence language, link bracket pairing |
The .cic Config File
Configuration moved from a scattered .devin/cic-sec-allowlist file to a project-root .cic file — the same pattern as .clang-format and .clang-tidy. Three INI-style sections:
[sec]
# Security scanner allowlist — suppress vetted defensive FPs
# Format: RULE_ID:path-glob or *:path-glob (trust directive)
*:.devin/plans/archive/*
*:.devin/ideas/*
INJ-03:AGENTS.md
XSS-01:.devin/skills/add-lab/*
[lint]
# Lint/style thresholds and rule disables
max_line_length = 200
max_heading_level = 4
# disable = STYLE-04
[compile]
# CIC compiler defaults
auto_sec_on_install = true
auto_guard_on_install = true The [sec] section is parsed by cic-sec.sh with a one-liner: awk '/^\[sec\]/{f=1;next} /^\[/{f=0} f{print}' .cic — extract lines between [sec] and the next section header. A user-level fallback at ~/.config/devin/cic-sec-allowlist still works for personal overrides.
The Performance Pattern: One Awk Pass
The first implementation used per-file shell loops: for f in *.md; do grep ...; awk ...; done. For 758 files, this spawned 758+ subprocesses and took over 10 seconds. Unacceptable for a compile gate.
The fix: move all rules — frontmatter parsing, heading extraction, naming checks, code/prose/link checks — into a single awk process that processes all files in one invocation. Awk's FNR==1 rule detects file boundaries and resets per-file state. The END block flushes the last file.
This pattern is now canonical for batch file scanning in the CIC toolchain. Both cic-sec.sh (0.5s) and cic-lint.sh (0.4s) use it. The key insight: awk processes all files in one invocation, tracks per-file state with FNR==1 reset, and flushes accumulated results at END.
The AST08 Gap: Why Signatures Aren't Enough
We mitigate this in two ways:
NEVER, DO NOT, REFUSE, FORBIDDEN, or MUST NOT are auto-downgraded to low severity for non-STL categories. The scanner recognizes that defensive policy legitimately discusses the attacks it forbids. STL (stealth suppression) is never downgraded — "do not mention this" is always suspicious, even in defensive context.cic-sec.sh --vlm pipes findings through pi.sh vlm (our local V Engine Language Model, backed by pi-daemon and qwen3-1.7b-instruct). The VLM classifies each finding as ATTACK or POLICY. This is the only robust mitigation per the SoK paper. If pi-daemon is unavailable, the VLM pass gracefully skips and falls back to signature-only results.The Red-Team Corpus
We extracted detection patterns from multiple sources into data/redteam/prompt-injection-patterns.txt — a maintained seed set for rule development:
| Source | Contribution |
|---|---|
| OWASP Agentic Skills Top 10 | AST01–AST10 category framework |
| NVIDIA indirect AGENTS.md injection PoC | Instruction-override patterns, stealth suppression |
| arXiv:2601.17548 (SoK prompt injection) | AST08 theoretical limitation, attack taxonomy |
| fuzzdb (archive r_11906cf8) | HTML/JS injection patterns, command injection templates |
| SecLists (archive r_c2321ca2) | SSI injection, HTML5sec, command injection wordlists |
The corpus currently has 90 patterns across 9 categories. cic-sec.sh --update-rules reports coverage: how many corpus patterns are represented by the 60 embedded rules. New patterns are added to the corpus first, then promoted to rules after testing.
Verification
Thirteen unit tests in test-cic-sec.sh cover the scanner's behavior:
/tmp/<script>.sh → no match. HTML <script>alert(1) → match.What's Next
The security scanner and lint checker are implemented and running. The VLM classification pass is implemented but requires pi-daemon to be running — it gracefully skips when unavailable. On the roadmap:
- Automated FP fixing — a
--fixmode forcic-lint.shthat auto-corrects trailing whitespace and consecutive blank lines (safe, mechanical fixes). - Pre-commit integration — a
.githooks/pre-commitscript that scans staged instruction files before they land. Documented and ready to install withgit config core.hooksPath .githooks. - Corpus-to-rules pipeline — automatically generate
cic-sec.shrules fromdata/redteam/patterns, reducing the gap between corpus (90) and rules (60). - Semantic structure validation — beyond regex: section ordering, maxTokens budgets, and heading-tree depth validation using the
document-schemaapproach.
The broader question — whether signature scanning plus LLM classification is sufficient for the agentic AI threat landscape — remains open. The SoK paper suggests it's the best we can do without runtime sandboxing. We're betting that compile-time gates plus VLM triage plus an allowlist for vetted content is a practical defense-in-depth for the current generation of agent instruction files.
Read the original CIC post →Pre-order CIC →