Intermediate · 18 min read · updated 2026-07-14

Claude Code Permissions: A Practical Safety Guide

How Claude Code permissions actually work in 2026 - allow/deny lists, sandboxing, and how to let an agent run fast without letting it wreck your repo.

Claude CodeAI SafetyDevOpsAutomationSecurity
Who this is for: developers and teams running Claude Code on real codebases - not toy demos - who want the agent to move fast on routine work but never touch production data, secrets, or git push --force without a human looking first.

Claude Code permissions are the single lever that decides whether an agentic coding tool is a productivity multiplier or a liability. Get them wrong in one direction and you spend your day clicking "allow" on ls commands. Get them wrong in the other direction and an agent quietly runs rm -rf on the wrong directory, or pushes a broken commit straight to main at 11pm while you're not watching. This guide is the settings, patterns, and mental model I actually use across client projects - not the theoretical happy path from the docs.

Why permissions matter more than model choice

Most teams evaluating Claude Code, Cursor, or similar tools spend their evaluation time on model quality - which is reasonable, but it's not where the real risk lives. A model that's 95% as smart but runs inside a tight permission boundary is safer than a smarter model with --dangerously-skip-permissions turned on. The failure mode with agentic coding tools isn't usually "the AI wrote bad code" - static analysis and tests catch that. It's "the AI took an action outside the codebase" - deleted a file it shouldn't have touched, committed a secret, or ran a command against a real database because a .env file pointed at production instead of staging.

I've run Claude Code on production client repos for over a year now. In that time the permission system has caught three things that would have been genuinely bad: an attempted git push --force to a shared branch, a Bash command that would have truncated a log file being actively read by a running process, and once, an agent trying to curl an internal admin API because a stale environment variable pointed at prod instead of a local mock. None of those were the model being malicious. All of them were the model doing the plausible next step given incomplete context - which is exactly what permissions exist to catch.

How the permission system actually works

Claude Code intercepts every tool call - Bash, file edits, web fetches, MCP tool calls - before execution and checks it against three lists, in this order:

  1. Deny list - if a pattern matches here, the action is blocked outright, no prompt, no override in that session.
  2. Allow list - if a pattern matches here, the action runs silently, no prompt.
  3. Ask (default) - everything else surfaces an interactive permission prompt.

Rules live in .claude/settings.json (project-level, checked into git so the whole team shares it) and .claude/settings.local.json (personal, gitignored - your own looser rules on top of the shared baseline). There's also a global ~/.claude/settings.json for machine-wide defaults.

{
  "permissions": {
    "allow": [
      "Bash(npm run test:*)",
      "Bash(npm run lint)",
      "Bash(git status)",
      "Bash(git diff:*)",
      "Read(*)"
    ],
    "deny": [
      "Bash(git push --force:*)",
      "Bash(rm -rf:*)",
      "Bash(curl:*)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(**/secrets/**)"
    ]
  }
}

The pattern matching is glob-based and tool-scoped - Bash(npm run test:*) allows any npm run test invocation with any arguments after it, but doesn't touch Bash(npm run build). This granularity is the whole point: you're not choosing between "trust everything" and "approve everything," you're drawing a specific line per command family.

Tip: Run /permissions inside a Claude Code session to see the live effective ruleset - merged across global, project, and local settings - rather than guessing from three separate JSON files.

The four permission modes

Claude Code ships with modes that change the default posture, switchable mid-session with /permissions or on launch with a flag.

ModeBehaviorWhen I use it
Default (interactive)Prompts for anything not explicitly allowedNew codebase, first week on a client repo
Plan modeRead-only - agent can explore and propose a plan but cannot edit or executeUnderstanding an unfamiliar repo, writing a spec before touching code
Accept editsFile edits auto-approved, Bash/network still promptedRefactors and multi-file renames I've already scoped
--dangerously-skip-permissionsEverything auto-approved, no prompts at allDisposable sandboxes and CI only - never on a machine with real credentials

That last mode's name is not marketing - it is a genuinely dangerous flag and Anthropic named it accordingly on purpose. I use it exactly twice: inside a throwaway Docker container spun up for a single headless task, and inside CI runners that have no access to anything outside the checked-out repo and a scoped deploy token. If you're running it on your laptop with your AWS credentials in ~/.aws/credentials, you're one bad prompt away from a very bad day.

Plan mode as the default for unfamiliar code

If you're dropping Claude Code into a codebase you or your team didn't write - inherited legacy code, an open-source dependency you're patching, a client handoff - start every session in plan mode. It costs nothing (the agent still reads and reasons at full capability) and it forces a checkpoint where a human reviews the actual diff before anything executes. See claude-code-plan-mode-workflow for the full workflow, including how to convert an approved plan into an execution pass without re-explaining context.

Sandboxing: the layer below permissions

Permissions are policy - what the agent is allowed to try. Sandboxing is enforcement - what actually happens if it tries anyway, or if a permission rule has a gap you didn't anticipate. Treat them as two separate layers, because permission rules will always have edge cases.

Practical sandboxing options, roughly in order of how often I reach for them:

  • Dev containers / Docker - the cheapest real boundary. Mount only the repo directory, no home directory, no cloud credentials in the container's environment. A rm -rf / inside the container costs you a docker rm and nothing else.
  • Separate cloud credentials per environment - staging and production should never share an IAM role or API key. If Claude Code (or you) fat-fingers an environment variable, the blast radius is staging, not customers' data.
  • Git worktrees for parallel agent runs - if you're running multiple Claude Code sessions on the same repo concurrently, isolate them in separate worktrees so one agent's uncommitted mess doesn't collide with another's. Covered in depth in claude-code-git-worktrees.
  • Read-only database replicas for anything an agent might query directly - never point an autonomous session at a primary database connection string.
Warning: A permission deny rule for Bash(curl:*) does not stop a Node script the agent writes from making an HTTP request via fetch(). Permissions gate tool calls, not everything a script it writes can do at runtime. If network isolation matters, enforce it at the container/network level, not just the permission list.

Building a real allow/deny list

Don't start from a blank file. Start from what the agent actually does in a normal day and lock in the safe 80%, then deny the small number of catastrophic commands explicitly.

Step 1: allow the repetitive-but-safe

"allow": [
  "Bash(npm run test:*)",
  "Bash(npm run lint:*)",
  "Bash(npm run typecheck)",
  "Bash(git status)",
  "Bash(git diff:*)",
  "Bash(git log:*)",
  "Bash(git add:*)",
  "Bash(git commit:*)",
  "Edit(src/**)",
  "Write(src/**)"
]

Notice git commit is allowed but git push is not in this list - I want commits to happen freely during a session (they're local and reversible with git reset) but pushes to stay a deliberate, prompted action.

Step 2: deny the irreversible or credential-adjacent

"deny": [
  "Bash(git push --force*)",
  "Bash(git push origin main)",
  "Bash(rm -rf:*)",
  "Bash(DROP TABLE*)",
  "Bash(npm publish:*)",
  "Read(./.env*)",
  "Read(**/*.pem)",
  "Read(**/id_rsa*)",
  "Read(**/service-account*.json)"
]

Step 3: leave everything else on ask

Resist the urge to allow-list everything just to stop the prompts. The prompt is doing its job precisely when it interrupts something you didn't anticipate - an unusual curl to a domain you don't recognize, a chmod 777, a database migration on a table name that isn't in your usual list. If you're hitting the same unexpected prompt repeatedly and it's genuinely safe, that's the signal to add a narrow allow rule for it - not to blanket-allow the whole tool.

Reviewing destructive actions before they run

Even inside a good permission setup, some actions deserve a manual look every single time, regardless of how well-scoped your allow list is:

  • Any DROP, TRUNCATE, or DELETE FROM without a WHERE clause
  • Force-pushes to any shared branch
  • Anything that writes outside the project directory
  • Package publishes (npm publish, pip upload) - these are effectively irreversible
  • Deleting or overwriting anything in a backups/ or migrations/ directory

For teams running Claude Code across multiple repos, claude-code-hooks-guide covers PreToolUse hooks - shell scripts that run before a tool call executes and can block it programmatically, which is a more auditable mechanism than eyeballing prompts, especially useful for enforcing the same destructive-action checks across an entire team without relying on everyone maintaining their own settings.json.

Trust boundaries in multi-agent and headless setups

Once you move beyond a single interactive session - subagents, headless automation, CI pipelines - permission scope needs to travel with the task, not sit at the machine level. A subagent spawned to "fix the failing test" should not inherit permission to touch deployment config, even if the parent session has that permission. See claude-code-subagents-guide for how scoped subagent permissions work in practice, and claude-code-headless-automation if you're running Claude Code unattended via claude -p in a cron job or CI step - headless mode has no human in the loop to catch a prompt, so your allow/deny list there needs to be far tighter than your interactive laptop config, effectively the opposite failure mode from over-restricting a human-supervised session.

A quick trust-boundary checklist

  • Does this session have credentials it doesn't need for this task? Remove them.
  • Is the working directory scoped to exactly the repo, or does it have access to sibling directories with other clients' code?
  • If this runs unattended, what's the worst single command it could execute, and is that command explicitly denied?
  • Are secrets loaded via environment variables the agent's Bash tool can echo, or via a mechanism (like a secrets manager fetch inside the app, not the shell) the agent can't directly read?

FAQ

What's the difference between Claude Code permissions and a sandbox?

Permissions are a policy layer inside Claude Code that decides which tool calls are allowed, denied, or need approval. A sandbox is an infrastructure layer - a container, VM, or restricted user account - that limits the damage even if a permission rule has a gap. Use both; neither replaces the other.

Is --dangerously-skip-permissions ever safe to use?

Yes, but only in an environment where the worst-case outcome is acceptable - a disposable container with no real credentials, or a CI job scoped to a throwaway branch. On a developer laptop with real cloud credentials and SSH keys, it removes the one safety net that catches mistakes.

Can I set different permission rules for different folders in the same repo?

Yes - Edit and Write rules accept glob paths, so you can allow edits under src/** while denying them under infra/** or migrations/**, which is useful when application code and infrastructure-as-code live in the same repo but deserve very different trust levels.

Do permission settings sync across my team?

.claude/settings.json is meant to be committed to the repo so the whole team shares the same baseline allow/deny rules. .claude/settings.local.json stays personal and gitignored for individual looser rules on top of that shared floor.

Does plan mode slow the agent down?

Marginally, since you review before execution, but it's usually faster overall on unfamiliar code because you catch a wrong assumption before it turns into five files of changes built on that wrong assumption, rather than after.

What should never go on the allow list, no matter how convenient?

Force-pushes to shared branches, anything touching .env or credential files, database commands without a scoped WHERE clause, and package publish commands. These are the ones where "it prompted me one extra time" is a much smaller cost than "it ran once and can't be undone."

Getting this right on your own stack

If you're setting up Claude Code across a team or wiring it into CI and want a second pair of eyes on the permission model before something expensive goes wrong, grab a free 30-minute call through the contact form or message me directly on wa.me/972585802298.

Sources