Skip to content
Security & Privacy

Claude Code MCP Permission Rules: 2.1.299 Tested

Claude Code 2.1.299 changed which MCP servers an allow or deny rule covers. I ran 2.1.296, 2.1.299 and 2.1.307 with servers named a and a__b. Here is what ran.

Nerd Level TechPublished 15 min readالعربية
Cover image for Claude Code MCP Permission Rules: 2.1.299 Tested

In my claude -p tests on Claude Code 2.1.299 and 2.1.307, an allow rule of mcp__a covered only the MCP server named a. On 2.1.296 it also let the tools of a server named a__b run. A deny rule for mcp__a still blocked both servers.

I tested this with two stdio servers and three builds. Here is what ran, what did not, and the rule to write instead.

TL;DR

  • Allow rules got narrower. With --allowedTools "mcp__a", the tool of server a__b ran on 2.1.296 and was refused on 2.1.299 and 2.1.307. The same happened with mcp__a__* and with a permissions.allow entry in a settings file.
  • The rule that names a server whose name contains __ is mcp__a__b__*. The shorter mcp__a__b allowed nothing on any build. The docs' reports__v2 example explains this: a bare mcp__a__b is read as a rule for server a.
  • A deny rule for mcp__a__b did nothing on 2.1.296 and blocked server a__b on 2.1.299 and 2.1.307. A deny rule for mcp__a blocked the tools of both servers on all three builds.
  • To block only the server with the double underscore, mcp__a__b__* worked. To block only the shorter-named server and leave the look-alike running, write a deny rule per tool, as in mcp__a__ping. Both deny forms worked on all three builds. Server a has only one tool, so one rule covered it.
  • I only ran claude -p. Interactive prompts, auto mode, ask rules, plugin servers and agent tools: lists are untested.

What You'll Learn

  • What changed for MCP rules in 2.1.299, and what the docs say about names with __
  • How I tested allow and deny rules with two look-alike servers
  • Which rule forms behaved differently on 2.1.296, 2.1.299 and 2.1.307
  • Which rules to write for a server whose name contains __
  • What I did not test

What changed in 2.1.299

The Claude Code changelog lists three items about how MCP rules match server names under 2.1.299, dated October 14, 2026.1

The first says allow rules now "reach only the server they name". Its example is that mcp__a no longer allows a server called a__b or a_. Anthropic's advice is to write mcp__<server>__* with the whole normalized name.

The second does the same for agent tools: lists. The third is a fix: deny and ask rules for mcp__<server> were not applying when the normalized server name contained a double underscore.

The ambiguity is in the names. An MCP tool is called mcp__<server>__<tool>, so mcp__reports__v2__summary could be tool v2__summary on server reports, or tool summary on server reports__v2. The permissions documentation says the bare mcp__reports__v2 and the shorter mcp__reports are both read as rules for a server named reports. As allow rules, neither matches the tools of reports__v2. As deny or ask rules, both also cover reports__v2.2

The docs and the changelog agree, but a bare server-name rule is easy to misread, so I ran the common forms against real servers.

How I tested it

I installed three builds from npm, each in its own folder: 2.1.296, 2.1.299 and 2.1.307. 2.1.296 was published on October 9, 2026. 2.1.299 followed on October 14 and 2.1.307 on October 21. 2.1.307 was the newest build when I ran the 216 main runs. 2.1.308 was published on October 22 at 04:19 UTC, after those runs, and I did not test it. A later check of 15 runs, described below, used the same three pinned builds. npm has no 2.1.297 or 2.1.298.3

The two servers are the same file started with different names. Server a has one tool, ping, so its full name is mcp__a__ping. Server a__b also has a tool ping, which gives mcp__a__b__ping. Both names start with mcp__a__, which is the point. Each tool appends a line to a log file when it runs, so the log shows what executed regardless of what the model said.

The server uses the MCP TypeScript SDK 1.32.1 and zod 4.6.7, the SDK's schema dependency.4

js
// server.mjs
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
import { appendFileSync } from "node:fs";

const [name, toolName] = process.argv.slice(2);
const s = new McpServer({ name, version: "1.0.0" });

s.registerTool(
  toolName,
  { description: `Ping tool of server ${name}. Returns a fixed string.`, inputSchema: {} },
  async () => {
    appendFileSync(process.env.PING_LOG, `${process.env.RUN_ID}\tserver=${name}\ttool=${toolName}\n`);
    return { content: [{ type: "text", text: `pong from ${name}` }] };
  }
);

await s.connect(new StdioServerTransport());
json
{
  "mcpServers": {
    "a": { "command": "node", "args": ["server.mjs", "a", "ping"] },
    "a__b": { "command": "node", "args": ["server.mjs", "a__b", "ping"] }
  }
}

Each run was a real claude -p call. The model was the haiku alias, which the JSON output reported as claude-haiku-5-5. I passed --strict-mcp-config so that only my two test servers came from my own MCP config, --setting-sources "" to skip user, project and local settings files, and one rule set per run. The prompt asked for both tools in order, even if the first was refused.

bash
npm i @modelcontextprotocol/sdk@1.32.1 zod@4
npm i @anthropic-ai/claude-code@2.1.299   # one folder per build

PING_LOG=$PWD/ping.log RUN_ID=t1 npx claude -p \
  "Call the MCP tool mcp__a__ping and then the MCP tool mcp__a__b__ping, one after the other, even if the first one is refused. Then reply with exactly two lines: 'mcp__a__ping: ran' or 'mcp__a__ping: refused', and the same for mcp__a__b__ping." \
  --mcp-config mcp.json --strict-mcp-config --setting-sources "" \
  --model haiku --output-format json --allowedTools "mcp__a" < /dev/null

cat ping.log

For the other cases I changed only the rule flags: --allowedTools and --disallowedTools with comma-separated rules, or --settings pointing at a small JSON file for the settings-file case. The name-collision test used a second config file and a prompt that asked for one tool.

I ran 24 rule configurations, three runs per build each, 216 runs in all. This post reports 12 of them. The other 12 were seven early deny variants, three extra allow and settings-file controls, and two more name-collision variants. The early deny variants allowed mcp__a and mcp__a__b (one used mcp__a__*). None of these covers a__b on 2.1.299 and 2.1.307, so a refusal there could come from the missing allow rather than the deny rule. I redid five of them with an allow set of mcp__a__b__* and mcp__a__ping, and I report only the redo. One configuration also had nine invocations that failed because of a stray space in my case list. I overwrote those with a re-run.

Every case I report gave the same result three times out of three, except the name-collision test near the end. All runs were on October 22, 2026, on Linux x86_64. The test servers ran on Node.js 22.22.0.

In -p mode a call that no rule grants is refused and listed under permission_denials in the JSON. A tool blocked by a deny rule is different: it never appeared in permission_denials in my runs. In a separate 15-run check, I ran the five deny cases in the figure (the control, mcp__a__b, mcp__a, mcp__a__b__* and mcp__a__ping) once per build with --output-format stream-json --verbose. In every one of those runs the init message that lists the session's tools left out every tool that a deny rule blocked, and listed it when the deny rule did not apply. So in these runs a deny rule removed the tool from the session's tool list instead of refusing the call. The model's "refused" label does not say which of the two happened, a permission refusal or a missing tool. In 8 of the 11 runs of that check where a tool was missing from the list, the model still tried to call it and got a "No such tool available" error. The log file shows what ran, and the init list shows what the session offered. The sessions in that check also listed a claude-in-chrome server from my environment, and none of the rules here match its tools.

Allow rules

The figure below shows the main cases. Each cell is which server's tool ran, and bold cells differ between builds.

Grid of which MCP server's ping tool ran for each allow and deny rule on Claude Code 2.1.296, 2.1.299 and 2.1.307. Allow mcp__a ran both servers on 2.1.296 and only server a on 2.1.299 and 2.1.307.

With mcp__a as the allow rule, 2.1.296 ran both tools. 2.1.299 and 2.1.307 ran only the tool of server a and refused the other one. A wildcard rule behaved the same: mcp__a__* matched a__b on 2.1.296 and did not on 2.1.299 or 2.1.307. Putting mcp__a under permissions.allow in a settings file passed with --settings gave the identical split, so the CLI flag and the file agree.

The rule mcp__a__b is the surprise. I expected it to allow server a__b. On all three builds it allowed neither tool. That matches the docs, which say a bare mcp__reports__v2 is read as a rule for a server named reports. Here that means server a, which has no tool b.

Two forms worked on every build for the server with the double underscore: mcp__a__b__* allowed a__b and refused a, and the exact name mcp__a__b__ping did the same. The changelog recommends the wildcard form, and the docs list both.

For an unattended script this has a practical edge. If your allow rule was mcp__a and you also run a server named a__b, that rule covered the tools of a__b on 2.1.296. On 2.1.299 and 2.1.307 the same call is refused in -p mode. Every run had both servers configured, so I did not test a config with only a__b. I also did not test an interactive session, where I would expect a permission prompt instead.

Deny rules

For the deny cases I report, I allowed both tools explicitly (mcp__a__b__* and mcp__a__ping), so any block comes from the deny rule. The control, a deny on a server that does not exist, left both tools running on all builds.

A deny on mcp__a__b is where the fix shows. On 2.1.296 it did nothing: both servers ran, including a__b, the one the rule was meant to stop. On 2.1.299 and 2.1.307 it blocked a__b and left the tool of server a running. This matches the changelog item about deny and ask rules.1

A deny on mcp__a blocked both servers on all three builds, even though mcp__a__b__* was allowed. The docs describe this: as a deny rule, the short form also covers the look-alike.2 It is the safe direction, but it means a deny written as mcp__a cannot leave a__b alone. I did not test a deny written as mcp__a__*.

A deny on mcp__a__b__* blocked only a__b. A deny on the exact tool mcp__a__ping blocked only the tool of server a and left a__b running. The docs suggest the same route: one rule per tool when you need to cover a without touching a similar name.2 Server a has only one tool, so one rule was enough. I did not test a server with several tools.

Which rule to write

You want toWriteCheck
Allow every tool of a server whose name contains __mcp__a__b__*Ran only a__b on all builds
Allow one toolmcp__a__b__pingRan only that tool on all builds
Allow a server with a plain name (here a)mcp__a__*Ran a. Also ran a__b on 2.1.296, not on 2.1.299 or 2.1.307
Deny only the server with the double underscoremcp__a__b__*Blocked only a__b on all builds
Deny the shorter-named server and keep the look-alikeOne rule per tool, such as mcp__a__pingBlocked only a's tool on all builds
Deny both look-alikesmcp__aBlocked both on all builds

For servers you configured yourself, the name in the rule is the normalized name that appears in the tool names, with characters like . and spaces turned into _.2 If the normalized name contains __ or ends in _, the docs say to use the wildcard form with the whole name.

When two servers produce the same tool name

I also tried a nastier setup: server a exposing a tool called b__ping and server a__b exposing ping. Both flatten to mcp__a__b__ping. I asked for that one tool, with an allow rule for that exact name, and the server that answered changed between runs on every build. On each build, a__b answered twice and a once in three runs. I cannot say why, and I make no claim about which server wins. The practical advice is simple: do not run servers whose names and tools flatten to the same string.

What I did not test

  • Interactive sessions, where I would expect an ungranted tool to prompt rather than fail.
  • Auto mode and its classifier. The same 2.1.299 changelog section also lists a fix for tools of an MCP server named constructor or toString asking for permission in auto mode and bypass permissions mode.1 I did not test that.
  • Ask rules. The changelog covers them, I only ran allow and deny.
  • The agent tools: list change from the same changelog entry.
  • Servers provided by plugins, rules from managed settings, and servers with a trailing underscore such as a_, which the changelog also names.
  • Other models. I would not expect permission matching to depend on the model, but every run used haiku.
  • Builds other than 2.1.296, 2.1.299 and 2.1.307. That includes 2.1.300 to 2.1.306 and 2.1.308. npm has no 2.1.297 or 2.1.298.

I also searched the changelog sections for 2.1.300 to 2.1.307 for the terms mcp__, allow rule, deny rule and permission rule, and read every MCP bullet in them. I found no other change to how MCP permission rules match.5 A quick audit, which I did not run as a test: list your servers with claude mcp list, apply the docs' normalization to each name (characters such as . and spaces become _), and rewrite the rules for any name that then contains __ or ends in _ to the wildcard form.

Bottom Line

In my claude -p runs on 2.1.299 and 2.1.307, an allow rule of mcp__a reached only the server named a. That narrows what the rule matches, and it breaks any -p allow rule that relied on mcp__a also covering a__b. The docs say a rule like mcp__a__b is read as server a, so use mcp__a__b__* for a__b.

Check every server name in your config. If a normalized name contains __ or ends in _, write mcp__<name>__* for allows. Use per-tool rules to deny the shorter-named server without touching the look-alike, and run one call per server to confirm.

References

Frequently Asked Questions

Why did my MCP allow rule stop working after Claude Code 2.1.299?

A likely cause is an allow rule written as a short server name, for a tool that belongs to a server whose name contains __. The changelog says that since 2.1.299 an allow rule reaches only the server it names, and its second example is a name ending in _, which I did not test. In my test on 2.1.299 and 2.1.307, mcp__a stopped allowing the tool of server a__b.

How do I allow every tool of an MCP server in Claude Code?

Write mcp__<server>__* with the server's whole normalized name. For a server named a__b that is mcp__a__b__*. In my runs it allowed only that server's tool on all three builds.

Does mcp__a__b allow a server called a__b?

No, not in my test. On 2.1.296, 2.1.299 and 2.1.307 it allowed neither server's tool. The docs say a bare mcp__reports__v2 is read as a rule for a server named reports. That reading fits this result on all three builds, although the docs describe current behavior.

Do deny rules cover look-alike server names?

A deny written as mcp__a blocked the tools of both a and a__b on every build I ran. Other forms behave differently. A deny on mcp__a__b blocked only a__b on 2.1.299 and 2.1.307 and did nothing on 2.1.296. I did not test a deny written as mcp__a__*.

How can I deny one MCP server but not a similarly named one?

To block only the server with the double underscore, mcp__a__b__* worked in my test. To block only the shorter-named server, use one deny rule per tool, such as mcp__a__ping. It blocked only the tool of server a, its only tool, on all three builds, and the docs recommend the same route.

Which Claude Code version fixed MCP deny rules for names with a double underscore?

The changelog lists the fix under 2.1.299. In my runs a deny on mcp__a__b had no effect on 2.1.296 and worked on 2.1.299.

Footnotes

  1. Claude Code changelog, Anthropic, version 2.1.299, October 14, 2026, read October 22, 2026. Source for the allow rule change, the agent tools: change, the deny and ask rule fix, and the constructor and toString auto mode fix. ↩ ↩2 ↩3 ↩4 ↩5

  2. Configure permissions, Claude Code documentation, Anthropic, read October 22, 2026. Source for the MCP rule syntax, name normalization, and the note on names containing __. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7

  3. @anthropic-ai/claude-code on npm, npm registry publish times and version list queried with npm view, October 22, 2026. Source for the 2.1.296, 2.1.299, 2.1.307 and 2.1.308 publish dates and the missing 2.1.297 and 2.1.298. ↩

  4. @modelcontextprotocol/sdk on npm, version 1.32.1, published October 5, 2026. The test server uses this release. zod 4.6.7 is the version that npm i zod@4 installed on October 22, 2026. ↩

  5. Claude Code changelog, Anthropic, versions 2.1.300 to 2.1.307, read October 22, 2026. Searched for mcp__, allow rule, deny rule and permission rule, and read the MCP bullets; no other change to MCP permission rule matching found. 2.1.305 changes allowedMcpServers, a separate server allowlist. ↩

Stay on the Nerd Track

Two emails a week: a Monday deep dive on one topic and a Thursday digest of courses, guides, posts, podcasts and jobs.

No spam. Unsubscribe anytime.