●DESIGN — Claude Design gets a major update: design-system imports, direct canvas editing, and more export formats●CODE — Claude Design can start from your local codebase and hand a design off to Claude Code to implement●FABLE — Fable 5, a Mythos-class model made safe for general use, is now available in Claude Code v2.1.170●FIX — Mid-stream connection drops now preserve partial responses instead of showing a raw error●SCROLL — A new wheelScrollAccelerationEnabled setting disables mouse-wheel scroll acceleration in fullscreen●TIER — The Claude Design beta is available to Pro, Max, Team, and Enterprise customers●DESIGN — Claude Design gets a major update: design-system imports, direct canvas editing, and more export formats●CODE — Claude Design can start from your local codebase and hand a design off to Claude Code to implement●FABLE — Fable 5, a Mythos-class model made safe for general use, is now available in Claude Code v2.1.170●FIX — Mid-stream connection drops now preserve partial responses instead of showing a raw error●SCROLL — A new wheelScrollAccelerationEnabled setting disables mouse-wheel scroll acceleration in fullscreen●TIER — The Claude Design beta is available to Pro, Max, Team, and Enterprise customers
Give an Unattended Agent Only the MCP Tools It Needs — Enforcing a Deny-by-Default Policy
An unattended Claude Code agent can't lean on a permission prompt, so whatever a tool can reach becomes the blast radius. Here's how to lock MCP servers and tools down to deny-by-default and hand back only what the job needs, with managed-settings.json examples.
The agents I launch on a schedule have no one standing by to click the permission prompt.
I run Dolice Labs as an indie developer, and across several sites I kick off Claude Code-based agents at fixed times every day. Early on, one of those unattended agents reached for tools it had no business touching, then sat waiting on a permission dialog that no one was there to answer — frozen until morning.
In an interactive session, a risky tool call surfaces on screen and I can stop it. In unattended operation, that last line of defense simply isn't there. So before asking "what should make it stop," the better question is "what can it reach at all" — and that belongs in configuration. This piece is about denying MCP servers and their tools by default, then handing back only what the job actually needs.
For an unattended agent, reach is the blast radius
The same permission settings mean very different things in interactive versus unattended use.
Interactively, the ask permission mode is effectively a safety net. Tool calls it isn't sure about get routed to you, so even a loose allowlist is survivable because a human makes the final call.
Unattended, there is no one to ask. A tool that isn't allowed stalls; one that is allowed simply runs. There's no middle. So for an unattended agent, the breadth of the allowlist is exactly the breadth of what can happen without anyone watching.
For me, MCP-backed tools were where this bit hardest. An agent whose only job was to generate an article and push it could still see a filesystem-wide MCP server and a browser-automation MCP. For that job, the reach was obviously far too wide.
Per-tool allow and "policy enforcement" live on different layers
The easy thing to conflate here is project-level permissions versus policy enforced at a higher layer.
The permissions.allow / permissions.deny keys in settings.json are project or user settings. Convenient — but because they live at a layer you can also write to, another setting can override them. For automation, it's worth asking: what happens if this agent's settings file gets rewritten somehow?
Managed (administrator-layer) settings exist precisely to set a floor that lower local settings cannot override. Claude Code reads settings across several layers and resolves them roughly in this order of precedence:
Priority
Layer
Typical location
Overridable from below
High
Managed policy
OS system location
No
Mid
Command-line flags
-- options at launch
—
Mid
Project settings
.claude/settings.json
Yes
Low
User settings
~/.claude/settings.json
Yes
The key move is to put an unattended agent's "floor" at that top layer. However the local settings get touched, the deny survives.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Build a configuration that denies MCP servers and tools by default and hands back only the necessary ones via an allowlist, with concrete managed-settings.json examples
✦Understand how per-tool allow lists differ from policy enforced at a higher layer, so you can set a floor that local settings cannot override
✦Learn how to confirm at startup that the policy is actually in effect, plus the precedence and fallback traps I hit running my own unattended pipelines
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Start by flipping the stance to "anything not allowed is denied." Concretely, broadly deny the higher-risk categories in managed policy, then add back only what's truly needed in the next step.
On macOS, the managed policy lives here (paths vary by environment):
Two things matter here. First, setting defaultMode to deny means anything you haven't explicitly allowed generally won't pass. Interactively, ask is reasonable; unattended, with no one to ask, deny is the safe starting point. Second, an MCP server can be denied wholesale as a single unit: mcp__<server>. "This agent never sees the filesystem MCP" becomes one line.
At this point you have an agent that can do almost nothing. That's fine. Next, hand back only what the job requires.
Narrow MCP at two levels: per-server and per-tool
It helps to think of the allowlist as two stages: enabling MCP servers, and allowing individual tools.
First, don't auto-launch every server bundled in the project's .mcp.json. In unattended operation, just being explicit here cuts down on "a server I didn't know about quietly appeared" incidents.
Setting enableAllProjectMcpServers to false keeps the project's .mcp.json servers off by default, launching only those listed in enabledMcpjsonServers. If the job is generating and pushing an article, you can narrow it to a single repository-operations server at most.
Then, within a launched server, allow tools by name. MCP tools can be addressed individually as mcp__<server>__<tool>:
Now, on that same GitHub MCP server, "read a file" and "create a file" pass, while any other destructive tool — should one exist — won't, unless it's on the allowlist. Close down per server, then open per tool. That two-stage stance is what pays off unattended.
Before / After: from leaning on prompts to closing things down in config
My first unattended launch looked like this:
# Before — grants broad permission on the spotclaude -p "Generate an article and push it" \ --dangerously-skip-permissions
--dangerously-skip-permissions is handy because it skips interactive confirmation — but using it unattended skips the entire step of reasoning about reach in configuration. That's exactly what let an agent wander into unrelated tools and stall.
After switching to deny-by-default, the launch side actually got plainer, because closing things down is now the config's job:
# After — assumes the managed policy already closed things down; state only what's neededclaude -p "Generate an article and push it" \ --allowed-tools "mcp__github__create_or_update_file,Bash(git push:*),Read,Write"
The --allowed-tools on the launch line is only a declaration of "what I'd like to use this session." It can't open anything beyond the managed policy's deny. So even if you write something too broad at launch, the administrator layer still holds the floor. Whether that un-overridable floor exists was the essential difference between Before and After.
Confirm the policy is in effect at startup
I learned the hard way not to relax just because I wrote the settings. The scariest case is a policy you believe you set that, due to precedence, isn't actually in effect.
Before putting it on an unattended schedule, launch it interactively once and check:
# 1. Inspect which settings load, and from which layerclaude --debug 2>&1 | grep -i "settings\|permission\|mcp"# 2. Confirm which MCP servers actually launchedclaude mcp list# 3. Environment health check (including isolation of broken config)/doctor
If claude mcp list shows only the servers you named in enabledMcpjsonServers, your per-server narrowing is working. If a server you meant to deny is still in the list, suspect precedence or the file's location. /doctor inspects configuration health and shows whether broken config has been isolated.
If you want to automate the check, you can put a front gate at the very start of the unattended run: pull the MCP list, and if it differs from what's expected, exit before doing any real work. In my own setup, if an unexpected server shows up, that run is skipped and only a log is left behind.
Traps worth knowing: precedence and fallback
In practice, what tripped me up wasn't settings that "didn't work" — it was settings losing to a different layer.
The most common one: an allow written in local project settings is blocked by the managed policy's deny and never passes. That's correct, by design. Deny wins at the higher layer, so adding an allow locally won't open it; if you want it open, you have to edit the managed policy itself. Unattended, that "can't open it locally" property is the safety device.
A second one: mistaking an MCP server's launch failure for a denial. If a server won't start because its command isn't found or an environment variable is missing, the outcome — the tool is unusable — looks just like a deny. Reading the status in claude mcp list separately from the startup log lets you tell "stopped by policy" apart from "failed to launch."
Finally, fallback design. When a tool is blocked by deny, does the agent route around it — say, trying to do the same thing via Bash? That's why deny should cover not just specific MCP tools but also the commands that tend to become detours, like Bash(rm:*). Holes open right next to the one you thought you'd plugged.
A small first step
You don't need to put every task on deny-by-default at once.
Pick the single agent you run unattended for the longest stretch, and run claude mcp list in that session. Just looking at whether it can see more servers than the job warrants will tell you where to tighten. The one thing you can do today: read that list and add to deny the one server you can immediately say this agent doesn't need.
The more something runs unattended, the more worth it is to narrow what can happen ahead of time. Since I took this on in configuration, I find far fewer agents stalled in the morning. If you're running overnight batches too, I hope this gives you a foothold for your own operational design.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.