Claude Code security: what the agent may touch

I once told an agent, in plain language, that it had my full authorization to run a production mutation. It refused. I said it again, more explicitly. It refused again.

That was correct behavior, and it took me a minute to appreciate it. The safety layer sits in the harness, not in the conversation. A chat message does not change a permission decision, because if it could, the boundary would be worth exactly as much as the model's willingness to be talked out of it.

This is the useful mental model for Claude Code security: the things you say are input, the things you configure are enforcement. Everything below follows from that split.

Three layers, and what each one actually stops

There are three places a boundary can live, and they fail differently.

LayerWhere it livesWhat it stopsFailure mode
Permission rulessettings.jsonTool calls matching a patternToo broad, quietly approves neighbors
Hookssettings.json, external commandAnything you can express in codeWrong event; observes instead of blocks
SandboxOS / container boundaryEverything outside the boxExpensive to set up, easy to leave open

Most teams have the first one, half-configured. Almost nobody has the third.

The reason to care about the ordering is that each layer catches what the one above it missed, and the cost goes up as you descend. Permission rules are free and coarse. Hooks are cheap and precise. A real sandbox is the only one that holds when the other two are wrong, and it is the one that takes an afternoon.

The three enforcement layers around an agent: permission rules, hooks, and the sandbox boundary
Each layer catches what the one above missed, at increasing cost

The single most useful distinction I have found, and the one that took me longest to name.

A standing mandate is "go ahead, work through the backlog." It covers the ordinary shape of the work: read the repo, write code, run tests, open a branch.

Per-action consent is required for a specific class of actions that a standing mandate cannot reasonably cover, no matter how enthusiastically it was given.

In one session my orchestrator was blocked three times in an afternoon. Sending keystrokes into another agent session's trust dialog. Instructing a worker to change its own settings. Merging after a review that had not come back clean.

All three blocks were correct. All three were resolved by asking me, per action, and all three together cost under ten minutes.

The pattern in those three is worth stating explicitly, because it generalizes past this tool:

  • Self-modification. An agent changing the rules it runs under is not covered by permission to do the work. If it were, every boundary would be one instruction away from removal.
  • Answering someone else's security prompt. A trust dialog exists to ask a human. An agent that can click through it has defeated the dialog for every future case, not just this one.
  • Bypassing a gate that did not pass. A merge after a failed review is not a faster version of the same action. It is a different action.

If you configure nothing else, configure these three as things that always come back to a person. They occur rarely enough that the friction is negligible and they are exactly the ones where an automatic yes is unrecoverable.

What to put in the permission rules

Permission patterns are the cheapest layer and the one people over-invest in. Two rules keep it sane.

Deny by exception, not by enumeration. You cannot list every dangerous command. Anyone who has tried has a rule set with forty entries that still misses python -c. Deny the small set of genuinely irreversible things (force push, destructive database commands, anything that writes outside the project root) and let the rest go through the normal flow.

Allow narrowly and specifically. The point of an allow rule is to remove a prompt you are tired of, not to open a category. Bash(npm test) removes one prompt. Bash(npm *) removes that prompt and quietly hands over npm publish.

That second failure is silent, which is what makes it worth stating. Nothing breaks when an allow rule is too broad. It works better than before. You find out at the moment the broad half gets used.

Where hooks belong in the security picture

Permission rules match on tool names and command patterns. Hooks run code, so they can decide on things a pattern cannot express.

For access control the shape is a PreToolUse hook, because that is the event that fires before the tool executes and is allowed to block it. PostToolUse fires after success and cannot block; it is a record, not a gate.

The one detail worth carrying over from the file-access case: a hook on Read and Grep does nothing about cat .env, because that is a single Bash call. If you write a hook that guards file reads, write the second one that inspects Bash command strings, or you have locked the door and left the window.

I go through the full configuration in Claude Code hooks: stop the agent from reading your .env.

The failure mode nobody plans for: the guard that breaks quietly

In June I added a restriction that was entirely correct: a read-only review process should not be able to commit or push. Good change. It shipped.

It also broke the review process for three weeks, and nobody noticed.

The guard set a deny flag on the lane and did not turn on the corresponding scope flag that the caller needed. The lane refused fail-closed, which is the right default. But the error output was being discarded, so the failure surfaced as silence: the review simply produced no verdict from that seat, round after round.

The panel still returned results, because it had a quorum rule that tolerates a single missing participant. The fail-safe worked so well that it hid the outage.

Two lessons, and I would trade a lot of clever configuration for either one.

The dangerous regression is not bad code. It is correct code that a caller was not migrated onto. New guard added, existing consumer left behind. Every security tightening should be followed by the question: who called this before, and do they still work?

A discarded error message turns a failure into a mystery. I spent a day on the wrong diagnosis because the real reason was being swallowed. Surfacing that output was a smaller fix than the one I actually needed, and it would have made the real fix obvious three weeks earlier.

If your security layer can fail closed, make sure it fails loudly. Robustness and observability pull against each other here, and robustness usually wins by default because it is the one you designed for.

The sandbox question

Everything above is enforcement inside the tool. A sandbox is enforcement outside it, and it is the only layer whose correctness does not depend on your configuration being right.

The honest position: for a project on a machine that also holds your credentials, your SSH keys and your other clients' code, the permission layer is doing more work than it should. It is not designed to be the boundary between an agent and your entire filesystem. It is designed to be the boundary between an agent and mistakes within a project.

The practical middle ground I use:

  • Agents that touch client code run in a dedicated worktree, not the main checkout
  • Secrets are not on disk in any path reachable from the project root
  • Anything that writes outside the project goes through one place I can point at

That last one matters more than the first two. The value of a single exit point is not that it is safer in itself, it is that "what did this system send outside" becomes a question with an answer instead of a search.

What I would do this week

If you run agents on real code and have configured nothing, four steps, roughly two hours total.

  1. Deny the irreversible. Force push, destructive database commands, writes outside the project root. Five entries, not forty.
  2. Audit your allow rules for wildcards. Every * is a category you opened. Narrow the ones you cannot justify out loud.
  3. Add a PreToolUse hook on credential paths, plus the Bash companion. Thirty minutes.
  4. Decide your three per-action items. Self-modification, third-party security prompts, and bypassing a failed gate are a good default set.

Then the harder one, which is not configuration: check that your guards still fire. Pick one and try to trip it deliberately. A guard nobody has tested since the day it was written is a guard you believe in rather than one you have.

Frequently asked questions

The broader point

Every one of these controls exists because agreeing to something is not the same as enforcing it. That is not a Claude Code observation. It is the same thing I write about when a client asks why their AI review process approves everything, and the answer is that a check which never blocks anything is not a check.

Lees ook: Claude Code hooks: stop the agent from reading your .env

The orchestration layer I run this on is open source: github.com/Vinix24/vnx-orchestration. The permission and hook configuration is in the repository if you would rather read it than take my word for it.

Designing that boundary for a system that is going into production is what I do as AI-architect.

Vincent van Deth

AI Strategy & Architecture

I build production systems with AI — and I've spent the last six months figuring out what it actually takes to run them safely at scale.

My focus is AI Strategy & Architecture: designing multi-agent workflows, building governance infrastructure, and helping organisations move from AI experiments to auditable, production-grade systems. I'm the creator of VNX, an open-source governance layer for multi-agent AI that enforces human approval gates, append-only audit trails, and evidence-based task closure.

Based in the Netherlands. I write about what I build — including the failures.

Reacties

Je e-mailadres wordt niet gepubliceerd. Reacties worden beoordeeld voor plaatsing.

Reacties laden...