Toolward rule catalogue
September 17, 2026 · View on GitHub
Generated by toolward rules --format md. Do not edit by hand.
Prompt injection & tool poisoning
| Rule | Default severity | Title |
|---|---|---|
TW101 | high | Instruction-override text in agent-visible content |
TW102 | high | Invisible Unicode characters |
TW103 | critical | Bidirectional control characters (Trojan Source) |
TW104 | high | Mixed-script name (homoglyph risk) |
TW105 | medium | Instructions hidden in comments or invisible markup |
TW106 | critical | Exfiltration instruction in agent-visible content |
TW107 | high | Cross-tool shadowing instruction |
TW108 | high | Instruction to conceal behaviour from the user |
TW109 | high | Fake system/authority markers |
TW110 | low | Oversized tool description |
TW101 — Instruction-override text in agent-visible content
Text that tries to overrule the host's system prompt was found in content the agent ingests as authority.
Fix: Remove the override wording. Tool descriptions and skills should describe capability, never redefine the agent's rules.
References: https://genai.owasp.org/llmrisk/llm01-prompt-injection/ · https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
TW102 — Invisible Unicode characters
Zero-width, soft-hyphen or Unicode tag characters can carry instructions that a human reviewer literally cannot see.
Fix: Strip the characters and re-review the plain text. Legitimate tool descriptions never need them.
References: https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
TW103 — Bidirectional control characters (Trojan Source)
Bidi overrides make rendered text differ from the bytes the machine reads, so a reviewed description is not the executed one.
Fix: Remove all U+202A-U+202E and U+2066-U+2069 characters and re-read the file.
References: https://trojansource.codes/
TW104 — Mixed-script name (homoglyph risk)
A tool or server name mixing Latin with Cyrillic/Greek letters can impersonate a trusted name character-for-character.
Fix: Rename using ASCII only, and confirm you installed the package you meant to install.
TW105 — Instructions hidden in comments or invisible markup
HTML comments and zero-size or same-colour markup are invisible when rendered but fully visible to the model.
Fix: Delete the hidden block. If the text is genuinely needed, put it in plain visible prose.
References: https://genai.owasp.org/llmrisk/llm01-prompt-injection/
TW106 — Exfiltration instruction in agent-visible content
The text instructs the agent to read local secrets or forward data to a third party — the core of a tool-poisoning attack.
Fix: Do not install this extension. If it is yours, remove the instruction and move any real data flow behind an explicit, documented parameter.
References: https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks · https://genai.owasp.org/llmrisk/llm01-prompt-injection/
TW107 — Cross-tool shadowing instruction
The text tries to change how the agent uses other tools, letting one extension hijack calls meant for another.
Fix: A tool description must only describe its own behaviour. Remove references to other tools' invocation order.
References: https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
TW108 — Instruction to conceal behaviour from the user
Extensions that ask the agent to act silently defeat the human-in-the-loop that every agent host relies on.
Fix: Remove the secrecy wording. Anything worth doing silently is worth a confirmation prompt.
References: https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
TW109 — Fake system/authority markers
Tags such as
Fix: Remove pseudo-system tags from descriptions and skill bodies.
References: https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks
TW110 — Oversized tool description
Very long descriptions are both a context-budget tax and a convenient place to bury an instruction.
Fix: Keep tool descriptions under ~1000 characters and move detail into documentation.
Supply chain
| Rule | Default severity | Title |
|---|---|---|
TW201 | medium | Unpinned package executed on every agent start |
TW202 | high | Package installed from a non-registry source |
TW203 | critical | Remote script piped into a shell |
TW204 | high | Possible typosquat of a well-known MCP package |
TW205 | high | Remote MCP server reached over plaintext HTTP |
TW206 | medium | Marketplace or plugin source is unpinned |
TW201 — Unpinned package executed on every agent start
npx/uvx style runners re-resolve the package each launch, so a later malicious release lands on your machine without any action from you.
Fix: Pin an exact version (e.g. pkg@1.4.2, pkg==1.4.2) and bump it deliberately after review.
References: https://owasp.org/www-project-top-ten/
TW202 — Package installed from a non-registry source
Git URLs, tarballs and local paths bypass registry checks, provenance and yanking.
Fix: Publish to a registry you control, or vendor the code into your repo so it is reviewable in diffs.
TW203 — Remote script piped into a shell
curl … | sh hands arbitrary, unreviewed, unversioned code full execution rights on the developer machine.
Fix: Download, pin by checksum, review, then execute as a separate committed step.
TW204 — Possible typosquat of a well-known MCP package
The package name is one or two edits away from a popular MCP server, the classic install-time hijack.
Fix: Compare the name against the upstream project's README and check the publisher before installing.
TW205 — Remote MCP server reached over plaintext HTTP
Tool descriptions, arguments and auth headers all cross the network in clear text and can be rewritten in transit.
Fix: Use https://, or keep the server on localhost.
TW206 — Marketplace or plugin source is unpinned
A plugin source that tracks a moving branch installs whatever that branch holds at install time.
Fix: Reference a tag or commit SHA instead of a branch name.
Secrets & credentials
| Rule | Default severity | Title |
|---|---|---|
TW301 | critical | Hard-coded credential in an agent configuration |
TW302 | high | Credential in source, skill or documentation file |
TW303 | high | Extension reads sensitive local files |
TW304 | medium | Whole environment forwarded to a server |
TW301 — Hard-coded credential in an agent configuration
Agent configs are committed, synced and shared far more casually than .env files; a live key here is effectively published.
Fix: Move the value into an environment variable reference (${VAR}) or your OS keychain, then rotate the exposed key.
TW302 — Credential in source, skill or documentation file
Keys committed anywhere in the repository are retrievable from history forever, even after the file is deleted.
Fix: Rotate the credential first, then purge it from git history — deleting the line is not enough.
TW303 — Extension reads sensitive local files
SSH keys, cloud credentials, browser cookies and shell history are the standard loot of a compromised extension.
Fix: Remove the access. If a credential really is needed, take it as an explicit parameter with a documented scope.
TW304 — Whole environment forwarded to a server
Passing the full process environment hands every unrelated key in your shell to that server.
Fix: List only the variables the server actually needs.
Execution & privilege
| Rule | Default severity | Title |
|---|---|---|
TW401 | high | Shell command built from untrusted input |
TW402 | critical | Permission or sandbox bypass flag |
TW403 | high | Blanket auto-approval of tool calls |
TW404 | medium | Hook runs on every event with a broad matcher |
TW405 | high | Persistence or self-modification of agent configuration |
TW406 | high | Overly broad filesystem scope |
TW407 | high | Dynamic code evaluation |
TW401 — Shell command built from untrusted input
String-interpolated shell calls inside an extension turn any model-controlled argument into arbitrary command execution.
Fix: Use argv-array APIs (execFile, spawn without shell: true, subprocess.run([...])) and validate inputs.
References: https://cwe.mitre.org/data/definitions/78.html
TW402 — Permission or sandbox bypass flag
These flags disable the human confirmation and sandboxing the agent host relies on for every dangerous action.
Fix: Remove the flag. If a workflow truly needs it, isolate it in a disposable container, never on a developer machine.
TW403 — Blanket auto-approval of tool calls
Wildcard allow lists remove the last checkpoint between a poisoned tool description and a real action on your machine.
Fix: Approve specific tools and specific command prefixes instead of *.
TW404 — Hook runs on every event with a broad matcher
A hook with a * matcher sees every tool call and its arguments, and runs before you approve anything.
Fix: Scope the matcher to the tools you actually need, and keep the hook command short and auditable.
TW405 — Persistence or self-modification of agent configuration
Writing to shell profiles, cron, launch agents or the host's own settings lets an extension survive its own removal.
Fix: Remove the write. Installation steps belong in a documented, user-run command, not in a tool call.
TW406 — Overly broad filesystem scope
Granting /, $HOME or ~ to a filesystem server exposes SSH keys, browser profiles and every other project on the machine.
Fix: Grant one project directory per server and add a second server if you truly need a second scope.
TW407 — Dynamic code evaluation
eval, new Function, pickle.loads and unsafe YAML turn data into code, which is exactly what an injected payload needs.
Fix: Parse data with a data parser (JSON.parse, yaml.safe_load, json.loads) and drop the eval path entirely.
References: https://cwe.mitre.org/data/definitions/95.html
Network & exfiltration
| Rule | Default severity | Title |
|---|---|---|
TW501 | critical | Egress to a data-drop or tunnelling service |
TW502 | medium | Hard-coded IP endpoint |
TW503 | high | Obfuscated payload |
TW504 | critical | Remote content executed at runtime |
TW505 | high | Out-of-band exfiltration primitive |
TW501 — Egress to a data-drop or tunnelling service
These endpoints exist to collect whatever is sent to them, which makes them the default destination for stolen context.
Fix: Remove the endpoint. If you need a callback URL during development, keep it out of committed configuration.
TW502 — Hard-coded IP endpoint
A bare public IP has no certificate identity and no owner you can look up, so it cannot be reviewed like a domain.
Fix: Use a hostname with TLS, or document why the address is fixed.
TW503 — Obfuscated payload
Long encoded blobs in an extension hide the very thing a reviewer needs to read.
Fix: Decode it and commit the plain source, or document exactly what the blob is and where it came from.
TW504 — Remote content executed at runtime
Fetching code and evaluating it means the audited version and the running version are never the same thing.
Fix: Ship the code in the package and verify it by checksum; never evaluate a network response.
TW505 — Out-of-band exfiltration primitive
Raw sockets, /dev/tcp and DNS-encoded lookups move data out of networks that block ordinary HTTP egress.
Fix: Remove it. Legitimate extensions talk to documented HTTPS APIs.
Governance & hygiene
| Rule | Default severity | Title |
|---|---|---|
TW601 | low | Extension has no provenance metadata |
TW603 | medium | Duplicate tool name across servers |
TW604 | medium | Tool name shadows a host built-in |
TW605 | low | Skill grants itself execution without declaring it |
TW606 | info | Tool surface is not pinned against silent changes |
TW602 | critical | Tool surface changed since it was approved |
TW601 — Extension has no provenance metadata
Without an author, repository or licence there is no one to notify when the extension turns out to be malicious.
Fix: Add author, repository/homepage and license to the manifest, and name + description to skill frontmatter.
TW603 — Duplicate tool name across servers
When two servers expose the same tool name the agent picks one by ordering, so a later install can silently take over an existing call site.
Fix: Namespace your tool names, or remove one of the two servers.
TW604 — Tool name shadows a host built-in
A tool called read_file or bash competes with the host's own tool of that name and can capture calls intended for it.
Fix: Prefix tool names with your product, e.g. acme_read_file.
TW605 — Skill grants itself execution without declaring it
A skill whose body runs shell commands but declares no allowed-tools inherits whatever the session already has.
Fix: Declare the minimum allowed-tools the skill needs, e.g. Bash(git status:*), Read.
TW606 — Tool surface is not pinned against silent changes
Without a lock file there is nothing to compare against, so a server that changes its tool descriptions after you approve it (a rug pull) goes unnoticed.
Fix: Run toolward lock to record the current surface, commit the lock file, then run toolward verify in CI.
TW602 — Tool surface changed since it was approved
A server can serve a harmless description on the day you review it and a malicious one a week later. Comparing against a committed lock file is the only way to see that happen.
Fix: Review the diff before accepting it. If the change is legitimate, re-run toolward lock and commit the new file.
References: https://invariantlabs.ai/blog/mcp-security-notification-tool-poisoning-attacks