What it detects
toolpoison is a static, offline scanner for MCP (Model Context Protocol) tool manifests. It reads the JSON a server returns from tools/list and flags patterns associated with tool poisoning and prompt injection: hidden instructions in descriptions, invisible Unicode, references to secrets, exfiltration hints, data-smuggling parameters, cross-tool shadowing, confusable names, duplicates, and hidden markup.
Honesty notice. toolpoison uses heuristic pattern matching. A clean result does not prove a server is safe. It never runs or connects to a server, so it cannot see runtime behavior (what the tool actually does with your data), tool *outputs*, or descriptions that change after you approved them ("rug pulls"). The only way it can catch a rug pull is if you save snapshots and compare them with
--diff. Expect false positives and false negatives. A determined attacker can phrase instructions in ways no regex anticipates.
Rules
| ID | Name | Default severity | What it flags |
|---|---|---|---|
| TP01 | hidden-instruction | high | "ignore previous instructions", "do not tell the user", <IMPORTANT>/<system> tags, "before using this tool, read…", "otherwise the tool will not work" |
| TP02 | invisible-unicode | high / critical | Zero-width chars, BOM, soft hyphen, variation selectors, private-use chars, and other format/control chars (high). Bidi overrides and Unicode tag characters (critical). Tag text is decoded and shown |
| TP03 | sensitive-path | high | ~/.ssh, id_rsa, .env, ~/.aws/credentials, mcp.json, claude_desktop_config.json, ~/.cursor/, kube/docker/npm/pypi/netrc/git credentials, /etc/passwd, shell history, private keys/seed phrases |
| TP04 | exfiltration-hint | high (medium for base64-only) | "send … to https://…", curl/wget to URLs, "include the contents in the X parameter", base64-encoding instructions, known request-catcher domains, hard-coded BCC/CC addresses |
| TP05 | smuggling-parameter | medium (high / low) | Parameter names such as sidenote, notes, context, metadata, feedback, reasoning. Escalates to high if the parameter's description asks for file contents, keys, credentials, conversation history, and similar. Drops to low if the parameter has no description |
| TP06 | cross-tool-shadowing | high | "when the send_email tool is used…", "instead of the X tool", "all other tools must…", "always BCC/redirect…" |
| TP07 | excessive-length | low | Descriptions longer than --max-desc-len (default 1000 chars) |
| TP08 | confusable-name | medium (high if mixed scripts) | Tool or parameter names that mix scripts (for example, a Cyrillic а inside read_file) or contain non-ASCII letters |
| TP09 | duplicate-name | medium | The same tool name (NFKC + case-folded) defined more than once, within or across scanned files |
| TP10 | hidden-markup | medium | HTML comments <!-- -->, display:none/hidden elements, <details>, markdown comments [//]: #, script/style/iframe tags, long runs of blank lines or padding that push text out of view |
| TP11 | annotation-deception | high | readOnlyHint / destructiveHint / title annotations that contradict a destructive name, description, or schema |
| TP12 | hidden-schema-default | high | Hidden or unusual schema defaults on exfil-shaped parameters (bcc, webhook, callback, and similar) |
| TP13 | preference-manipulation | high | Description steers the model to prefer this tool, levy an extra fee, or always invoke a follow-up tool |
| TP14 | elicitation-credential-phish | high | Tool text or server-level instructions / serverInstructions tell the model to use MCP elicitation to harvest a password, token, or other secret |
Usage
# Scan one or more manifests
python3 toolpoison.py server-a.json server-b.json
# Read from stdin (for example, a saved tools/list response)
cat tools.json | python3 toolpoison.py -
# Machine-readable output, failing only on high/critical
python3 toolpoison.py --json --min-severity high tools.json
# Rug-pull check: compare an approved snapshot with the current one (also scans NEW)
python3 toolpoison.py --diff approved.json current.json
python3 toolpoison.py --list-rulesAfter pip install of the kit, the same options work as agentsec toolpoison …. Every tool also accepts --help and almost every tool accepts --json.
Example output
These are real runs against the files in examples/.
python3 toolpoison.py clean.json (exit code 0):
python3 toolpoison.py poisoned.json --min-severity high (exit code 1):
python3 toolpoison.py --diff clean.json clean_v2_rugpull.json (exit code 1):
Scanned 2 tool(s) in 1 file(s): no findings (min severity: low).
Note: toolpoison is heuristic pattern matching. A clean result does not prove a server is safe; it cannot see runtime behaviour or descriptions that change after approval (rug pulls) unless you diff snapshots (--diff).
SARIF and CI
agentsec toolpoison path/to/input --json > toolpoison.json
# toolpoison does not yet emit SARIF; use --json and keep --fail-on for CI.
# GitHub Actions (composite action ships with the repo)
- uses: dunkleschitz/agentsec@v0
with:
tools: toolpoison
paths: path/to/input
fail-on: high
--sarif is not available on this tool yet (resultguard and toolpoison are reserved as good first issues). Use --json and a --fail-on exit gate instead.
OWASP mapping
Heuristic mapping of what this tool helps a reviewer look for. Not an official OWASP endorsement, and not complete coverage of any item. Sources: OWASP Top 10 for LLM Applications 2025 and OWASP Top 10 for Agentic Applications 2026.
- LLM01 Prompt Injection — OWASP LLM Top 10 (2025)
- ASI04 Agentic Supply Chain Vulnerabilities — OWASP Agentic Top 10 (2026)
Known limits
- Heuristic only. Regexes catch common phrasings. Paraphrases, other languages, typos, splitting instructions across fields, or encoding them (other than Unicode tags) can evade detection. Benign tools can also trigger findings, for example a legitimate SSH tool that mentions
~/.ssh. Treat findings as prompts for human review, not as verdicts. - Static only. The scanner sees only the manifest you give it. It does not see tool *results* (indirect prompt injection via returned content), server code, network behavior, or permissions.
- Rug pulls. A server can serve a benign description at approval time and a malicious one later. toolpoison detects this only when you keep snapshots and run
--diff. In diff mode, if a snapshot contains duplicate tool names, the last definition wins (TP09 still flags the duplicates). - Confusables. TP08 flags mixed scripts and non-ASCII letters in names. It does not use the full Unicode confusables table, so whole-script lookalikes in descriptions are not detected.
- It cannot prove a server is safe.
Full source, tests, and fixtures: tools/toolpoison/ in the repository. Install the kit with pip install git+https://github.com/dunkleschitz/agentsec.git.