Room 42, home

MCP security: what to allow and what to block

The permissions, secrets, and scopes to settle before you connect an MCP server, and how to keep every connection least-privilege.

MCPadvanced9 min readUpdated 13 Jun 2026

In short

  1. 01Connecting an MCP server is a privilege grant, since its tools act on a real system with whatever credentials you handed it.
  2. 02Decide if you trust the code, keep secrets in env vars referenced from config, and scope each server to the narrowest place.
  3. 03Allow only the specific tools a task needs, deny destructive ones, and pin OAuth scopes for remote servers.
  4. 04Treat any server that reads outside content as untrusted input, and contain prompt injection by keeping access least-privilege.
Contents · 11 sections
  1. 1The one-line answer
  2. 2Why is connecting an MCP server a privilege grant?
  3. 3Step 1: decide if you trust the server
  4. 4Step 2: keep secrets out of your config
  5. 5Step 3: scope the server to the narrowest place
  6. 6Step 4: allow only the tools you need
  7. 7Step 5: control which servers may load at all
  8. 8Step 6: contain prompt injection
  9. 9A quick pre-connect checklist
  10. 10How do I audit which permissions I already granted?
  11. 11Where this leaves you

Before you connect an MCPModel Context Protocol: an open standard that lets AI apps connect to tools and data through one common interface. server, decide three things: do you trust the code, what secrets does it need, and which of its tools Claude may call. Connecting a server hands Claude a new set of actions on a real system, so treat each connection like granting an account access. This guide shows you the questions to ask and the exact Claude Code settings that enforce your answers.

If you have not met MCP yet, start with MCP fundamentals for the mental model. This guide assumes you know what a server, tool, and transport are, and focuses on keeping access tight.

The one-line answer

Allow only the specific tools a task needs, store secrets outside your config, scope each server to the narrowest place that works, and treat any server that reads outside content as untrusted input. Everything below is how you put that into practice.

Why is connecting an MCP server a privilege grant?

A connected server gives Claude tools that act on a live system: open a pull request, run a query, send a message, delete a record. The tool runs with whatever credentials you handed the server, not with some reduced "read-only AI" identity. If you connect a database server with a write-capable login, Claude can write to that database.

Two risks follow from this:

  • Over-broad access. A server with more permission than the task needs can do more damage from a mistake or a bad instruction.
  • Prompt injection. A server that fetches outside content (web pages, issue text, emails) can pull attacker-controlled text into the conversation, and that text may try to redirect Claude into calling other tools. Claude Code analyses the full request and requires approval for sensitive operations, but the durable defence is to not hand the server power it does not need.

Step 1: decide if you trust the server

Claude Code asks you to verify trust the first time you run in a new codebase and the first time it sees a new server. That prompt is the moment to think, not a box to click through.

  • Prefer servers you wrote or servers from a provider you already trust with the same data.
  • The Anthropic directory lists connectors reviewed against listing criteria, but Anthropic does not security-audit or manage the servers themselves. Reviewed is not the same as audited.
  • A stdio server runs as a local process on your machine with your file access. A remote HTTP server runs on someone else's infrastructure and you send it your data over the network. Different trust questions apply to each.

For a project-scoped server checked into version control, Claude Code holds the server as pending and asks for approval before first use. You can reset those approval choices later:

claude mcp reset-project-choices

Step 2: keep secrets out of your config

A server usually needs a credential: an API key, a database connection string, an access token. The rule is that the credential lives in your environment or a secret store, and the config references it. The config can be shared or committed. The secret cannot.

Claude Code expands environment variables inside .mcp.json, so you reference a value instead of pasting it:

{
  "mcpServers": {
    "billing-api": {
      "type": "http",
      "url": "${BILLING_BASE_URL:-https://api.example.com}/mcp",
      "headers": {
        "Authorization": "Bearer ${BILLING_API_KEY}"
      }
    }
  }
}

The ${VAR} form reads the variable from your environment. The ${VAR:-default} form falls back to a default when the variable is unset. If a referenced variable has no value and no default, Claude Code refuses to parse the config rather than connecting with a blank secret. That fail-closed behaviour is the point.

When you add a server from the command line, pass the secret through --env for a stdio server or --header for a remote one, so it never lands in a tracked file:

# Local stdio server, key supplied at add time
claude mcp add --env API_KEY=$MY_KEY --transport stdio reports \
  -- npx -y example-reports-mcp
 
# Remote HTTP server, token supplied as a header
claude mcp add --transport http github https://api.githubcopilot.com/mcp/ \
  --header "Authorization: Bearer $GITHUB_PAT"

For OAuth servers, you do not handle a long-lived secret at all. Run /mcp inside Claude Code to complete the browser sign-in. Claude Code stores and refreshes the token, keeping it in the system keychain on macOS and behind file permissions on Windows and Linux.

How do I stop Claude from reading my secret files?

Environment references keep secrets out of the MCP config, but Claude can still read files. Add a deny rule so it never opens your env files or secret directories, whatever a server or instruction asks:

{
  "permissions": {
    "deny": [
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(./secrets/**)"
    ]
  }
}

Deny rules win over allow rules across every settings scope, so a deny entry cannot be undone by a broader allow elsewhere.Common gotchaA Read(./.env) deny only stops Claude's own file reads. A stdio server runs as a subprocess that inherits your whole shell environment, so it can still see every variable you exported, including secrets meant for other tools.Launch each server with only the variables it needs (pass them via --env) rather than relying on the deny rule to hide them.

Step 3: scope the server to the narrowest place

Scope controls which projects load the server and whether the config is shared. Pick the smallest scope that still works. For the mechanics of each scope and where its config lives, see MCP configuration.

ScopeLoads inShared with teamStored in
LocalCurrent project onlyNoYour home config
ProjectCurrent project onlyYes, via version control.mcp.json in the project root
UserAll your projectsNoYour home config
  • Local is the default and the tightest. Use it for a personal or experimental server, or one whose credentials you do not want shared.
  • Project shares the server with the team through .mcp.json. Use it for tools everyone on the repo needs. Remember the secret still comes from each person's own environment, not the committed file.
  • User makes a server load in every project on your machine. A user-scoped server with write access is reachable from work you have not reviewed yet, so reserve this scope for cross-project, low-risk tools.

Set the scope explicitly when you add a server:

# Default local scope, stays in this project, private to you
claude mcp add --transport http stripe https://mcp.stripe.com
 
# Project scope, written to .mcp.json for the team
claude mcp add --transport http paypal --scope project https://mcp.paypal.com/mcp
 
# User scope, every project on this machine
claude mcp add --transport http hubspot --scope user https://mcp.hubspot.com/anthropic

Step 4: allow only the tools you need

This is the heart of least-privilege. A server can expose a dozen tools, and you rarely need all of them. Claude Code permission rules name an MCP tool as mcp__server__tool, so you can allow read tools and leave write tools to prompt, or deny destructive ones outright.

{
  "permissions": {
    "allow": [
      "mcp__github__search_repositories",
      "mcp__github__get_pull_request"
    ],
    "deny": [
      "mcp__github__delete_repository"
    ]
  }
}
  • Anything in allow runs without a prompt.
  • Anything in deny is blocked, and deny beats allow on any conflict.
  • Anything matched by neither falls through to a prompt, so Claude asks before it acts. That prompt-by-default is a feature, not friction: it keeps a tool you forgot to list from running silently.

You can allow every tool from one server with a wildcard, which is convenient for a server you fully trust and risky for one you do not:

{
  "permissions": {
    "allow": ["mcp__notes__*"],
    "deny": ["mcp__notes__delete_note"]
  }
}

The wildcard allows the whole notes server, and the explicit deny still carves out the one tool you never want auto-run. Start narrow and widen only when a real task needs more.Common gotchaThe mcp__server__* wildcard auto-approves tools the server adds later, not just the ones present when you wrote the rule, so a future server update can ship a delete or transfer tool that runs with no prompt.The explicit deny only covers names you already knew to block. Name each tool you use in allow, and reserve the wildcard for a server whose author you trust to add nothing destructive.

How do I limit OAuth scopes for a remote server?

For a remote OAuth server, you can also restrict access at the grant itself, not just at the tool list. Set oauth.scopes to pin the permissions Claude Code requests during sign-in to a subset your security team approves, even when the upstream server advertises more:

{
  "mcpServers": {
    "slack": {
      "type": "http",
      "url": "https://mcp.slack.com/mcp",
      "oauth": {
        "scopes": "channels:read chat:write search:read"
      }
    }
  }
}

This value is a single space-separated string of scope names, and it takes precedence over whatever the server discovers on its own. Pinning here means the token Claude Code holds cannot do more than the listed scopes, which is a stronger guarantee than a tool allowlist alone.

Step 5: control which servers may load at all

Two settings decide whether project servers from .mcp.json connect without per-server approval. Use them deliberately.

  • enableAllProjectMcpServers: true approves every server in the project file automatically. Convenient on a repo you fully trust, dangerous on one you do not, since a new server added by a teammate would load with no prompt.
  • enabledMcpjsonServers is an explicit allowlist of project servers to approve by name. This is the safer default for a shared repo, because a server not on the list still needs your approval.
{
  "enabledMcpjsonServers": ["github", "sentry"],
  "disabledMcpjsonServers": ["filesystem"]
}

If you run an organisation, managed configuration goes further. An administrator can set allowedMcpServers and deniedMcpServers, and with allowManagedMcpServersOnly only the admin-approved set connects while user additions are blocked. Managed settings sit at the top of the precedence order and cannot be overridden locally.

Step 6: contain prompt injection

Any server that reads outside content can carry instructions written by someone other than you. The defence is layered, and you control the layers.

  • Least-privilege first. A server that can only read cannot be tricked into writing. Steps 1 through 5 are your strongest protection here.
  • Network commands prompt by default. Claude Code does not auto-approve curl or wget. To block them outright, add them to deny.
  • Sandbox risky work. Run scripts and tool calls that touch external services inside a virtual machine or a dev container, so a bad outcome stays contained.
{
  "permissions": {
    "deny": [
      "Bash(curl *)",
      "Bash(wget *)"
    ]
  }
}

Claude Code also fetches web content in a separate context window to reduce the chance that a fetched page injects instructions, and it requires approval for network requests by default. These help, but they do not replace giving each server the minimum it needs.

A quick pre-connect checklist

Run through this before you type claude mcp add:

QuestionTight answer
Do you trust the code or provider?Yes, or you do not connect it
Where does the secret live?In your environment, referenced by ${VAR}
What scope does it need?The narrowest one that works, usually local
Which tools may auto-run?Only the ones a real task uses
What must never auto-run?Destructive tools, listed in deny
Does it read outside content?If so, keep it read-only and sandbox the work

How do I audit which permissions I already granted?

Settings drift as projects grow. Review them on a schedule.

# See every connected server and its scope
claude mcp list
 
# Inspect one server's transport, scope, and auth
claude mcp get github

Inside a session, run /permissions to review the active allow, deny, and ask rules, and /mcp to see connected servers, their tool counts, and their auth state. When a server has tools you never use, remove it or tighten its allowlist. Least-privilege is not a one-time setup; it is something you keep trimming.

# Remove a server you no longer need
claude mcp remove old-reports

Where this leaves you

You now have the moves that keep MCP access tight: verify trust, reference secrets instead of pasting them, scope to the narrowest place, allow specific tools, deny destructive ones, pin OAuth scopes, and sandbox anything that reads the outside world. The mechanism behind each setting lives in MCP fundamentals; this guide is the discipline you apply on top of it.

Try this prompt
Review the MCP setup in my project. List every connected server, the tools each one exposes, and which tools my settings currently allow to run without a prompt. Flag any server that has write or delete tools auto-allowed, any secret pasted directly into a config file instead of referenced from an environment variable, and any server scoped wider than it needs to be. Then propose a least-privilege settings.json with allow and deny rules.