Claude Code Security: Permissions, Denylists, and What They Don't Cover
Claude Code ships with a permission system. You’ve probably already disabled it. The --dangerously-skip-permissions flag exists because the built-in prompts interrupt agent flow to the point where autonomous operation becomes impractical. So teams skip them — and then they have an agent with no controls running against their infrastructure.
That flag name isn’t an accident. Anthropic is telling you this is dangerous. The honest version of this article: Claude Code’s native permission controls are real and worth configuring — but they’re a partial control, not a production governance layer. This piece covers exactly how the native system works, the three places it leaks, and where you have to put a control plane instead.
How Claude Code’s permission system actually works
Claude Code evaluates every tool call against three rule lists in your settings.json:
{
"permissions": {
"allow": ["Bash(npm run test:*)", "Read(src/**)"],
"ask": ["Bash(git push:*)"],
"deny": ["Read(./.env)", "Read(~/.aws/**)", "Bash(sudo *)", "Bash(rm -rf *)"]
}
}
Two things matter here:
- Evaluation order is deny → ask → allow. A
denyrule always wins. Anaskrule prompts a human. Anallowrule auto-approves. Anything unmatched falls back to the default (interactive prompt). - Settings are layered. Enterprise managed settings override the command line, which overrides project
.claude/settings.local.json, then.claude/settings.json, then user settings. Managed settings can’t be overridden by a developer — which is the only reason any of this is enforceable on a team.
This is genuinely useful. Deny Read(./.env), Read(~/.aws/**), Bash(sudo *), and Bash(rm -rf *) in a managed policy and you’ve blocked the most obvious foot-guns. Configure it. It’s free and it helps. But it stops well short of governance, for three reasons.
Leak #1: --dangerously-skip-permissions turns it all off
When you skip permissions, every tool call is auto-approved. Every shell command executes. Every file write goes through. The agent has exactly the same access as the user or service account that launched it.
If that user has AWS admin credentials, the agent has AWS admin credentials. If their SSH key can push to production, the agent can push to production. If there’s a database connection string in the environment, the agent can query or drop tables.
You can prevent developers from entering bypass mode — set disableBypassPermissionsMode to "disable" in a managed settings file. But that only works if you ship and enforce managed settings on every machine, and it pushes developers back into interactive prompts, which is the friction they were escaping in the first place. The native system makes you choose between enforced-but-annoying and autonomous-but-ungoverned. Production needs both.
Leak #2: the denylist is bypassable by design
This is the one most teams miss. A denylist blocks named paths to a capability, not the capability itself. Deny Read(./.env) and the agent simply runs:
cat .env # Bash, not Read
grep -r SECRET . # also reads the file
xxd .env | head # so does this
Unless you also deny Bash(cat:*), Bash(grep:*), Bash(xxd:*)… and every other tool that can read a file — which you can’t enumerate — the secret is reachable. Anthropic’s own permission model evaluates the declared tool (Read vs Bash), so a Read deny does nothing about a Bash that reads. Security researchers have demonstrated exactly this class of deny-rule bypass.
Denylists fail open. The only model that fails closed is deny-by-default: nothing is allowed unless explicitly permitted, scoped to the specific action and resource. Claude Code’s native config is an allowlist-with-holes, not deny-by-default — and you enforce deny-by-default at a layer the agent can’t talk around, not in the agent’s own config.
Related: Why static RBAC isn’t enough for AI agents — agents take dynamic paths to the same outcome, so role lists over-grant or break.
Leak #3: MCP servers and env vars expand the surface underneath the rules
Model Context Protocol servers extend Claude Code’s capabilities — a GitHub MCP server creates PRs, a Postgres MCP server runs queries, an AWS MCP server manages cloud resources. There’s no permission layer between Claude Code and the MCP server: if the server can do it, the agent can do it.
Consider a Postgres MCP server you added so the agent can check schema definitions. The server doesn’t distinguish SELECT information_schema from DROP TABLE on production — it’s a database connection, and everything the connection can do, the agent can do. “Use a read-only DB user” is correct and insufficient; you need that same least-privilege thinking on every MCP server, environment variable, and tool.
Environment variables are the quiet version of this. Claude Code can scrub variables from subprocess environments, but by default the agent inherits the shell it launched in — every AWS_SECRET_ACCESS_KEY, DATABASE_URL, and API token sitting in your env. Scrubbing helps; it’s still a per-machine setting you have to ship and verify, not a guarantee.
The AWS credential problem (a worked example)
The most dangerous Claude Code configuration is also the most common: AWS admin credentials, because the developer who set it up uses that profile for daily work.
Real scenario: the agent deploys a Lambda. It has AdministratorAccess. Mid-deploy it decides the function needs an SQS queue and a DynamoDB table — so it creates them. It decides the function needs internet access — so it modifies a VPC security group. Every action lands in CloudTrail; nobody reviews CloudTrail in real time. Two weeks later the security team finds an over-permissive security group, an unencrypted table, and a queue with no DLQ, and tracing it back to one agent session means correlating timestamps across services.
What proper AWS access looks like (and what AWS itself now prescribes for AI agents — least-privilege, actor-type-differentiated IAM):
- Temporary credentials from STS
AssumeRole, not long-lived keys - IAM policies scoped to specific resources (this bucket, this function) —
s3:GetObject, nots3:* - Service-level restrictions (read EC2, cannot modify security groups)
- Session duration limits (expire in 1 hour, not 12)
- Session tags identifying the agent, developer, and project
That’s a per-request decision, not a static profile. See Governing Claude Code’s AWS access for the full pattern.
When “no controls” actually bites: the terraform destroy story
Early 2026, a team running Claude Code in auto mode let it “clean up unused infrastructure.” It ran terraform destroy against a workspace pointed at production and wiped roughly two and a half years of data before anyone noticed. No deny rule for Bash(terraform destroy*) existed because nobody thought to write it — which is the whole problem with denylists: you only block the disasters you predicted.
A deny-by-default policy with a human approval gate on destructive infra commands (terraform apply/destroy, kubectl delete, rm -rf, sudo, aws … delete-*) turns that incident into a one-tap “deny” in Slack. See approval gates for Claude Code.
What a real security layer looks like
The model production needs has four components, and only the first is partially covered by native config:
- Identity — every session attributed to a specific human and purpose; session ID in every log line; any action traceable to who launched it and why.
- Policy — every operation checked against a deny-by-default engine before it executes, defined by the team (not the individual developer), scoped to action + resource, at a layer the agent can’t reconfigure.
- Audit — every operation in an immutable, structured log: which session, which developer, what operation, what parameters, what result. Not “agent called AWS API.”
- Control — stop any session instantly, change policy without restarting agents, set cost limits, and require human approval for specific operations.
Native Claude Code config gives you a weak version of policy and nothing durable for identity, audit, or control. The gap between “Claude Code with no controls” and “Claude Code with production security” is exactly where a control plane sits: a gateway between the agent and your infrastructure that enforces policy, logs every action immutably, and gives you a kill switch. In that model the agent holds zero credentials — it asks the gateway, the gateway decides, the gateway acts.
The uncomfortable truth
Claude Code’s security model assumes a human is watching. --dangerously-skip-permissions is an escape hatch for development, not a production configuration, and the native permission system — useful as it is — fails open: skippable, bypassable, and per-machine. Without an external enforcement layer, the answer to every question about agent permissions is “the agent can do whatever its credentials allow.” That’s not security. That’s hope.
FAQ
Does Claude Code have built-in security?
Yes — a permission system with allow/ask/deny rules (deny takes precedence), layered settings, a disableBypassPermissionsMode managed control, and subprocess env scrubbing. It’s worth configuring, but it’s a partial control: it can be skipped with --dangerously-skip-permissions, its denylists are bypassable (a Read(./.env) deny doesn’t stop Bash(cat .env)), and it’s enforced per-machine rather than centrally.
Is --dangerously-skip-permissions safe for production?
No. It auto-approves every tool call and gives the agent the full access of the account that launched it. Use it only in disposable sandboxes; in production, route the agent through a policy-enforced gateway instead.
How do I stop Claude Code from reading secrets or running destructive commands?
Native deny rules (Read(~/.aws/**), Bash(sudo *), Bash(rm -rf *)) help but are incomplete because the same capability has many command paths. A deny-by-default control plane with human approval gates on destructive operations is the enforceable answer.
What’s the difference between Claude Code permissions and an AI agent control plane? Permissions live in the agent’s own config (skippable, editable by the developer, per-machine). A control plane sits outside the agent as a proxy it can’t reconfigure — enforcing centralized policy, holding the credentials so the agent has none, and producing an immutable audit trail.
Put this into practice with Sentrely
Everything covered in this article is built into Sentrely's managed control plane. Get early access and have it running against your Claude agents in minutes.