---
title: 'Claude Code MCP Permission Rules: 2.1.299 Tested'
description: 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.
url: https://nerdleveltech.com/claude-code-mcp-allow-deny-rules-server-names-tested
language: en
date: '2026-10-22'
category: security
tags:
  - claude code
  - MCP
  - claude code permissions
  - allowedTools
  - ai agent safety
  - agentic coding
  - developer tools
markdown_url: https://nerdleveltech.com/claude-code-mcp-allow-deny-rules-server-names-tested.md
attribution: 'Source: NerdLevelTech — https://nerdleveltech.com/claude-code-mcp-allow-deny-rules-server-names-tested'
terms: https://nerdleveltech.com/llms.txt
---

# 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](https://code.claude.com/docs/en/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](https://code.claude.com/docs/en/permissions) 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](https://www.npmjs.com/package/@anthropic-ai/claude-code), 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](https://www.npmjs.com/package/@modelcontextprotocol/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.](https://nerdleveltech.com/images/claude-code-mcp-permission-rules-server-names.png)

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 to | Write | Check |
| --- | --- | --- |
| Allow every tool of a server whose name contains `__` | `mcp__a__b__*` | Ran only `a__b` on all builds |
| Allow one tool | `mcp__a__b__ping` | Ran 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 underscore | `mcp__a__b__*` | Blocked only `a__b` on all builds |
| Deny the shorter-named server and keep the look-alike | One rule per tool, such as `mcp__a__ping` | Blocked only `a`'s tool on all builds |
| Deny both look-alikes | `mcp__a` | Blocked 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.

## Related reads

- [Claude Code Hook Failures: onFailure Block Tested](https://nerdleveltech.com/claude-code-hook-fail-closed-onfailure-tested)
- [Claude Code updatedInput Hooks: 2.1.300 Change Tested](https://nerdleveltech.com/claude-code-pretooluse-updatedinput-validation-tested)
- [Claude Code Mods: 10 Tested Use Cases (2026)](https://nerdleveltech.com/claude-code-mods-use-cases-examples)
- [MCP Token Audience Check: expectedResource Tested (2026)](https://nerdleveltech.com/mcp-expected-resource-token-audience-tested)

## FAQ

### 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.[^1] 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.[^2] 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.[^2]

### 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.[^2]

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

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

## 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

[^1]: [Claude Code changelog](https://code.claude.com/docs/en/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]: [Configure permissions](https://code.claude.com/docs/en/permissions), Claude Code documentation, Anthropic, read October 22, 2026. Source for the MCP rule syntax, name normalization, and the note on names containing `__`.
[^3]: [@anthropic-ai/claude-code on npm](https://www.npmjs.com/package/@anthropic-ai/claude-code), 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](https://www.npmjs.com/package/@modelcontextprotocol/sdk), 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](https://code.claude.com/docs/en/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.
