Claude Code permissions not working? Here's what settings.json can't catch

Published

I started looking into Claude Code permissions because I was testing Claude Code with autoAccept turned on and wanted a very boring setup: let it edit files inside one project, but do not let it read secrets or wander into unrelated directories. That sounded simple enough. I added deny rules, ran a few tests, and for a minute I thought I had it locked down.

Then I hit the kind of problem that makes you stop trusting the guardrails. I was trying to let an agent refactor a small script in one repo. Instead, it started inspecting files I did not mean to expose, and one run even tried to touch something outside the project boundary. Nothing catastrophic happened, but it was exactly the sort of “that should not even be possible” moment that sends you digging through config docs at 1 a.m.

What settings.json does and doesn’t do

To be fair, Claude Code’s settings.json permissions are not useless. The allow and deny system gives you a static policy layer. If you know a specific command or file pattern you never want touched, you can express that. For obvious cases, it helps.

{
  "permissions": {
    "deny": ["Bash(rm -rf:*)", "Read(**/.env)"]
  }
}

The issue is that this kind of rule set only catches what you can describe in advance, in the exact shape you thought of in advance. Deny Read(**/.env), and maybe that blocks the direct path you had in mind. But what about a secret copied into another config file? What about a tool invocation that causes an indirect read? What about an agent making an API call with data you did not expect it to collect first?

I also ran into the brittle part that is easy to miss during initial testing: wait, it worked, then it didn’t. One run would respect the boundary I expected, and the next run would take a slightly different path through the task and suddenly interact with something I had not explicitly denied. Static rules can look solid when the workflow is narrow. As soon as the agent behavior gets a little less predictable, the gaps show up.

That is the core limitation. settings.json is a policy file, not a live observer. It can express rules. It cannot tell you much about what actually happened during a run, what nearly happened, or which behaviors drifted outside the intent of the task.

The gap: what happens at runtime that static rules miss

The part that bothered me most was not the obvious dangerous command case. It was the subtle stuff. An agent does not need to run rm -rf to create risk. It can read something sensitive, make a network call you were not expecting, or modify a file outside the intended scope because the task phrasing nudged it there.

And once the run is underway, the real question changes. It is no longer “did my config contain a deny rule for this exact string?” It becomes “what is this agent actually doing right now?” Static config is weak at answering that because the important events are behavioral. Path expansion, tool chaining, task reinterpretation, and side effects happen at runtime.

If you have used Claude Code long enough, you have probably seen this pattern: the rule set feels fine in a toy example, then a real task exposes edge cases immediately. That does not mean the permissions system is broken. It means it is incomplete.

Runtime monitoring as the missing layer

What ended up making more sense for me was adding a runtime behavioral layer on top of the static rules. That is what agentcheck is for. It does not replace Claude Code’s built-in permissions. It watches what the agent actually does during execution, flags behavior that violates the policy you care about, and starts in shadow mode by default so you can see issues before you start blocking anything.

That last part mattered to me. I did not want to guess at a perfect policy on day one and accidentally break legitimate tasks. I wanted visibility first. In shadow mode, you can see the behavior you would have blocked without changing the run outcome.

[agentcheck] WARN filesystem/no-write-outside-scope
  agent tried to write: /Users/alex/.ssh/config
  allowed scope: /Users/alex/myproject/**
  action: shadow (would have blocked)

That kind of output is what I was missing with plain settings.json. It answers the practical question: what happened, why is it a problem, and what would enforcement have done? That is much easier to reason about than trying to predict every dangerous path up front.

Getting started

If you want to try it, start with the GitHub repo: github.com/paprika-org/agentcheck. The project is a runtime behavioral governance layer for Claude Code agents. It monitors agent behavior at runtime and flags violations, with shadow mode on by default. That default is the right call, in my opinion, because you can watch a few real sessions first and figure out where your actual risk is before you turn any rule into a hard block.

Closing

I do not think the answer is “throw away settings.json.” Static deny rules are still worth having. They are just not enough on their own if you care about real boundaries rather than hopeful ones.

What feels solved for me now is visibility. I can tell when an agent tries to step outside the allowed scope, and I do not have to rely entirely on a brittle pile of string-matching rules. What is not solved yet is everything else. agentcheck is still early; it only has three rule packs so far, and there is plenty of room to improve coverage and ergonomics. But even in that state, adding runtime monitoring closed a gap that Claude Code permissions alone did not catch.

Canonical URL: https://paprika-org.github.io/agentcheck/blog-permissions.html