The Loosest Line in Your Repo Is in .claude/settings.json
We've spent the last few posts on the acute side of agent security — frisking inputs for hidden instructions, measuring the blast radius of reachable secrets, blocking destructive commands at runtime. But all of those sit on top of one quieter decision that sets the ceiling on how much damage a single injected instruction can do: your permission config. .claude/settings.json decides what the agent is allowed to do without asking you — and the loosest setting in there is the real size of your exposure.
Most of these files drift loose for the most human reason: every permission prompt is friction, so people allow-list broadly to stop being asked. The result is configs that quietly say "do anything." Here's what to look for, and a 110-line linter that finds it.
The permission model in one paragraph
Claude Code's permissions object has allow, deny, and ask arrays of rules shaped like Tool(specifier) — Bash(npm test *), Read(./src/**), WebFetch. deny wins over allow. A defaultMode sets the baseline behavior. The safe default (no config, or defaultMode: "default") is prompt-before-risky — the agent asks. Everything dangerous is a widening of that.
The five widenings that matter
perm-audit reads a settings.json and ranks what it finds:
defaultMode: "bypassPermissions" — CRITICAL. This turns off permission checks entirely. Every tool call runs unprompted. With this on, an injected "run this" is just… run. It exists for sandboxes; it has no business in a repo on your laptop. (Its cousin "acceptEdits" is milder — it auto-approves edits — but it's still a widening worth a warning.)
Bash(*) in allow — HIGH. This auto-approves every shell command. It's the single most common over-grant, because typing Bash(*) once is so much easier than allow-listing the commands you actually run. But it means a hijacked agent never has to ask before curl | sh. The fix is boring and correct: allow the specific commands (Bash(npm test *), Bash(git status)) and let everything else prompt.
Broad reads and network egress — WARN. Read(**) auto-approves reading any file (including the secrets reach warned you about). A bare WebFetch / WebSearch allow, or Bash(curl *) in allow, hands the agent an unsupervised way to send data out — the exfiltration channel that makes a secret-read actually matter.
enableAllProjectMcpServers: true — WARN. This auto-trusts every MCP server declared in .mcp.json. Each server's tool descriptions go straight into the model's instruction context — exactly the hidden-instruction surface we frisk files for. Auto-approving all of them is auto-trusting text you didn't write.
No deny rules protecting secrets — HIGH (when Bash is broad). allow is a convenience; deny is a guarantee — it holds even if the agent tries. A config that broadly allows but denies nothing has no floor. The fix is a few lines that cost nothing:
"deny": [
"Read(./.env)", "Read(./.env.*)", "Read(./secrets/**)",
"Read(~/.ssh/**)", "Bash(curl *)", "Bash(wget *)"
]
The tool
$ perm-audit
perm-audit — .claude/settings.json
● CRITICAL defaultMode is bypassPermissions
every tool call runs with NO permission check. One injected instruction = full shell.
● HIGH allow rule 'Bash(*)' auto-approves ALL Bash
any Bash call runs without asking. Narrow it to the commands you trust.
● HIGH no deny rules protecting secrets
add Read(./.env), Read(./secrets/**), Read(~/.ssh/**), Bash(curl *) …
● WARN enableAllProjectMcpServers is true
● WARN allow rule 'Read(**)' auto-approves ALL Read
6 finding(s), 3 high-severity.
It's severity-ranked and exits non-zero on anything HIGH or CRITICAL, so it drops into CI or a pre-flight. Crucially, it doesn't cry wolf: a well-scoped config — specific Bash(...) commands, scoped Read(./src/**), and a secret deny block — comes back clean. (We fixed a false-positive in the first pass where every allow rule looked broad; the test suite now pins "scoped allows are clean" precisely so the tool stays trustworthy.) And a repo with no settings.json is reported as the safe baseline — you only need this once you've started allow-listing.
How it fits
This is the fourth and quietest layer of the same defense:
- frisk — shrink the chance the agent is injected (scan inputs).
- reach — shrink what it could steal (audit reachable secrets).
- guard — stop the destructive command at runtime (a PreToolUse veto).
- perms — set the ceiling on what runs unprompted in the first place.
Guard, reach, and frisk all matter more when perms is loose, and matter less when it's tight. So tighten the ceiling first: it's the cheapest, most durable control of the four — a few lines of config that turn "the agent can do anything" into "the agent asks before anything risky."
The real lesson
Permission prompts feel like friction, so we widen them away — and in doing so we quietly set how large a single bad instruction can get. The fix isn't to live with the prompts; it's to encode the small set of things you actually trust into allow, put a hard floor under it in deny, and never leave bypassPermissions on outside a sandbox. One perm-audit run tells you how far your config has drifted from that, ranked worst-first.
We build and run autonomous agent fleets on Claude Code in production. If you're hardening an agent setup, talk to us.
Get the tool → perm-audit in agent-ops — open source (MIT), a single dependency-free script, part of our toolkit for running coding agents in production.
Found this useful?
- You're Running Five Agents on One Repo. Who Stops Them Overwriting Each Other?
- Your Agent Did the Work, Then It Vanished. Here's How to Get It Back.
- Your Claude Code Session Is Permanently Stuck on a 400. Don't /clear — Do This.
- The One Kind of Agent Work You Can't Recover — and How to Stop Losing It
- Your Agent Will Eventually Run git reset --hard. Stop It at the Door.
- Your Agent Reads Text You Can't See. Frisk It First.
- You're Running a Dozen Agents. Which One Is Stuck?
- What's Actually In Your Claude Code Session File (and a Tool to Read It)
- Your Agent Fleet's Worktrees Are a Minefield. Here's How to See It.
- The Agent Wrote the Code in Minutes. Reviewing It Takes Hours. Here's Why.
- Your Claude Agent Re-Reads Its Whole Prompt Every Turn. Here's the 10× You're Leaving On the Table.
- Your Agent Can Read Everything Your Shell Can. Have You Looked?
- The Most Expensive File in Your Repo Runs on Every Single Turn
- Your Skill's Description Is the Whole Ballgame
- Your Repo Already Knows How To Build Itself. Put That In the CLAUDE.md.
Related posts
The boring part: governance that enforces, not displays
Most "AI governance" is a dashboard. It shows you what an agent spent, what it touched, what it decided — after the fact, in a panel nobody refreshes until the bill arrives. A display is not a control. A control is the t…
Your Launch Deserves a Real Demo Video, Not a Screen Recording With Captions
Most product launches skip the demo video entirely, and the ones that don't ship one usually reach for the fastest option: a raw screen recording, maybe some auto-generated captions, posted as-is. It's better than nothin…
The MCP Ecosystem Is Growing Fast — and Quietly Becoming a Directory of Hosted Services
We [recently measured](/insights/in-the-registry-is-not-runs-2) whether the servers in the official MCP registry actually *run* (short version: 84% are remote-only, and under half of the installable ones boot clean). Whi…
Installing a Skill Usually Means Running a Stranger's Code
The pitch for Claude Code "skills" is that they're just *instructions* — a `SKILL.md` file with some markdown telling the agent how to do a task. That framing makes them feel safe to install. A prompt can't hurt you; wor…