Skip to main content

The 'Always Allow' Trap: Auditing the Permissions You've Granted Your Coding Agent

· 14 min read
Pere Pages
Software Engineer
A long row of open gates guarded by a single distracted gatekeeper waving everything through

Every "always allow" click quietly trades a little control for a little convenience — and after a few months of daily agent use, those clicks add up to a security posture nobody designed. Here's how to audit what your coding agent can already do without asking, which grants deserve scrutiny, and how to put deliberate guardrails back.

note

This post describes Claude Code's permission system as of mid-2026. The mechanics move fast — rule syntax, modes, and file locations may have evolved, so verify the details against the official permissions documentation before copying configuration.

The prompt that stops meaning anything

The mental model is simple: every permission prompt is a decision point, and every "yes, don't ask again" deletes that decision point forever. The first week with an artificial intelligence (AI) coding agent, you read every prompt. By week three, the prompts have taught you that saying yes is almost always fine, and you start approving on reflex. By month three, you've granted dozens of standing permissions you couldn't list if asked — and the prompts you do still see get rubber-stamped in the half-second it takes to hit enter.

This is approval fatigue, and it has a nasty property: it inverts the value of the prompt. A permission prompt is only useful if there's a realistic chance you'll say no. Once you're approving reflexively, the prompt no longer protects you — it just trains you to click faster, while the list of things that never prompt again grows silently in a config file you haven't opened since you installed the tool.

The fix is not to approve less — it's to decide deliberately which things should never prompt, which should always prompt, and to audit the pile of grants that accumulated while you weren't deciding at all. The examples below use Claude Code, but the audit questions and the shape of the guardrails apply to any agent that asks for permission.

One permission system, four layers

Before auditing anything, it helps to see that "permissions" is not one knob but four stacked layers, each answering a different question:

The layers differ in who enforces them. Rules and modes are the agent's harness checking a list; hooks are your own code making the call; the sandbox is the operating system (OS) itself, which holds even if the model is confused or manipulated into trying something it shouldn't. The guardrails later in this post work through these four layers in order, cheapest first.

Where your approvals actually live

Every "yes, don't ask again" is written to a settings file, and which file depends on where you were when you clicked. In Claude Code there are three:

FileScopeIn version control?What accumulates there
~/.claude/settings.jsonEvery project on your machineNoApprovals granted outside any repository, plus rules you add by hand
.claude/settings.jsonOne project, whole teamYesDeliberate, shared team policy
.claude/settings.local.jsonOne project, just youNo (gitignored)The pile: interactive approvals granted inside a repo land here

That last row is where the trap lives. Interactive approvals inside a repository are saved to .claude/settings.local.json at the repository root — resolved through worktrees, so a grant made in any subdirectory or worktree applies to the whole repo. It's gitignored, so no reviewer ever sees it, and it grows monotonically: nothing ever removes an entry except you.

The audit is therefore concrete: open .claude/settings.local.json in each active repo, open ~/.claude/settings.json, and read the permissions.allow arrays as what they really are — a list of things your agent can do with no human in the loop. Inside a session, the /permissions command shows the same picture interactively: every allow, ask, and deny rule, organized by which file it came from, with the ability to add and remove rules on the spot.

While reading the list, the question to ask of each entry is not "did I approve this?" — you did — but "would I approve this as a standing policy, knowing I'll never be asked again?" Those are very different questions, and the next section is about how to answer the second one.

Not all grants are equal

Judge each grant on two axes: is it reversible, and how far does the damage reach when it goes wrong. A grant that's easy to undo and contained in the repo is a fine standing permission; one that's irreversible or reaches outside your machine deserves a human every single time.

GrantExample ruleReversible?Blast radius
Reading filesRead(src/**)Nothing to undoContained
Editing tracked filesEdit(src/**)Git undoes itContained
Scoped project scriptsBash(npm run test:*)Usually cleanRepo + local state
Package installsBash(npm install *)Lockfile reverts, side effects don'tRuns arbitrary install scripts
Local git writesBash(git commit *)Reflog has your backLocal history
Broad git globBash(git *)Includes push --forceShared history, CI, prod
Network accessBash(curl *), WebFetch(domain:*)Data sent is sentAnything reachable, both directions
Bulk deletionBash(rm -rf *)Gone is goneWhatever the path matches
Wildcard MCP toolsmcp__server__*Depends on the serverWhatever the server can reach

safe fine watch it audit target

A few of these deserve a closer look, because they're exactly the grants that sneak in during a busy afternoon:

  • Broad globs that were approved for a narrow reason. You approved git * once because the agent was committing and you were tired of prompts — but that glob also covers git push --force and git clean -fdx. The same applies to any Bash(tool *) grant: the pattern is the permission, not the command you happened to approve it for.
  • Network in either direction. A standing curl or web-fetch grant means anything the agent can read — including a maliciously crafted file or web page instructing it — has a path out of your machine. Exfiltration needs exactly one unguarded outbound channel. Model Context Protocol (MCP) tools count double here: a wildcard grant on a server is a grant on everything that server can reach.
  • Install commands. npm install looks like dependency bookkeeping, but install scripts execute arbitrary code with your user's privileges the moment the package lands.

With the audit done and the risky entries identified, the remaining work is expressing your actual policy in the four layers — starting with the cheapest.

Declarative guardrails: allow, ask and deny rules

The first layer is the same mechanism the "always allow" button uses, pointed in the right direction: rules you write on purpose. Claude Code's permission rules come in three lists — allow, ask, and deny — each holding Tool(pattern) entries:

{
"permissions": {
"allow": [
"Read(src/**)",
"Bash(npm run test:*)",
"Bash(git commit *)",
"WebFetch(domain:github.com)"
],
"ask": [
"Bash(git push *)",
"Bash(npm install *)"
],
"deny": [
"Bash(git push --force*)",
"Read(.env*)",
"Bash(curl *)"
]
}
}

Precedence is the part worth memorizing: deny wins over ask, ask wins over allow, and specificity changes nothing. A broad deny like Bash(aws *) blocks even a narrower allow like Bash(aws s3 ls), and an ask rule prompts even when a more specific allow matches the same call. This asymmetry is a feature — it means a deny list stays authoritative no matter what accumulates in the allow list underneath it:

Two practical moves follow from the audit. First, prune and promote: delete the accidental entries from settings.local.json, and move the grants you genuinely stand behind into the checked-in .claude/settings.json, where they're reviewed like any other code and shared with the team. Second, write the deny list you wish you'd had on day one — secrets files, force pushes, raw network tools — because deny rules are the one list that reflexive clicking can never widen.

Permission modes: choosing a posture, not a rule

Rules decide call by call; the second layer decides your default when no rule matches. Claude Code's permission modes form a spectrum from control to agency:

ModeWhat runs without a promptSensible for
planReads only; the agent proposes before touching anythingExploring an unfamiliar or sensitive codebase
defaultReads; everything else prompts (or follows your rules)Day-to-day work
acceptEditsReads, file edits, and working-directory file commands (mkdir, mv, cp, rm, sed…)Iterating fast on changes you're reviewing anyway
autoNearly everything, with a background safety classifierLong autonomous tasks you'll review at the end
dontAskOnly what's pre-approved; everything else is refused, not promptedScripts and continuous integration (CI)
bypassPermissionsEverything, no checksIsolated containers and virtual machines (VMs) only

The healthy pattern is matching the mode to the task rather than ratcheting one way: plan while you scope a refactor, acceptEdits while you churn through it, back to default when the work touches deployment or secrets. You can cycle modes with Shift+Tab mid-session, so the posture can change as often as the work does. bypassPermissions deserves its reputation — it belongs in disposable environments, not on your laptop. (Even there a circuit breaker survives: rm -rf / and rm -rf ~ still prompt, and suspicious command strings are flagged for manual approval even when previously allowlisted. Nice — but a circuit breaker is a last line, not a policy.)

Hooks: guardrails with logic in them

Pattern rules can't express conditions — "block force pushes to main, allow them to my own branches" doesn't fit in a glob. The third layer, PreToolUse hooks, runs a script of yours before a matched tool call, and that script gets a veto. Exiting with code 2 blocks the call and feeds your message back to the agent; a JavaScript Object Notation (JSON) response can return an explicit permissionDecision of allow, deny, or ask.

A hook that enforces the force-push condition above:

#!/bin/bash
input=$(cat)
cmd=$(echo "$input" | jq -r '.tool_input.command // ""')

if [[ "$cmd" == *"git push"* && "$cmd" == *"--force"* && "$cmd" == *"main"* ]]; then
echo "Force pushes to main are blocked by policy. Push to a branch instead." >&2
exit 2
fi
exit 0

Registered in the same settings files as the permission rules:

{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "$CLAUDE_PROJECT_DIR/.claude/hooks/no-force-push-main.sh"
}
]
}
]
}
}

Because the hook is code, it can check anything code can check: which branch you're on, whether the path is inside a protected directory, whether a ticket number appears in the commit message. And because hooks live in the checked-in project settings, a team can encode its actual working agreements once, instead of each member re-deciding them one fatigue-weakened prompt at a time. A hook turns a rule you keep explaining to the agent into a rule the harness enforces without you.

Sandboxing: the guardrail that doesn't trust the agent

Everything so far filters which calls run. The fourth layer constrains what a running command can touch, enforced by the operating system rather than the agent — Seatbelt on macOS, bubblewrap on Linux. Sandboxing draws two boundaries around every Bash command:

  • Filesystem isolation. By default a sandboxed command can write only to the working directory and session temp space; you can tighten or widen it with allowWrite, denyWrite, denyRead and allowRead path lists — including making secrets like .env unreadable outright.
  • Network isolation. Outbound traffic goes through a proxy with a domain allowlist that starts empty; the first request to any new domain prompts, and you pre-approve known ones with sandbox.network.allowedDomains.

This is the layer that answers the exfiltration worry from the audit: with an empty network allowlist, a prompt-injected curl to an attacker's domain surfaces as a visible domain prompt instead of a silent success — even if the permission layer had been talked into allowing the command. Rules and hooks constrain what the agent asks to do; the sandbox constrains what happens even when the asking goes wrong. That property is why sandboxing pairs so well with looser modes: an agent in auto mode inside a sandbox with a curated domain list is often safer than a prompt-fatigued human approving everything in default mode.

Finding the equilibrium

Maximum control (prompt for everything) and maximum agency (bypass everything) are both failure modes — one burns you out until you flip to the other. The equilibrium is a policy, and the policy fits in one decision per grant:

The four layers then map cleanly onto how much each costs and how much it resists being eroded:

GuardrailSetup costResists approval fatigue?
Allow/ask/deny rulesMinutesDeny list always wins
Permission modesOne keystrokeEasy to ratchet loose
PreToolUse hooksA scriptCode, not clicks
SandboxingConfig; extra packages on LinuxOS-enforced

And because the allow pile regrows — that's what daily use does — the audit isn't a one-off. Make it a small ritual, monthly or whenever you notice yourself approving without reading:

  1. Open .claude/settings.local.json in your active repos and ~/.claude/settings.json, or run /permissions.
  2. Delete every grant you wouldn't re-approve as standing policy — deletion just means it prompts again.
  3. Promote the keepers into the checked-in project settings, where the team can see and review them.
  4. Check the deny list still covers your current stack — new tools mean new force-push equivalents.

The goal was never to approve less; it's for every standing permission to be one you chose on purpose, so that the prompts you still see are once again questions worth reading. An agent with well-drawn boundaries can be given more agency, not less — that's the trade the guardrails buy you.

References

  1. Claude Code docs — Permissions
  2. Claude Code docs — Permission modes
  3. Claude Code docs — Hooks
  4. Claude Code docs — Sandboxing
  5. Claude Code docs — Security