| name | security-audit |
| description | Security auditor for Claude skills, MCP servers, and pre-publish deployments. Use PROACTIVELY, not only when asked. Trigger whenever: a skill or MCP is about to run, is newly installed, or changed; the first time a skill/MCP is used this session; an MCP config, settings.json, or .mcp.json is edited; OR before pushing, publishing, deploying, or uploading anything to GitHub, npm, a website, or any production surface. It vets skills & MCPs for hidden-instruction, exfiltration, and supply-chain risk (including following external links and re-checking that a trusted redirect wasn't repointed to something hostile), and runs an exposure gate that blocks secrets, API keys, tokens, PII, and EXIF from being published. Token-cheap: a deterministic hash-diff runs first and stops when nothing changed. Triggers on: "is this skill safe", "audit my MCPs", "check before I push", "security review of my skills", "scan for secrets before publishing". |
| argument-hint | [scan | deploy | full] (default: scan) |
| metadata | {"version":"1.0.0","author":"RoeeAI","repo":"https://github.com/roeea2/security-audit-skill"} |
Security Audit — skills, MCPs, and deployments
Skills and MCP servers are third-party instructions and code that the agent
loads and acts on. A malicious or compromised one can hide instructions from the
user, read credentials, exfiltrate data, or point at a link whose destination is
silently changed after you trusted it. Separately, it is easy to leak a secret,
token, personal email, or geotagged image when publishing. This skill defends
both fronts and reports every finding in one severity-ranked table with a clear
fix for each item.
The golden rule: run the scanner first, always
The deterministic engine does the heavy lifting and keeps this cheap. Always
start by running it and let its JSON tell you whether there is anything to
review:
python3 scripts/scan.py <mode> --project "$PWD" --json
- If the engine reports no new or changed items (
summary.new == 0 and
summary.changed == 0 in scan/full against a populated cache), do not
burn tokens re-reviewing. Print the short "nothing changed" verdict and stop.
- Otherwise, review only the items the engine flags as
new/changed (they are
in review_items), verify their external URLs live (below), then render the
table and let the engine update its cache.
This change-awareness is the whole point: the engine hashes every skill/MCP and
diffs against a machine-local cache at ~/.claude/.security-audit/state.json, so
re-audits are near-free until something actually changes.
Modes
| Mode | When | What it does |
|---|
scan (default) | A skill/MCP ran, was installed, or changed; start of session | Change-aware audit of skills + MCP configs. Short-circuits if nothing changed. |
deploy | Before any push / publish / deploy / going public | Exposure gate over the working tree: secrets, tokens, PII, EXIF, git author email. Never short-circuits. |
full | "audit everything", periodic deep check | Re-scans every skill + MCP ignoring the change cache. |
Run a mode explicitly with /security-audit deploy, etc. With no argument, use
scan.
Setup & enforcement (ask the user — don't assume)
Automatic checking can run at two cadences, and the per-call cadence can warn or
block. These are the user's call, so when they first set up the skill, ask to
enable automatic auditing, or say something like "audit my skills automatically",
ask them two short questions and then configure it — do not pick silently.
-
Cadence — "Run the check before every skill/MCP call, or just once at
session start?"
- session-start — a near-instant change-nudge once per session. Zero per-call
overhead.
- per-call — runs the guard before every skill/MCP invocation (a
PreToolUse
hook). Truly "check whenever a skill/MCP runs," at ~100–300 ms per call.
-
Enforcement (only if per-call) — "Warn, or block?"
- warn — surfaces a notice to me (e.g. "this skill hasn't been audited yet /
changed since your last audit / has Critical findings — run /security-audit")
but never blocks the call.
- block — an audit gate: denies a skill/MCP call that is unaudited (new
or changed since your last full audit) or carries a Critical finding,
until a
/security-audit reviews and records it. This is how you get
"audited before it runs" without putting the deep audit in the blocking path.
It targets the specific item, is offline, has a 10 s self-watchdog, and fails
open on any error, so it won't wedge the session.
-
Live link checking — "Should links be auto-resolved in the background to
detect a hijacked/repointed destination?"
- off (default) — hooks stay network-free; links are only flagged as "due"
and actually verified when you run
/security-audit.
- resolve — adds a detached, hard-bounded background resolver after the
instant offline nudge. It never blocks startup (the foreground returns in
well under a second; the resolver is capped by
--resolve-budget /
--resolve-timeout / --max-urls and uses --urls-only so it can't mask
file changes). When it spots a changed destination it records an alert
that the next session-start nudge surfaces — no model/tokens. The
model-level WebFetch judgement — whether a new/changed destination is
actually hostile — still happens only when you run /security-audit.
⚠️ Never put --resolve-urls directly in a foreground SessionStart command —
a SessionStart hook blocks Claude Code startup, and synchronous network calls
there can stall the session past its init timeout. The installer handles this
correctly (detached + bounded + watchdog); don't hand-edit it back to blocking.
Then apply the choice with the installer (it merges into settings.json,
preserving everything else):
python3 scripts/install_hooks.py --cadence <session-start|per-call> \
--enforcement <warn|block> --link-check <off|resolve>
python3 scripts/install_hooks.py --uninstall
Use AskUserQuestion for these two questions when you have it. Be honest about
the trade-off: only the deterministic engine runs in a hook (no model, no
network) — the deep review and live URL verification still happen when the user
runs /security-audit.
Workflow — scan / full (auditing skills & MCPs)
- Run the engine with
--json (above). Read summary.
- Short-circuit check. In
scan mode, stop early only when
summary.new + summary.changed == 0 and there are no carried-forward
findings and urls_to_verify is empty (no links are due for
re-verification). Then report: ✓ No changes since last audit (<last_audit>). N items unchanged — nothing to review. If any of those is non-empty, there is
still work to do — continue. (Suggest /security-audit full for a forced deep
pass.)
- Review changed items. For each entry in
review_items, look at its
findings. The engine already classifies the obvious things (plaintext
secrets in MCP env, unpinned npx -y packages, hidden-instruction phrasing,
credential reads, obfuscation, shorteners/IP-literal hosts). Add your own
judgement: read the actual SKILL.md/scripts of a flagged item if a finding
is ambiguous, and decide whether it is a real risk or a false positive.
- Verify external links live (the TOCTOU step). This is time-based, not
file-change-based — a skill file can stay byte-identical while the domain
behind one of its links is repointed. The engine therefore lists every link
that is new or whose last check is older than the TTL (default 7 days;
--url-ttl-hours, 0 = always) in urls_to_verify, even for items whose
hash did not change. To actually re-verify, run the engine with
--resolve-urls (e.g. scripts/scan.py scan --resolve-urls): it follows each
redirect chain, compares the final destination + content fingerprint against
the cached trusted value, refreshes the cache, and emits a Critical
URL_REDIRECT_CHANGED if a previously-trusted link now lands somewhere new
(or High URL_CONTENT_CHANGED if the page changed). Then use WebFetch
on anything flagged (or anything you want a content-level judgement on) to
confirm whether the new destination is actually hostile. Treat a redirect
change on a link the agent was about to act on as stop-and-confirm.
- Render the findings table (template below).
- Let the engine update its cache. A normal (non-
--no-update) run records
the new hashes so the next audit is cheap. Re-run without --json if you only
need the cache write, or trust the run you already did (it updates unless you
passed --no-update).
Workflow — deploy (pre-publish exposure gate)
This is the hard gate before anything goes public — it mirrors the user's global
publish rules.
- Run
python3 scripts/scan.py deploy --project "<repo>" --json.
- Review every finding. Exposure findings (real secrets, tokens, private keys,
presigned URLs, personal emails, a personal git commit email, EXIF GPS) are
what matter here.
- Verdict is a gate: if the engine returns BLOCK (any Critical, or any
High in deploy mode), tell the user clearly: do not publish until these are
fixed. Offer to remediate (move secrets to env vars, switch git email to a
noreply, strip EXIF, add .gitignore rules), then re-run until PASS.
- Only after PASS should the push/publish proceed. If a real secret was
found, remind the user to rotate it — removing it from the file is not
enough once it has existed.
Output: the findings table (use this exact shape)
## 🔒 Security Audit — <mode> · <N> scanned, <M> changed · Verdict: <✅ PASS | ⚠️ FLAGGED | ⛔ BLOCK>
| # | Severity | Finding | Location | Why it matters | How to fix |
|---|----------|---------|----------|----------------|------------|
| 1 | 🔴 Critical | <title> | `<path:line>` | <one sentence> | <one action> |
**Summary:** <X 🔴> · <Y 🟠> · <Z 🟡> … — Verdict: <…>
- Severity scale: 🔴 Critical · 🟠 High · 🟡 Medium · 🔵 Low · ⚪ Info.
- Keep evidence redacted (the engine already masks it, e.g.
AQ.Ab8…HsY6) so
the report itself never leaks a secret. Put redacted evidence in a collapsible
appendix, not the main table.
- End a
deploy BLOCK with an explicit "do not publish yet" line.
scripts/render.py produces exactly this table from the engine's JSON
(scan.py <mode> --json | python3 render.py -). Use it when you want a
guaranteed-consistent render; weave in your live WebFetch URL results around it.
How severity is assigned
Briefly: Critical = active compromise or a live exposed credential (plaintext
secret in config, reverse shell, a redirect that now lands somewhere new). High =
strong exfiltration/hidden-instruction signal or a real exposed token before
publish. Medium = supply-chain weakness or risky-but-common patterns (unpinned
package, remote-installer-piped-to-shell). Low/Info = hygiene. The full rubric and
the complete rule catalog (ids, patterns, fixes) are in references/.
Suppressing a false positive
If a flagged line is an intentional example, a detector definition, or a vetted
exception, add an inline marker on that line and it will be ignored on future
runs: # security-audit: ignore (also accepts pragma: allowlist secret /
nosec). Use this sparingly and only when you are sure.
Reference files
Read these when you need detail beyond the summary:
references/severity-model.md — how each severity is decided, and the verdict
gate (PASS / FLAGGED / BLOCK).
references/detection-rules.md — the full catalog: every rule id, what it
matches, its severity, why it matters, and how to fix it.
references/url-verification.md — the redirect / time-of-check-time-of-use
methodology and how the URL fingerprint cache detects a repointed link.
Notes
- Network is only needed for live URL verification (WebFetch, or
--resolve-urls).
Everything else is fully offline and deterministic.
- The cache and any resolved-URL fingerprints live under
~/.claude/.security-audit/,
outside any repo, and must never be published.