Audit skills, workflows, and companion scripts for injection, exfiltration, traversal, and prompt-override risks with delta tracking, baseline suppression, issue filing, and per-finding remediation
Scanned 9/5/2026
Install to Claude Code
npx -y skills add anajuliabit/aeon --skill skill-security-scan --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Skill Security Scan?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/anajuliabit-skill-security-scan)More formats (shields.io, HTML) on the badges page.
---
name: Skill Security Scan
description: Audit skills, workflows, and companion scripts for injection, exfiltration, traversal, and prompt-override risks with delta tracking, baseline suppression, issue filing, and per-finding remediation
var: ""
tags: [dev]
---
<!-- autoresearch: variation B — sharper output: delta tracking + issue filing + code-fence-aware suppression + baseline + remediation snippets + expanded coverage (workflows, companion scripts) -->
> **${var}** — a SKILL.md path, a skill name (e.g. `token-movers`), or a directory. Empty = full corpus scan.
Today is ${today}. Audit the codebase for security risks in skill instructions, CI workflows, and companion scripts before they run.
## Threat categories
Files instruct Claude Code and GitHub Actions runners to take actions. Adversarial or sloppy files can:
- **Shell injection** — unquoted variable expansion, `eval`, backticks, `$(...)` in bash blocks
- **Secret exfiltration** — env vars or file contents piped into outbound HTTP requests
- **GitHub Actions script injection** — user-controlled template expressions (`${{ github.event.* }}`, PR titles, issue bodies, incoming messages) interpolated directly into `run:` blocks (see the 2026-04-11 `messages.yml` incident in `articles/workflow-security-audit-2026-04-11.md` for the canonical pattern and fix)
- **Path traversal** — access files outside repo via `../..` chains or absolute paths
- **Prompt override** — instructions in fetched content or skill bodies attempting to make the agent disregard prior guidance, switch persona, or act on new "system" rules
- **Destructive commands** — irreversible ops like recursive deletes from root, device writes, forced pushes to main
- **Obfuscation (2026 additions)** — zero-width Unicode (U+200B, U+FEFF), bidi override (U+202E / Trojan Source), base64-decoded payloads, `fromCharCode`, hex-escaped command strings, webhook SSRF hosts (ngrok, interact.sh, webhook.site, burpcollaborator, pipedream, requestbin)
## Coverage
Scan every run:
- `skills/*/SKILL.md` (primary)
- `skills/*/*.sh` and `skills/*/*.py` (companion scripts that skills invoke)
- `.github/workflows/*.yml` (CI — especially `run:` blocks referencing `${{ ... }}`)
- `scripts/*.sh` (repo-level scripts)
When `${var}` is set:
- If it matches an existing SKILL.md path (absolute or relative) → scan that file only
- Else if a directory exists at `skills/${var}/` → scan everything under it
- Else if it looks like a bare skill name and `skills/${var}/SKILL.md` exists → scan that file
- Else abort with `ERROR: scope not found for var=${var}`
## Inputs and state
| Path | Purpose |
|------|---------|
| `skills/skill-security-scan/scan.sh` | Raw regex scanner (HIGH/MEDIUM/LOW pattern library) |
| `skills/security/trusted-sources.txt` | GitHub owners/repos whose skills get format-only scans |
| `skills/security/scan-baseline.yml` | Human-reviewed-as-safe suppressions (bootstrap if missing) |
| `memory/state/security-scan.json` | Prior scan snapshot — used for delta |
| `memory/issues/INDEX.md` | Open/resolved issue index (HIGH findings file here) |
| `articles/security-scan-${today}.md` | Report output (only written if there are findings or a delta) |
### Baseline file format
`skills/security/scan-baseline.yml`:
```yaml
# Each entry suppresses a specific (file, line_range, pattern) match that a human has reviewed.
# Format:
# - file: <path>
# pattern: <regex pattern from scan.sh HIGH_PATTERNS/MEDIUM_PATTERNS/LOW_PATTERNS>
# lines: "15-25" # optional line range; omit to suppress across whole file
# reason: "documentation in threat model section"
# reviewed_by: "aaronjmars"
# reviewed_at: "2026-04-20"
suppressions: []
```
Seed `suppressions` at bootstrap with the self-documenting matches that we already know are false positives:
1. `skills/skill-security-scan/SKILL.md` — all prompt-override pattern matches inside the "Threat categories" section (documentation, not payload)
2. `skills/security-digest/SKILL.md` — any curl/token pattern inside fenced code blocks showing example usage
## Steps
1. **Read memory.** Read `memory/MEMORY.md` and today's `memory/logs/${today}.md` (create if missing) for context.
2. **Bootstrap baseline.** If `skills/security/scan-baseline.yml` does not exist, create it with the seed suppressions listed above and record `BASELINE_BOOTSTRAPPED` in the exit status.
3. **Resolve scope** per the `${var}` rules above. Log the chosen scope.
4. **Preflight scanner.** Verify `skills/skill-security-scan/scan.sh` is present and executable, then attempt to invoke it. **Two known failure modes trigger the inline Grep fallback:**
- `missing` — file absent or non-executable (rare edge case).
- `sandbox-blocked` — `scan.sh` execution denied at the sandbox approval gate. This is the **dominant** failure mode: every weekly run since 2026-05-25 has taken this path (7+ consecutive runs as of 2026-07-13). Treat sandbox denial as expected, not exceptional.
In either case, fall back to inline Grep over the same HIGH/MEDIUM/LOW pattern library defined in `scan.sh`, plus the manual `${{ }}`→`run:` interpolation audit and obfuscation sweep — never silently skip. Record which trigger fired (`missing` or `sandbox-blocked`) on the log's `Scanner mode:` line so future reflect cycles can distinguish a real scanner regression from expected sandbox denial.
5. **Run scanner in JSON mode** — invoke `scan.sh --json` (or `--all --json` for the full corpus) and capture the structured output: `[{skill, status, file, high, medium, low}, ...]`. Do not parse stderr into findings. If invocation returns a sandbox denial rather than JSON, route back to step 4's `sandbox-blocked` fallback path — do not retry, do not surface the denial as a scan failure.
6. **Trusted-source filter.** Load `skills/security/trusted-sources.txt`. For each scanned file, check if the skill directory has an `origin:` field in its frontmatter, or fall back to the repo's git remote. If the source is trusted (owner or owner/repo match), downgrade to format-only validation: verify frontmatter has `name`, `description`, `tags`, and a `var` key — emit no HIGH/MEDIUM/LOW findings for trusted sources, only format errors.
7. **Code-fence downgrade.** For each non-trusted finding, re-read the file around the finding's line. If the line is inside a fenced code block (between ```` ``` ```` markers in a Markdown file, or inside a `run: |` / `script: |` YAML block in a workflow file that is clearly an example, not an executable step), downgrade severity by one tier (HIGH → MEDIUM, MEDIUM → LOW, LOW → drop). Never downgrade inside actual `run:` steps in real workflow files — those execute.
8. **Apply baseline suppression.** Drop any finding whose (file, pattern, line) tuple is in `skills/security/scan-baseline.yml`.
9. **Compute delta** against `memory/state/security-scan.json` (previous run's finding set, keyed by `sha256(file+line_content+pattern)`):
- **NEW** — findings present now but not last run
- **RESOLVED** — findings present last run but gone now
- **PERSISTENT** — findings in both runs (not re-notified, but still counted)
10. **File/close issues** in `memory/issues/`:
- For each NEW HIGH finding (post-suppression): create `memory/issues/ISS-{next_id}.md` with YAML frontmatter (`id`, `title`, `status: open`, `severity: high`, `category: quality-regression`, `detected_by: skill-security-scan`, `detected_at: ${today}`, `affected_skills`) and append a row to `INDEX.md` under `## Open`.
- For each RESOLVED finding that corresponds to an open ISS filed by `skill-security-scan`: set `status: resolved`, `resolved_at: ${today}`, move the row from `## Open` to `## Resolved` in `INDEX.md`.
- Do NOT file issues for NEW MEDIUM or LOW findings — those live in the article report only.
11. **Write the report** to `articles/security-scan-${today}.md` only if there are any NEW, RESOLVED, or current HIGH findings. Structure:
```markdown
# Security Scan — ${today}
**Verdict:** [CLEAN | ATTENTION | DEGRADED]
**Scope:** [full corpus | ${var}]
**Counts:** N files scanned · H HIGH · M MEDIUM · L LOW · X new · Y resolved since last scan
## Needs attention (NEW high-severity this run)
For each: file:line, pattern that matched, one-line remediation snippet (see table below).
## Resolved since last scan
List of findings that disappeared — good for confirming fixes.
## Persistent findings (unchanged)
Count per severity; full list only in the appendix.
## Per-file results
Table: file, status (PASS/WARN/FAIL), HIGH count, MEDIUM count, LOW count.
## Appendix — all current findings
Full structured dump.
```
12. **Remediation snippets.** For each HIGH finding, attach a one-line fix hint keyed off the pattern. Map (non-exhaustive — extend as new patterns are added to `scan.sh`):
| Pattern category | Remediation |
|---|---|
| Shell eval / backticks / `$(...)` with variable | Quote the variable; prefer `${VAR}` with explicit quoting; replace `eval` with a function |
| `curl`/`wget` with an env var in the URL or body | Move secret into a pre-fetch script (see `CLAUDE.md` Sandbox section); never interpolate secrets into shell-block strings |
| `${{ github.event.* }}` inside a `run:` block | Rebind the value to an `env:` key first, then read `$_SAFE_NAME` from the shell (see `articles/workflow-security-audit-2026-04-11.md`) |
| Path-traversal sequence | Validate input against `skills/*/` or explicit allow-list; reject absolute paths |
| Prompt-override phrasing | If the string is documentation, add a baseline suppression entry; if it's a payload, delete it |
| Recursive delete rooted at `/` or `~` | Scope to `$REPO_ROOT` or a specific subdir; never take a variable as the delete root |
| Force-push to main | Remove the option or gate behind explicit human dispatch |
| Obfuscation (zero-width / bidi / base64-decode pipe) | Delete unless there's a documented, reviewed reason |
13. **Persist state.** Write the full current finding set to `memory/state/security-scan.json` so the next run can compute delta. Include `{generated_at, scope, findings: [{file, line, pattern, severity, fingerprint}]}`.
14. **Notify** via `./notify` only when there is something new for the operator:
- If any NEW HIGH finding → one paragraph summary naming affected skill(s), finding count, and path to the report.
- If any RESOLVED HIGH finding (but no new HIGH) → short "Resolved: X HIGH findings cleared since last scan."
- If only MEDIUM/LOW changes → skip notification (report is written, operator reads on demand).
- If no findings and no delta → skip notification; emit `SECURITY_SCAN_OK` to stdout so heartbeat can log it.
15. **Log** to `memory/logs/${today}.md` with an `### skill-security-scan` section: scope, exit status code, counts by severity, new/resolved counts, PR/issue IDs filed, report path.
## Exit status codes
Emit exactly one to stdout (on its own line) before normal output:
- `SECURITY_SCAN_OK` — no findings after suppression, no delta
- `SECURITY_SCAN_NEW` — at least one NEW HIGH finding
- `SECURITY_SCAN_RESOLVED` — no new HIGH findings, but at least one was resolved
- `SECURITY_SCAN_NOCHANGE` — findings exist but identical to last run
- `SECURITY_SCAN_BOOTSTRAPPED` — baseline file was just created; this run writes initial state
- `SECURITY_SCAN_ERROR` — scope unresolvable, scanner missing, or write failure
## Constraints
- Never auto-delete a finding from `scan-baseline.yml`. Suppression is a human decision; the skill only *adds* seed entries on first bootstrap.
- Never file an issue for a finding that is already represented by an open ISS (match by fingerprint — file+line+pattern).
- Never change `scan.sh`'s pattern library from inside this skill. Pattern evolution happens in a separate, reviewed PR.
- Never notify on a pure no-op week. Silence is correct when nothing has changed.
- Treat trusted-sources downgrades as opt-in only — never trust a source not explicitly listed.
## Sandbox note
This skill reads local files and shells out to `scan.sh`; no network calls required. `scan.sh` execution is denied at the sandbox approval gate on every scheduled Actions run — that is expected, not a scanner outage. When denied (or if the file is missing), perform the scan inline using Grep with the same pattern library per step 4 — never silently skip. The `./notify` call is covered by the standard post-processor (see `CLAUDE.md` Sandbox section).
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!