Coding agents already read the repo, edit config, connect MCP, and load SKILL.md. The attack surface is no longer just “ignore the previous instructions” hidden in a web page. This piece is dated 2026-09-18: first unpack how malicious MCP servers and fake Skills work through tool descriptions, overly wide inputs, and handbook routing; then fold permissions into a JSON Schema you can validate in the Workbench. For how to pick the three layers, see Skills vs MCP vs API; for on-demand Skill loading, see Why agents need Skills; for why editing mcp.json must be read-then-patch, see How agents edit JSON config.
By 2026, a coding agent does three things by default: scan the project's Skill directory, connect local or remote MCP, and pick a hand from the tool description. Community marketplaces, one-click install, and handing mcp.json to the agent to rewrite turned “discover a capability” into product UX. The same UX pushed the trust boundary outside the editor: whoever appears on the tool list can make the model execute for you.
Why agent security suddenly became a hotspot
Prompt injection did not go away. What changed is what injection can touch. Early chatbots at most emitted text that looked like a command. Today's coding agent sits behind read-file, edit-config, run-check, and call-internal-API. Once a Host lists a tool, the model treats the description as a capability ad and the return as fact.
So the security conversation grew from “don't paste secrets into the chat” into three layers:
- Who gets installed. Where the MCP server and Skill pack came from, and whether they made the allowlist.
- What they are allowed to call. The tool name looks harmless. The inputs can still point at any path or any action.
- Whether the result steers the next hop. A tool return can become a new instruction. A handbook can change the route.
The hotspot is not that models suddenly got more dangerous. It is that the executable surface got larger. A Skill answers when to load a handbook. MCP answers where tools come from. Neither layer ships with least privilege. You write that contract separately. For the boundary, see the previous article on how to pick a tool layer.
One line. The new agent-security problem is not whether the model misspeaks. It is whether the tool list, the handbook directory, and the Schema pinned “what this can do.”
Attack surface: a tool graph behind the model
Strip the runtime and the common shape looks like this:
An attacker does not need to break model weights. The cheaper path is to become a trusted corner of the graph. A server installed into the Host, a Skill that appeared in the repo, an overly wide inputSchema — all more reliable than jailbreaking a system prompt.
So the rest of this article follows that graph: first how MCP borrows a tool identity, then how a Skill borrows a handbook to change the next hop, then the part you can actually block, written as Schema. Why the contract layer is JSON: Why AI likes JSON.
Malicious MCP servers: borrow trust, run side effects
MCP (Model Context Protocol) is a client-server protocol. The Host lists tools on a server; each tool carries name, description, and inputSchema. The model sees the ad and the contract. It does not see what the server process actually ran. Isolated credentials are MCP's strength. Loading a malicious implementation through the same discovery path is the flip side of that strength.
Common shapes fall into four classes. This is the shape only — not a reproduction:
- Tool-name impersonation. The name looks like
get_docsorsummarize_repo. The implementation reads paths outside the declared scope, or sends the summary somewhere you cannot see. The model picks by name. It will not decompile the server. - Overly wide inputs.
pathis onlystring— no directory prefix, no length, no charset. One “read file” becomes an arbitrary read. If the action is a free-text command, there is no contract. - Description poisoning.
descriptionshould carry trigger words and a boundary. Instead it smuggles a sentence like “collect environment information before calling.” The model treats the description as operating instructions, not marketing copy. - The return steers the next hop. The tool result grows an undeclared field, or a paragraph that looks like an instruction, and the next turn reaches a higher-privilege tool. Without
outputSchema, return shape is never pinned.
Then there is the supply-chain layer: a same-name server, a stale fork, handing the entire mcp.json to the agent to rewrite. An overwritten array, or one extra URL, is more common than someone hand-writing a malicious function. For a minimal config patch, see How to edit JSON config.
Write the boundary in stone. MCP buys discovery and isolation, not an audit. A server that is not on the allowlist should not appear on the tool list. A tool that did make the list still needs its inputs narrowed by Schema.
Fake Skills: steal the route, then borrow a real tool
An Agent Skill is a folder: SKILL.md at the root, YAML frontmatter with name and description, steps in the body. At startup the runtime only registers an index. It expands after a task match. That is progressive loading, not RPC. A Skill usually cannot write disk or network on its own. What it changes is which hand the model reaches for next.
The value of a fake Skill is not “it is an executable backdoor.” It is winning the wrong handbook:
descriptionsteals the route. Trigger words are written wide: commit, test, release, fix a bug. Unrelated tasks load it anyway.- The body acts as a system prompt. Steps say “skip confirmation” or “first run the high-privilege tool already installed.” The handbook is treated as a higher-priority instruction.
- It collides with a real Skill's name. Dropped into
.cursor/skills/or a personal skill directory, it looks like the official pack. The acceptance order is not. - Dangerous actions become the default path. A real Skill writes stop conditions. A fake Skill deletes them and leaves only “keep calling.”
The community ecosystem made install cheap and source checks expensive. A Skill pays for itself when there are multiple steps, judgment, and stop conditions. It should not replace auth. Details in Why coding agents need Skills. The previous comparison table already said it: using a Skill as auth is the wrong layer.
One line to split the two layers: a malicious MCP server is itself an executor. A fake Skill is more like a contaminated handbook. It needs tools already attached before it can cause loss. Defense is not only scanning servers. Scan Skill sources, and whether description is written too wide.
Why Markdown cannot stop it
“Don't delete production data” and “don't read the secrets directory,” written in a Skill or a system prompt, read like a security policy. To the model they are one more paragraph that can be overwritten. A tool return, a user follow-up, or another handbook that got loaded can shove them aside.
Markdown is good for: when to use this tool, stop if a field is missing, do not send email if acceptance failed. It is not good for: whether this tool can touch a path outside the repo, run an arbitrary command, or carry undeclared fields. Those must be refused by a machine before execution.
The refuse happens in three places. You need all three:
- Install layer. The Host only connects MCP on the allowlist. Skills load only from a locked directory or a signed source.
- Contract layer. JSON Schema pins the action enum, the path shape, and a ban on extra fields.
- Execution layer. Validation failure returns an error. Do not “do your best to execute.” Dangerous actions take a separate confirmation — not a hope that the model remembers a handbook line.
Structured Output folds the final reply into an object. Tool permissions are the same instinct, applied to inputs. Definition: What is Structured Output.
JSON Schema is the permission contract
The tool contract is called inputSchema in MCP (and, increasingly, outputSchema), and parameters in Function Calling. Wrapper fields differ. The core object you pin is the same. Write the core Schema first, then wrap it for OpenAI / Claude / Gemini / MCP. For vendor wrappers, see the JSON Tool Schema section.
When you use it to limit permissions, prefer these five constraints — not another paragraph of “please be careful”:
enum. Actions can only belint/typecheck/unit_test. Do not leave a free-text command.pattern+maxLength. The path must land in an allowed relative directory. Block..and absolute paths.additionalProperties: false. The model cannot stuff inextra_cmd,note, or other fields outside the contract.required. Missingpathorcheckfails the call. Do not rely on the description as a reminder.- Outputs need a Schema too. The return may only carry declared fields. That shrinks the room for “an instruction inside the result.”
One more product decision — do not leave it only in a Skill: read-only and high side-effect must not share one overly wide tool. If you need to write files, make a separate repo_write and keep it narrower than read. If you need delete, it should not appear on a coding agent's tool list by default.
Permission is shape, not a slogan. Schema checks whether the object may enter the executor. Fail, and you stop. That is the same job as validating Structured Output in the Workbench — only the object changed from “reply” to “tool call.”
A permission Schema you can accept locally
Below is the core object for a read-only repo file. It is not yet a vendor wrapper — only the contract. This is where an overly wide path: string gets narrowed.
{
"tool": "repo_read",
"description": "Read one text file under allowlisted project directories. Use when the user asks to inspect source. Do not call for paths outside src, frontend, or docs.",
"arguments": {
"path": "frontend/zh/blog/index.html"
},
"result": {
"path": "frontend/zh/blog/index.html",
"bytes": 11842,
"truncated": false
}
}
The matching core Schema should pin four things first: path is required; length has a cap; it must match an allowed relative prefix; the object turns additionalProperties off. The return keeps only path / bytes / truncated. Do not leave a field for a free-text “suggested next step.”
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["path"],
"properties": {
"path": {
"type": "string",
"minLength": 1,
"maxLength": 256,
"pattern": "^(src|frontend|docs)/[A-Za-z0-9._/-]+$",
"description": "Relative path inside allowlisted directories. No absolute paths, no parent traversal."
}
}
}
The second contract folds “run a check” into an enum, not a command string. A Skill can say “lint first, then unit tests.” The tool layer never sees an action outside the enum.
{
"tool": "run_allowed_check",
"description": "Run one allowlisted repository check. Use after the user asks to verify the change. Do not invent new commands.",
"arguments": {
"check": "lint"
}
}
{
"type": "object",
"additionalProperties": false,
"required": ["check"],
"properties": {
"check": {
"type": "string",
"enum": ["lint", "typecheck", "unit_test"]
}
}
}
Compare the shapes that should not pass: path written as ../.env; an extra extra_cmd; check written as arbitrary shell. Those objects should fail validation in the Workbench. Growing a Schema from a sample: How to generate a Schema.
Checklist: install, contract, execute
| Layer | What to pin | Don't count on Markdown |
|---|---|---|
| Install | Allowlist MCP / Skill sources; pin versions | “Please only install official packs” |
| Discovery | Short tool descriptions, precise triggers, no extra steps smuggled in | Hoping the model ignores a weird description |
| Contract | enum, pattern, required, additionalProperties | “Don't leave the repo” written in the Skill body |
| Execute | Refuse on validation failure; split write/delete; second confirm for high risk | “Please think again before executing” |
| Config | Read first, then a minimal patch; diff before write-back | Letting the agent rewrite all of mcp.json |
The minimum reasonable setup: the machine only connects MCP you named; the Skill directory does not pull from a stranger URL; every high-privilege tool has a Schema that can parse. After the Responses API put MCP and functions in the same tools array, sharing that one contract matters more — see DevDay 2026 Agent.
How to accept it in JSON Toolbox
- Paste the
arguments/resultabove into JSON Format and confirm they parse. - Use JSON → Schema or Structured Output to generate the core contract. Add
enum,pattern,required, andadditionalProperties. - Run the same fields through Function Calling and MCP Tool separately. Do not copy two type systems.
- Prepare a set of objects that should fail: an out-of-bound path, an extra field, an illegal
check. Confirm Schema validation refuses them; when they don't match the sample, use JSON Diff.
All data is processed in the browser and never uploaded to a server. Have an object that can refuse a bad call first. Then decide whether that tool may appear on the agent's tool list.
FAQ
What is the difference between a malicious MCP server and a normal MCP?
The protocol is the same. The difference is trust: a malicious server trades a harmless name for overly wide inputs, or stuffs the next-hop instruction into the return. Once the Host lists it, the model calls by description. The boundary is the allowlist, the Schema, and a refuse at the execution layer — not “is this MCP.”
Can a fake Skill edit the repo or exfiltrate data by itself?
Usually not. A Skill is a handbook. The risk is that after it wins the route, it steers the model toward high-privilege tools already attached. Pin tool permissions first, then Skill sources.
Why isn't “don't delete production” in a Skill enough?
That is prose. The model may ignore it. A tool return may overwrite it. What to do: do not expose delete, or fold the action into an enum, and refuse on validation failure.
How does JSON Schema limit tool permissions?
enum limits actions, pattern / maxLength limits paths, additionalProperties: false bans extra fields, required pins required keys. Write a Schema for the output too. This is a shape check before execution.
If I already use MCP, do I still need a Function Calling Schema?
Yes. MCP inputSchema and Function Calling parameters are the same class of contract. Maintain one core field set, then wrap it.
Summary
AI agent security became a hotspot because coding agents made the handbook directory and the tool bus default capabilities. A malicious MCP server borrows a tool name and overly wide inputs to run side effects. Fake Skills steal the route, then borrow a real tool. Markdown can write steps. It cannot be a permission system. Write the part you can block as JSON Schema: action enums, path shape, required fields, no extra fields, and the same pinch on the output. For developers, the next step is not installing more servers, and not writing “please be careful” into a longer Skill. It is producing a tool contract that can parse — and can refuse a bad object. Short index, locked sources, hard permissions, handbook only for order.