What you will be able to do
- Tell a 401 from a 403 insufficient_scope challenge and re-authorize without dropping permissions the client already has
- Find credential-handling gaps in per-user vaults, such as silent unauthenticated fallback and failed refreshes
- Choose allowlist and denylist settings that actually limit which MCP servers and tools an agent can reach
1.Scopes: 401 versus 403 and step-up authorization
Authentication asks who the caller is. Authorization asks what that caller may do. MCP keeps the two failures apart with different status codes, and treating them as the same thing is a common design gap. A 401 means the client needs authorization or its token is invalid. A 403 means the token is valid but lacks the needed scopes or permissions.
| Status | Meaning | Client response |
|---|---|---|
| 401 Unauthorized | Authorization required or token invalid | Discover the authorization server via resource_metadata; request the scope from the WWW-Authenticate header, or scopes_supported if absent |
| 403 Forbidden | Invalid scopes or insufficient permissions | Read error="insufficient_scope" and the scope parameter, then re-authorize |
| 400 Bad Request | Malformed authorization request | Fix the request |
Asking for scopes only when they are needed is how least privilege plays out at runtime. On the first request, the client asks for the scope named in the 401 challenge. Only if that is missing does it fall back to everything in scopes_supported. Later, when an operation needs more, the server returns 403 with error="insufficient_scope", the required scope, and the resource_metadata URL.
The client then has to re-authorize carefully. The 2026-07-28 spec makes this the client's job: it requests the union of the scopes it asked for before and the scopes in the new challenge. The earlier 2025-11-25 spec put more weight on the server's challenge, advising servers to include any existing granted scopes that are still required alongside the newly required ones. Either way, a token requested with only the new scope could lose permissions the client already had, and the next operation would fail.
The retry limit matters too. The client retries the original request with the new authorization no more than a few times, then treats the failure as permanent, so it does not loop forever on a scope it will never get.
Checkpoint 1 of 8· Put it in order
Put the client's response to a 403 insufficient_scope challenge in order
- 1.Determine required scopes as the union of previously requested and newly challenged scopes
- 2.Initiate re-authorization with that scope set
- 3.Parse error information from the WWW-Authenticate header
- 4.Retry the original request no more than a few times
The spec gives the sequence: parse, compute the scope union, re-authorize, then retry a limited number of times before treating it as a permanent failure.
“Determine required scopes by computing the union of the client’s previously requested scope set and the scopes from the current challenge.”Source: modelcontextprotocol.io
Checkpoint 2 of 8· Exam question
A security architect is designing an Agent SDK deployment that will process untrusted third-party documents and must prevent credential exfiltration even if a prompt injection convinces the agent to try to send data to an attacker-controlled endpoint. Which combination of controls together address this threat, given that TLS-encrypted traffic through a plain forward proxy cannot be inspected or have credentials injected into it? (Select 3)(Select 3)
Correct answers: A, B, C — Run the agent in a container with --network none and route all outbound calls through a Unix socket to a proxy on the host, so the agent has no direct network interface at all; Keep credentials outside the agent's security boundary entirely, injecting them at a proxy so the agent only ever sends requests without the underlying secret; Deploy a TLS-terminating proxy with its CA certificate trusted in the agent's environment so encrypted HTTPS traffic can be inspected and have credentials injected before re-encryption
- A. Correct. Removing the network namespace and forcing all traffic through a Unix socket to a host-side proxy means the agent has no path to reach arbitrary destinations directly, which is the architecture recommended for defense against prompt-injection-driven exfiltration.
- B. Correct. The proxy pattern keeps the actual credential outside the agent's trust boundary; the agent can make calls but never sees the secret, so even a fully compromised agent process can't leak the credential itself.
- C. Correct. A plain forward proxy only sees an opaque CONNECT tunnel for HTTPS and cannot modify or inspect its contents; injecting credentials into arbitrary HTTPS services requires terminating TLS at the proxy and having the agent trust its certificate.
- D. Not every tool respects HTTP_PROXY/HTTPS_PROXY, and even when they do, a standard forward proxy cannot see or modify HTTPS contents without terminating TLS, so it cannot inject credentials into an encrypted tunnel on its own.
- E. Granting broad IAM permissions directly to the agent process is the opposite of least privilege and increases the blast radius of a prompt injection rather than mitigating it.
- F. Relying on model judgment alone is not a security boundary; defense in depth requires network-level enforcement that holds even when the model's decision-making is manipulated.
2.Per-user credentials: who the agent acts as
An agent that serves many users needs to act as each of them, not as one shared identity. Claude Managed Agents handles this with vaults. A vault holds one end user's credentials and is attached to a session. Credentials come in two kinds. MCP credentials (mcp_oauth or static_bearer) are keyed by mcp_server_url and injected automatically when the agent connects to that URL. Environment-variable credentials are kept in the sandbox as placeholders and swapped for the real secret only when a request leaves the sandbox, so the agent never sees the secret. Secret values are write-only and never come back in API responses. If you supply a refresh block, Anthropic refreshes the OAuth access token for you.
Checkpoint 3 of 8· Fill the gap
Which parameter attaches an end user's vault to a session?
session = client.beta.sessions.create(
agent=agent.id,
environment_id=environment.id,
? =[vault.id],
title="Alice's Slack digest",
)Vaults are referenced per session through vault_ids, so each session can carry a different end user's credentials.
Source: platform.claude.comSeveral runtime behaviours are easy to miss in a review:
- Silent unauthenticated fallback. If no credential matches the server URL, the agent connects without credentials. That fails only if the server requires authentication.
- No validation at creation. Credentials are stored as provided and not checked until the session runs. A bad token shows up later as a runtime error.
- Precedence. When more than one vault has a matching credential, the first vault with a match wins.
- Multiagent scope. Vault credentials apply to every thread in the session, so any agent that declares the matching MCP server authenticates with them.
- Refresh failure. A failed refresh emits vault_credential.refresh_failed. Validation then returns invalid, meaning the grant is gone and the end user must re-authorize, or unknown, meaning a transient error (5xx, 429 or network failure) where you wait and retry.
For audit trails, archive instead of deleting. Archiving purges the secrets but keeps the records, while a hard delete keeps nothing.
Checkpoint 4 of 8· Check yourself
A multiagent session gets a vault holding a user's Slack MCP credential. One subagent's definition declares the Slack MCP server. What happens?
Vault credentials are not limited to one thread, so any agent that declares the matching server uses them. Check this when you work out what each subagent can reach.
“In multiagent sessions, vault credentials apply to every thread.”Source: platform.claude.com
Checkpoint 5 of 8· Check yourself
A compliance team needs to keep a record of which credentials existed for a departed user, without keeping the secrets. What should you do with the vault?
Archiving a vault cascades to its credentials, purging secrets while retaining records for auditing. A hard delete retains nothing.
“Delete a vault or credential: Hard delete. The record is not retained. Use archive if you need an audit trail.”Source: platform.claude.com
Sources3
3.Organisation policy: which servers and tools an agent can reach
The last layer controls which MCP servers a client may connect to at all. In Claude Code, administrators can provide remote servers through managedMcpServers in managed settings. These entries must use an https:// URL; Claude Code refuses plain http://, even for localhost. They also take only literal values, with no ${VAR} expansion. That limit has a consequence for identity: any header you write here is the same for every user. The example below does exactly that with the records server.
{
"managedMcpServers": {
"search": {
"type": "http",
"url": "https://search.example.com/mcp"
},
"records": {
"type": "http",
"url": "https://records.example.com/mcp",
"headers": {
"X-Records-Key": "key-issued-for-all-claude-code-users"
}
}
}
}The gap is that the records server cannot tell users apart, because every user sends the same key. When you need per-user authorization, look at what managed-mcp.json offers instead: ${VAR} expansion, OAuth or per-user headers, or headersHelper.
Allowlists and denylists decide which servers load. Read them in this order:
- Denylist first. A server that matches any deniedMcpServers entry, by URL, command or name, is blocked, and nothing overrides a match. Prefer serverUrl entries, because they keep working if a server is renamed.
- Then the allowlist. If allowedMcpServers is not set anywhere, every server that passed the denylist loads. If it is set, the server must match an entry.
- Unset versus empty. An unset allowlist allows everything; an empty array allows none, apart from servers that skip the check. An empty denylist blocks nothing.
- Soft allowlist. allowedMcpServers without allowManagedMcpServersOnly: true can be broadened by users in their own settings.
- Servers that skip the allowlist. Every managedMcpServers entry, built-in servers, and managed-mcp.json entries that use no ${VAR} expansion. A managed-mcp.json server that uses ${VAR} is still checked.
For example, if an admin sets an allowlist containing one serverUrl entry, a server a user adds that matches no entry is blocked, while the search and records servers above still load because provided servers need no allowlist entry. If the admin also denies the records URL, that server is blocked anyway.
Permission modes sit alongside these lists. In bypassPermissions mode, permission prompts are skipped except for the actions no mode auto-approves, so a mode setting is not the control that limits which servers load. Use the allowlist and denylist for that.
| Setting | Unset (default) | Empty array [] |
|---|---|---|
| allowedMcpServers | All servers allowed | No servers allowed, apart from those that skip the allowlist check |
| deniedMcpServers | No servers blocked | No servers blocked |
At the tool level, permission rules in a permissions object can match MCP tools by pattern, and which list a pattern sits in decides whether it blocks or pre-approves them.
Checkpoint 6 of 8· Fill the gap
Which pattern in the deny list blocks every MCP tool?
{
"permissions": {
"deny": [
" ? "
]
}
}The mcp__* pattern under deny matches every MCP tool. The other options are not the MCP tool prefix, or match only Bash commands.
Source: code.claude.comCheckpoint 7 of 8· Check yourself
An admin sets allowedMcpServers in managed settings but leaves allowManagedMcpServersOnly unset. What is the gap?
Without allowManagedMcpServersOnly, allowlists from every scope merge, so users can add servers. The denylist still cannot be overridden.
“When allowManagedMcpServersOnly is true, only the managed allowlist is kept; the denylist always merges from every scope.”Source: code.claude.com
Checkpoint 8 of 8· Exam question
A CISO reviewing an Agent SDK rollout learns that permission_mode is set to bypassPermissions in a batch-processing pipeline and asks which authorization controls, if any, still constrain the agent under that mode. Based on how bypassPermissions is evaluated, which of the following statements are accurate? (Select 2)(Select 2)
Correct answers: A, B — Hooks still execute and a hook that denies a call is still enforced, because hooks are evaluated before the permission mode step in the flow; An explicit ask rule configured in settings.json still forces the call through the canUseTool callback, even though the mode would otherwise auto-approve it
- A. Correct. Hooks run first in the evaluation order, ahead of deny rules, ask rules, and the permission mode step, so a hook denial still blocks the call even under bypassPermissions.
- B. Correct. Explicit ask rules are checked before the permission mode step, so a matching ask rule still routes the call to canUseTool for confirmation even when bypassPermissions would otherwise auto-approve it.
- C. allowed_tools only pre-approves the tools it lists; unlisted tools still fall through to the permission mode, where bypassPermissions approves them too. Listing a narrow set in allowed_tools does not scope or limit what bypassPermissions itself approves.
- D. disallowed_tools entries are deny rules, and deny rules are evaluated before the permission mode step and still block matching calls even in bypassPermissions mode; they are not overridden by it.
- E. plan mode explicitly routes file-edit and shell-write tools to canUseTool regardless of allow rules, while bypassPermissions auto-approves those same operations at the mode step; the two modes produce opposite outcomes for edits, not identical ones.
Exam traps
Each one states something that sounds right. Open it to see what is actually true.
1.After a 403 insufficient_scope, the client should request only the scope named in the challenge.Why is that wrong?
Requesting only the new scope can drop permissions the client already had. The client requests the union of its earlier scopes and the challenged ones.
2.Setting allowedMcpServers to an empty array removes the restriction, just as an empty deniedMcpServers blocks nothing.Why is that wrong?
The two lists treat an empty array differently. An empty allowlist allows no servers except those that skip the check, while an empty denylist blocks nothing.
Covered in Organisation policy: which servers and tools an agent can reach
3.If a session has no credential for an MCP server, the connection is blocked before it is attempted.Why is that wrong?
The connection is attempted without credentials and fails only if the server requires authentication.
Covered in Per-user credentials: who the agent acts as
Sources
Every claim above is drawn from one of these pages, quoted as it was written on the date shown.
- 1.
“403 | Forbidden | Invalid scopes or insufficient permissions”
↩︎ Scopes: 401 versus 403 and step-up authorization“Use scope parameter from the initial WWW-Authenticate header in the 401 response, if provided”
↩︎ Scopes: 401 versus 403 and step-up authorization“Retry the original request with the new authorization no more than a few times and treat this as a permanent authorization failure”
↩︎ Scopes: 401 versus 403 and step-up authorization“Determine required scopes by computing the union of the client’s previously requested scope set and the scopes from the current challenge.”
↩︎ Exam trap 1“Determine required scopes by computing the union of the client’s previously requested scope set and the scopes from the current challenge.”
↩︎ Checkpoint - 2.
“Include any existing granted scopes as well, if they are required, to prevent clients from losing previously granted permissions.”
↩︎ Scopes: 401 versus 403 and step-up authorization - 3.
“MCP credentials (mcp_oauth, static_bearer): each credential is keyed by an mcp_server_url.”
↩︎ Per-user credentials: who the agent acts as“Credentials are stored as provided and are not validated until session runtime.”
↩︎ Per-user credentials: who the agent acts as“invalid: the grant is gone or the OAuth server rejected the refresh with a 4xx. Prompt the end user to re-authorize.”
↩︎ Per-user credentials: who the agent acts as“unknown: a transient error (5xx, 429, or network failure). Wait and retry.”
↩︎ Per-user credentials: who the agent acts as“Delete a vault or credential: Hard delete. The record is not retained. Use archive if you need an audit trail.”
↩︎ Per-user credentials: who the agent acts as“When no MCP credential matches by mcp_server_url, the connection is attempted unauthenticated and will error if the server requires authentication.”
↩︎ Exam trap 3“In multiagent sessions, vault credentials apply to every thread.”
↩︎ Checkpoint - 4.https://code.claude.com/docs/en/managed-mcpOfficial docs
“OAuth or per-user headers so each user authenticates as themselves.”
↩︎ Organisation policy: which servers and tools an agent can reach“Check the denylist. A server that matches any denylist entry, by URL, command, or name, is blocked. Nothing overrides a denylist match.”
↩︎ Organisation policy: which servers and tools an agent can reach“Soft allowlist | Enforce an allowlist that users can broaden in their own settings”
↩︎ Organisation policy: which servers and tools an agent can reach“Provided servers need no allowedMcpServers entry.”
↩︎ Organisation policy: which servers and tools an agent can reach“allowedMcpServers | All servers allowed | No servers allowed, apart from those that skip the allowlist check”
↩︎ Exam trap 2“When allowManagedMcpServersOnly is true, only the managed allowlist is kept; the denylist always merges from every scope.”
↩︎ Checkpoint - 5.https://code.claude.com/docs/en/permissionsOfficial docs
“bypassPermissions | Skips permission prompts, except for the actions no mode auto-approves”
↩︎ Organisation policy: which servers and tools an agent can reach