| name | malware-scan |
| description | Scan a repository for injected malware, supply-chain backdoors, and obfuscated loaders. Use when the user asks to audit code for malicious injection, check for backdoors, investigate a suspicious commit, verify a dependency, or respond to a security incident. Also use proactively when reviewing commits that touch build configs, postinstall scripts, CI files, or code from an untrusted contributor. |
Malware Scan
⚠ Run in an isolated environment when responding to a live incident. This skill greps and runs git against a repo that may be actively hostile. A malicious .git/config (via core.fsmonitor, core.pager, core.sshCommand, submodule url = !…, etc.) can achieve RCE the moment you run any git command inside the working tree. scripts/scan.sh neutralizes the known git-config vectors, and manual history review must use the same hardened git -c ... wrapper shown below. A full triage still involves reading, decoding, and reasoning about attacker-controlled bytes — do it in a container or throwaway VM, not on a dev box with live credentials. Rotate any credentials the affected machine had access to if in doubt.
A detection skill for finding injected malware, supply-chain backdoors, and obfuscated loaders in a source repository. Built from real incident response — specifically an EtherHiding-style JS crypto-stealer that was hidden as a single 2000+ character line in hardhat.config.ts after ~200 spaces of whitespace padding. See references/incident-2026-03-25.md for the full case study.
When to use
Trigger this skill when the user asks any of:
- "audit this repo for malware / backdoors / injected code"
- "is this commit / PR / dependency safe?"
- "we think we were compromised — check the code"
- "scan for obfuscated / suspicious code"
- "review these commits from "
Also trigger proactively when reviewing changes to: *.config.{ts,js,cjs,mjs}, package.json lifecycle scripts, .github/workflows/*, Docker entrypoints, or any committed build output.
Workflow
Work through these phases in order. Report findings at the end with a severity grade. Do not stop at the first hit — complete the whole sweep so the user gets a full picture.
Phase 1 — Establish scope
Ask the user (or infer from context):
- Target: whole repo from cwd (default), a specific path, or a git commit range
- Depth: working tree only, or also git history
- Suspect: is there a particular commit, author, PR, or dependency they already mistrust?
If a suspect is named, start there, but still complete the full sweep afterwards.
Phase 2 — Literal IoC sweep (CRITICAL-tier)
Load references/iocs.md and grep every listed literal against the target. Any match is CRITICAL and must be surfaced to the user with file, line, and a decoded snippet.
Use rg -nF --no-ignore-vcs -g '!node_modules' -g '!.git' <literal> so that .gitignore doesn't hide an infected file. This is the default behavior in scripts/scan.sh; only disable it intentionally when doing a narrow, faster sweep.
Phase 3 — Structural pattern sweep (HIGH-tier)
Load references/patterns.md and run each regex against the target with rg -n -P (Perl regex mode). These patterns catch variants of known attack techniques even when the literal IoCs have been permuted.
For each hit:
- Show 3 lines of context.
- Classify: is this in production code, test code, or a legitimate library wrapper?
- Downgrade legitimate uses to INFO with a one-line justification (e.g. "
atob in auth/jwt.ts — decodes JWT payload, not an obfuscation vector").
Phase 4 — Long-line sweep (HIGH-tier for non-minified files)
The single most effective check against the whitespace-padding trick. Run:
git -c core.fsmonitor=false -c core.pager=cat -c core.editor=true -c core.sshCommand=false -c core.hooksPath=/dev/null -c protocol.file.allow=never -c protocol.ext.allow=never -c safe.directory=* ls-files -z \
| grep -zvE '\.(min\.|lock$|map$)|/dist/|/build/|/public/assets/|/\.wrangler/|/node_modules/' \
| while IFS= read -r -d '' f; do
F="$f" awk 'BEGIN{f=ENVIRON["F"]} length > 1000 {printf "%s:%d:%d\n", f, NR, length; exit}' "$f"
done
Any tracked, non-minified file with a line >1000 chars is HIGH until proven otherwise. To inspect the line, never pipe raw decoded bytes to your terminal — decoded payloads can contain ANSI escapes that spoof UI or inject keystrokes. Treat raw grep, git show, and JSON/string output from an infected repo the same way: either escape it first or render it as hex. Use base64 -d | xxd | head -40 for a hex view, or write to a file inside a sandbox/container for deeper analysis. See Phase 8 for the full quarantine rules.
Phase 5 — Package lifecycle script audit (MEDIUM-tier)
Enumerate every package.json in the tree and list the preinstall, install, postinstall, prepare, prepublish, and prepublishOnly scripts. For each non-empty one, summarize in one line what it does. Flag anything that:
- Downloads from the network (
curl, wget, fetch)
- Runs
node -e, sh -c, or evals a string
- Writes outside the package directory
- Touches
~/.ssh, ~/.aws, ~/.config, or env files
Phase 6 — Git history & commit/scope sanity (MEDIUM-tier)
If history is in scope:
- Define a hardened git wrapper and use it for every history command:
GIT="git -c core.fsmonitor=false -c core.pager=cat -c core.editor=true -c core.sshCommand=false -c core.hooksPath=/dev/null -c protocol.file.allow=never -c protocol.ext.allow=never -c safe.directory=*" .
- Run
$GIT log --oneline <range> and identify commits whose message doesn't match the files they touched. The hardhat.config.ts injection hid behind a commit titled "fix: respect starvingThreshold…" that had nothing to do with build config. That mismatch is the loudest retrospective signal.
- For each suspect commit:
$GIT show --stat --patch --format=fuller <sha> and re-apply Phase 2+3 patterns to the diff hunks. If you need to display suspicious lines, escape control characters before printing them to a terminal.
- Check if any commit author is new to the repo or authored only one commit.
Phase 7 — CI / Docker / entrypoint audit (MEDIUM-tier)
Read every .github/workflows/*.yml, Dockerfile, docker-compose*.yml, and any entrypoint.sh. Look for:
- Inline
curl | sh or wget | bash
- Secrets written to disk unencrypted
- Dynamic
run: steps pulled from untrusted branch inputs
node -e "<code>" in a script step
Phase 8 — Report
Produce a structured report:
## Malware Scan Report — <repo> @ <sha>
### CRITICAL (literal IoC hits)
- <file>:<line> — <ioc name> — <snippet>
### HIGH (structural pattern / long-line hits in prod code)
- <file>:<line> — <pattern name> — <snippet>
### MEDIUM (suspicious lifecycle / CI / config)
- <file> — <description>
### INFO (benign hits, verified clear)
- <file>:<line> — <why it's fine>
### Summary
<2–3 sentences: clean, suspicious, or compromised. Recommended next actions.>
For any CRITICAL finding, recommend:
- Do not edit in place — quarantine and revert via
git checkout <pre-infection sha> -- <file>.
- Rotate every credential the machine had access to (npm tokens, SSH keys, cloud creds, wallet private keys).
- Decode base64/XOR payloads in an isolated sandbox, never in the dev environment.
Quick-start script
For a one-shot sweep, run scripts/scan.sh <path> from this skill directory. It runs Phases 2–5 in sequence and prints an escaped findings dump that is safer to inspect in a terminal. It is not a substitute for the full workflow — it's a fast first pass.
References
references/iocs.md — literal indicators of compromise (update as new attacks are seen)
references/patterns.md — structural regex patterns for obfuscation techniques
references/incident-2026-03-25.md — case study of the EtherHiding injection that motivated this skill