Security scanner for MCP servers and AI agent skills. Scan third-party code before your agent runs it.
npx acidtest scan ./mcp-server
npx acidtest scan ./downloaded-skillNo install required. No API keys. No configuration.
AcidTest v2.0.1
Scanning: system-helper
Source: test-fixtures/fixture-danger
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TRUST SCORE: 0/100 ░░░░░░░░░░ DANGER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
FINDINGS
✖ CRITICAL instruction-override
SKILL.md:4
Attempts to override agent instructions
✖ CRITICAL eval-usage
handler.ts:12
Uses eval() function
✖ HIGH maintenance-mode
SKILL.md:4
Claims the system is in maintenance mode
... 13 more findings (9 CRITICAL, 3 HIGH, 2 MEDIUM, 1 LOW, 1 INFO total)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
RECOMMENDATION: Do not install. Prompt injection attempt detected.
Abridged output from acidtest scan test-fixtures/fixture-danger — one of the fixtures bundled in this repo, so you can reproduce it after cloning.
- Command & Code Injection -
eval(),exec(), shell injection, unsafe deserialization - Data Exfiltration - Tracks data flow from env vars/secrets to network calls
- Credential Theft - Hardcoded API keys, SSH key injection, token leaks, MCP-channel env exfil
- C2 Callbacks - Suspicious network requests to raw IPs or sketchy domains
- Obfuscation - Base64/hex payloads, entropy analysis, invisible-Unicode/Trojan-Source hiding
- Prompt Injection - Instruction override in SKILL.md body and frontmatter, plus 2026 phrasings (fake system-reminder blocks, embedded tool-call JSON)
- MCP Tool Poisoning - Instructions smuggled into tool/parameter descriptions; cross-server shadowing
- Rug-Pull Updates -
acidtest diffflags a new version that adds capability the old one lacked - Permission Escalation - Undeclared filesystem/network/shell access
84 security patterns across 12 category files, plus TypeScript/JavaScript AST and version-diff analysis. Scans MCP manifests, SKILL.md, and Python/TypeScript/JavaScript source. Run npm run validate:patterns to regenerate the count.
npm install -g acidtestOr run without installing:
npx acidtest scan ./path-to-skillIf you write an MCP server, run acidtest lint on it before you publish.
It checks your own tool and parameter descriptions for the things a
consumer's security scanner will flag — injected-looking instructions,
<IMPORTANT>-style tags, "do not tell the user" directives, covert
parameter names, and "use this instead of the official server" phrasing —
and points at the exact line so you can fix it or confirm it's intentional.
# Lint a manifest, a source file, or a whole server directory
acidtest lint ./my-server
acidtest lint ./src/tools/weather.tsIt reads descriptions both from a static mcp.json and from
description: string literals in your TypeScript/JavaScript/Python source,
because that's where real servers declare them. Output is eslint-shaped and
it exits non-zero on error-level findings, so it drops into a pre-commit
hook or CI.
The point is precision: it stays quiet on legitimate wording. Run against
the seven official modelcontextprotocol/servers reference servers, it
reports zero findings — while still catching a poisoned description. It
does not cry wolf on "you must provide a valid input."
# Lint your own MCP server before publishing
acidtest lint ./my-server
# Walk the Q4-2026 attack classes with the real scanner
# (fixtures are generated on the fly, nothing is left on disk)
acidtest demo
# Scan a skill or MCP server
acidtest scan ./my-skill
acidtest scan ./my-mcp-server
# Scan all skills in a directory
acidtest scan-all ./skills
# Check an update for a rug-pull (new version adds capability the old lacked)
acidtest diff ./skill-v1 ./skill-v2
# Show remediation suggestions
acidtest scan ./my-skill --fix
# JSON output
acidtest scan ./my-skill --jsonAcidTest runs two analysis layers:
- Injection scan - checks tool and parameter descriptions, MCP manifests, and markdown for instruction-override attempts, tool poisoning, covert parameters, cross-server shadowing, and invisible-Unicode payloads
- Code analysis - regex patterns (dangerous imports, exfil sinks,
credentials, Python sinks) plus TypeScript/JavaScript AST checks for
eval, dynamicrequire, the Function constructor, and bracket-notation bypasses
A single CRITICAL finding — an env exfil, a poisoned tool description — floors the result to at least FAIL, so a real attack is never reported as a mere warning.
See METHODOLOGY.md for the details and limits.
AcidTest has been run against 2,386 public agent skills from a large open skills repository. On that corpus it flagged live malicious payloads, including:
- C2 callbacks to raw IPs (
91.92.242.30) - SSH key injection into
~/.ssh/authorized_keys - Namespace squatting attacks
- Base64-encoded remote code execution
These are the same classes of attack that now show up in MCP servers and agent skills across the ecosystem.
Every command below exits non-zero on FAIL or DANGER, so hooks and scripts can gate on the result.
acidtest scan-all ~/.claude/plugins
acidtest scan-all ~/.claude/skillsThis exits 1 if any skill is FAIL or DANGER, so it works in a cron job or shell alias.
Block a skill or plugin before it runs. A Claude Code PreToolUse hook receives the tool call as JSON on stdin and blocks the action by exiting with code 2. This hook watches Bash calls that look like an install, scans the target directory, and refuses if the score is bad.
Install the ready-made hook:
mkdir -p ~/.claude/hooks
curl -o ~/.claude/hooks/acidtest-preinstall.sh \
https://raw.githubusercontent.com/chuckyatsuk/acidtest/main/hooks/claude-code-preinstall.sh
chmod +x ~/.claude/hooks/acidtest-preinstall.shOr write it yourself — this is the whole thing:
#!/usr/bin/env bash
# Blocks a Claude Code install action when AcidTest flags the target.
set -euo pipefail
# PreToolUse passes the tool call as JSON on stdin.
payload="$(cat)"
cmd="$(printf '%s' "$payload" | jq -r '.tool_input.command // empty')"
# Only act on install-shaped commands; let everything else through (exit 0).
case "$cmd" in
*"acidtest"*) exit 0 ;; # don't scan our own scans
*install*|*"plugin add"*|*clone*) ;; # scan these
*) exit 0 ;;
esac
# Scan the project directory the session is running in.
target="${CLAUDE_PROJECT_DIR:-.}"
if ! acidtest scan "$target" --json > /tmp/acidtest-preinstall.json 2>/dev/null; then
status="$(jq -r '.status' /tmp/acidtest-preinstall.json)"
{
echo "🛑 AcidTest blocked this action: $target is $status"
jq -r '.findings[] | select(.severity=="CRITICAL" or .severity=="HIGH")
| " [\(.severity)] \(.title): \(.detail)"' /tmp/acidtest-preinstall.json
} >&2
exit 2 # exit 2 is what tells Claude Code to block the tool call
fi
exit 0Wire it into ~/.claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{ "type": "command", "command": "~/.claude/hooks/acidtest-preinstall.sh" }
]
}
]
}
}Two things make this work: reading the tool call from stdin, and exit 2 to block (any other exit lets the action proceed). Adjust the case matcher to match your install flow. acidtest and jq must be on PATH.
For a postinstall check instead of a live hook, run the scan as a plain script. acidtest scan <dir> exits non-zero on FAIL or DANGER, so acidtest scan ./new-skill || echo blocked is enough.
# Diff two versions; exits 1 on a RUG_PULL verdict
acidtest diff ./skill-v1 ./skill-v2Add to .github/workflows/acidtest.yml:
name: Security Scan
on: [pull_request, push]
jobs:
acidtest:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx acidtest@latest scan . --json > results.json
- run: |
STATUS=$(jq -r '.status' results.json)
if [ "$STATUS" = "FAIL" ] || [ "$STATUS" = "DANGER" ]; then
echo "❌ Security scan failed"
exit 1
fiSee .github/workflows/acidtest-pr-comment.yml for a full example with PR comments.
curl -o .git/hooks/pre-commit https://raw.githubusercontent.com/chuckyatsuk/acidtest/main/hooks/pre-commit
chmod +x .git/hooks/pre-commitEvery commit now runs a scan first.
[](https://github.com/chuckyatsuk/acidtest)AcidTest can run as an MCP server so an agent like Claude can scan skills before installing them.
Add to claude_desktop_config.json:
{
"mcpServers": {
"acidtest": {
"command": "npx",
"args": ["-y", "acidtest", "serve"]
}
}
}It exposes three tools: scan_skill (scan a skill or MCP server),
lint_mcp_server (lint tool descriptions the way an author would), and
scan_all (scan a directory). So an agent can check a server before trusting
it:
User: "Can you scan this skill before I install it?"
Claude: [Uses acidtest scan_skill to analyze]
Create .acidtest.json in your skill directory:
{
"ignore": {
"patterns": ["di-008"],
"categories": ["obfuscation"],
"files": ["vendor/**", "*.min.js"]
},
"thresholds": {
"minScore": 80,
"failOn": ["CRITICAL", "HIGH"]
},
"output": {
"format": "detailed",
"showRemediation": true
}
}CLI flags override config file settings.
Starts at 100, deducts by severity:
- CRITICAL: -25 points
- HIGH: -15 points
- MEDIUM: -8 points
- LOW: -3 points
Ratings:
- 80-100: PASS (green)
- 50-79: WARN (yellow)
- 20-49: FAIL (orange)
- 0-19: DANGER (red)
- Zero-day exploits in the Node.js or Python runtime
- Vulnerabilities in npm/pip dependencies (use
npm audit/pip-audit) - Runtime behavior that static analysis can't see
- Advanced obfuscation or VM-level evasion
METHODOLOGY.md covers the limits in detail.
AcidTest is static analysis. It pairs with:
- npm audit / pip-audit - dependency vulnerabilities
- VirusTotal - known malware signatures
- Sandboxing - runtime isolation (Docker, VMs, Firecracker)
No single tool catches everything.
Detection patterns are JSON files in src/patterns/. To add a new pattern:
- Add pattern to the appropriate category file
- Test with
npm test - Submit a PR
See CONTRIBUTING.md for details.
- Methodology - Technical details and limitations
- Changelog - Version history
- Contributing - How to add patterns
- Security Policy - Responsible disclosure
- Template Repository - Starter kit
- Website: https://acidtest.currently.website
- NPM: https://www.npmjs.com/package/acidtest
- GitHub: https://github.com/chuckyatsuk/acidtest
- Issues: https://github.com/chuckyatsuk/acidtest/issues
MIT