Toolward rule catalogue

September 17, 2026 · View on GitHub

Generated by toolward rules --format md. Do not edit by hand.

Prompt injection & tool poisoning

RuleDefault severityTitle
TW101highInstruction-override text in agent-visible content
TW102highInvisible Unicode characters
TW103criticalBidirectional control characters (Trojan Source)
TW104highMixed-script name (homoglyph risk)
TW105mediumInstructions hidden in comments or invisible markup
TW106criticalExfiltration instruction in agent-visible content
TW107highCross-tool shadowing instruction
TW108highInstruction to conceal behaviour from the user
TW109highFake system/authority markers
TW110lowOversized 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 or [SYSTEM] impersonate the host's own prompt structure to borrow its authority.

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

RuleDefault severityTitle
TW201mediumUnpinned package executed on every agent start
TW202highPackage installed from a non-registry source
TW203criticalRemote script piped into a shell
TW204highPossible typosquat of a well-known MCP package
TW205highRemote MCP server reached over plaintext HTTP
TW206mediumMarketplace 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

RuleDefault severityTitle
TW301criticalHard-coded credential in an agent configuration
TW302highCredential in source, skill or documentation file
TW303highExtension reads sensitive local files
TW304mediumWhole 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

RuleDefault severityTitle
TW401highShell command built from untrusted input
TW402criticalPermission or sandbox bypass flag
TW403highBlanket auto-approval of tool calls
TW404mediumHook runs on every event with a broad matcher
TW405highPersistence or self-modification of agent configuration
TW406highOverly broad filesystem scope
TW407highDynamic 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

RuleDefault severityTitle
TW501criticalEgress to a data-drop or tunnelling service
TW502mediumHard-coded IP endpoint
TW503highObfuscated payload
TW504criticalRemote content executed at runtime
TW505highOut-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

RuleDefault severityTitle
TW601lowExtension has no provenance metadata
TW603mediumDuplicate tool name across servers
TW604mediumTool name shadows a host built-in
TW605lowSkill grants itself execution without declaring it
TW606infoTool surface is not pinned against silent changes
TW602criticalTool 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