Today is ${today}. Audit the codebase for security risks in skill instructions, CI workflows, and companion scripts before they run.
Files instruct Claude Code and GitHub Actions runners to take actions. Adversarial or sloppy files can:
-
Read memory. Read memory/MEMORY.md and today's memory/logs/${today}.md (create if missing) for context.
-
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.
-
Resolve scope per the ${var} rules above. Log the chosen scope.
-
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.
-
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.
-
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.
-
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.
-
Apply baseline suppression. Drop any finding whose (file, pattern, line) tuple is in skills/security/scan-baseline.yml.
-
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)
-
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.
-
Write the report to articles/security-scan-${today}.md only if there are any NEW, RESOLVED, or current HIGH findings. Structure:
# 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.
-
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 |
-
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}]}.
-
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.
-
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.