All posts
Guide

How to Lock Down Claude Code Permissions

How Claude Code's allow, ask and deny rules work, which settings file wins, a safe starter settings.json, and a one-minute check for risky permissions.

8 min read By Brad McEvilly

Claude Code asks before it runs a shell command, edits a file or fetches a web page, unless a setting tells it not to. Those settings live in a few JSON files, and the rules from every file add up. One loose rule in any of them is enough to let a command run without asking.

This guide explains how the rules work and which file wins, gives you a safe starter config, shows how to see what is in effect on your machine in about a minute, and lists the common mistakes. It follows Claude Code's current docs. Claude Code changes often, so if your version behaves differently, check your version's docs.

How allow, ask and deny rules work

The permissions block in a settings file holds three lists. Rules in allow run without asking. Rules in ask always ask you first. Rules in deny are refused.

{
  "permissions": {
    "allow": ["Bash(npm run test *)"],
    "ask": ["Bash(git push *)"],
    "deny": ["Read(./.env)"]
  }
}

What a rule names. A rule is a tool name, optionally followed by what it applies to in parentheses. Bash(npm run test *) covers one family of shell commands. Read(./.env) covers one file. WebFetch(domain:example.com) covers one site. A bare Bash covers every shell command, and so does Bash(*).

Deny wins, then ask, then allow. Claude Code checks deny rules first, then ask rules, then allow rules, and the first match decides, however specific the other rules are. So an allow rule can never carve an exception out of a deny rule, and an ask rule still asks even when an allow rule matches the same command.

Where the star goes. A * matches any text, spaces included. Put it after the subcommand. Bash(git log *) allows only git log commands. Bash(git *) allows every git command, git push included.

What a rule does not stop. A Bash rule matches the command as Claude writes it. Bash(git push *) stops git push origin main, but not the same program run another way, such as git -C . push origin main or sh -c 'git push'. Treat deny and ask rules as guard rails for the usual commands, not as a wall around a program. For a hard boundary, turn on Claude Code's sandbox as well.

Permission modes: what defaultMode does

permissions.defaultMode sets how each session starts. These are the values in current versions:

default            asks before anything beyond reading files
acceptEdits        also edits files without asking
plan               reads and plans; edits wait for your approval
auto               no routine prompts; a classifier reviews actions
dontAsk            refuses anything that would have asked
bypassPermissions  runs everything without asking

Never use bypassPermissions on your own machine. Claude Code's docs say to use it only in isolated environments, like containers or virtual machines. In this mode nothing stands between a command the agent writes and your machine. The agent reads files, issues and web pages as it works, and an instruction hidden in any of them can become a command that runs straight away. Deny rules still apply in every mode, but they only cover what you thought to list.

Turn it off for good. Set disableBypassPermissionsMode to "disable" in your user settings. Claude Code then refuses the mode, including the --dangerously-skip-permissions flag.

Which files can set it. In current versions, bypassPermissions and auto only take effect from your user file or from settings your organization manages. A project's files cannot switch them on. Before version 2.1.257, bypassPermissions worked from any settings file, so a repository you cloned could turn it on. On an older version, check every file.

Which settings file wins

Claude Code reads settings from four places:

~/.claude/settings.json      you, in every project
.claude/settings.json        everyone in this project
.claude/settings.local.json  you, in this project only
managed settings             your organization; cannot be overridden

For a single value, the closest file wins. For a key like defaultMode, managed settings come first, then command-line flags, then .claude/settings.local.json, then .claude/settings.json, then your user file.

Permission lists add up. The allow, ask and deny lists from every file are combined, not replaced. Because deny is checked first, a deny rule in any file blocks the action, whichever file allows it. That makes your user file a good home for deny rules: they protect every project. It also means one loose allow rule applies everywhere its file does.

Project allow rules wait for your trust. Allow rules and additionalDirectories in a project's .claude/settings.json only take effect after you accept the trust prompt for that folder, which lists them. Read that list before you accept, especially in a repository you did not write. Deny and ask rules apply straight away, since they only restrict.

Approvals you click become rules. Choosing "Yes, and don't ask again" on a prompt for a shell command saves an allow rule to .claude/settings.local.json. Over a few weeks that file can collect rules you no longer remember adding.

A safe starter config

Put this in ~/.claude/settings.json so it covers every project. Change the test and lint commands to the ones you actually run.

{
  "permissions": {
    "defaultMode": "default",
    "disableBypassPermissionsMode": "disable",
    "allow": [
      "Bash(npm run test *)",
      "Bash(npm run lint)"
    ],
    "ask": [
      "Bash(git push *)",
      "Bash(npm publish *)",
      "Bash(rm -rf *)"
    ],
    "deny": [
      "Read(~/.ssh/**)",
      "Read(~/.aws/**)",
      "Read(**/.env)",
      "Read(**/.env.*)",
      "Read(**/secrets/**)"
    ]
  }
}

Why each part is there. disableBypassPermissionsMode makes the riskiest mode impossible to turn on by accident. allow names only the commands you run all day and would never think twice about. ask covers commands whose effect leaves your machine or cannot be undone. Ask beats allow, so these still ask even if a project file allows them. deny keeps Claude Code's file tools away from your keys and secret files, and a deny on reading a file also stops Claude Code from editing it.

In a rule, ~/ is your home folder, and **/.env means a .env file in any folder of the project you are working in. Deny rules cover Claude Code's own file tools and the file commands it recognizes, such as cat. They do not stop a script that opens a file by itself. If that matters for your work, turn on the sandbox too.

If Claude Code warns about a rule when it starts, the rule syntax may differ in your version. Check your version's docs.

Check your effective settings in one minute

1. List the rules in effect. In Claude Code, type /permissions. It lists every allow, ask and deny rule, and the settings file each one comes from.

2. See which files loaded. Type /status. Its setting sources line names each settings file that loaded, and shows when your organization's managed settings apply.

3. Find broken entries. In a terminal, run claude doctor. It reports settings files and rules Claude Code could not read. A broken rule is skipped, so it protects nothing.

4. Search for the risky settings. From your project's root folder:

grep -snE 'bypassPermissions|"Bash"|Bash\(\*\)|git push|additionalDirectories|enableAllProjectMcpServers' ~/.claude/settings.json .claude/settings.json .claude/settings.local.json

No output means none of them is set in these three files. Each line it prints is a file and a line number to read. A git push rule under ask or deny is fine. Under allow, it is not.

Common mistakes

Each of these is one line in a settings file.

Turning on bypassPermissions. A defaultMode of "bypassPermissions" in any of the three files. Remove it, or set it to "default". Checked by DeepSweep.

Allowing every shell command. "Bash" or "Bash(*)" in allow lets Claude Code run any command without asking, including one it picked up from a file or page it read. Allow only the commands you need, such as "Bash(npm run test *)". Checked by DeepSweep.

Allowing git push. "Bash(git push *)" in allow, or a rule wide enough to include it, such as "Bash(git *)", lets Claude Code publish code to a shared branch before you have read it. The same goes for commands that publish, deploy or delete, such as npm publish, terraform apply or rm -rf. Move them to ask. Checked by DeepSweep.

Giving it your home folder. additionalDirectories lets Claude Code read files outside the project without asking, and edit them as your mode allows. An entry of ~, your home folder or / hands over every file you own, including keys and other projects. List only the folders the project needs, such as ../shared-lib. Checked by DeepSweep, for your home folder and the whole disk.

Approving every project MCP server. "enableAllProjectMcpServers": true starts every server listed in a project's .mcp.json without asking. Set in your user file, it approves the servers of every repository you open, including ones you just cloned. Remove it and approve servers one at a time. Checked by DeepSweep.

No deny rules for secrets. By default Claude Code reads files inside your project without asking, .env files included. Add deny rules for the secret files and folders you have, as in the starter config above.

Related: what other agents can do without asking, in Codex, VS Code and Gemini CLI: https://deepsweep.ai/blog/ai-coding-agent-auto-approve-settings

And the checklist for the MCP servers your agents use: https://deepsweep.ai/blog/mcp-security-best-practices

DeepSweep checks the items marked “Checked by DeepSweep” locally in under a second — free, no account.

Get the free extension