← All posts
·7 min read

Claude Code Permissions Explained

Claude CodeSecurityPermissions
Claude Code Permissions Explained

Why Claude Code permissions matter

Claude Code runs real commands on your machine. It reads files, writes code, executes shell scripts, and installs packages. Without a permission system, you'd be handing an AI unrestricted access to your entire development environment.

The Claude Code permission system exists to give you control over exactly what the agent can and cannot do. It's the difference between a helpful assistant and a liability. Understanding how claude code permissions work isn't optional: it's the first thing you should configure before using Claude Code on any real project.

The permission model

Claude Code uses a layered permission system with three tiers:

  1. Allow by default: Tools like Read, Glob, and Grep that only observe your codebase. These are safe because they can't modify anything.
  2. Ask once, then allow: Tools like Write, Edit, and most Bash commands. Claude asks for permission the first time, and you can approve for the session.
  3. Always ask: Dangerous operations that require explicit approval every time. Things like rm -rf, git push --force, or running unknown scripts.

When Claude wants to use a tool, it checks your permission configuration. If the tool is allowed, it runs silently. If it needs approval, you'll see a prompt describing exactly what Claude wants to do. You can approve, deny, or approve for the rest of the session. If you are seeing a "permission denied" or "tool not allowed" message and want the exact allow list syntax to fix it, Claude Code permission errors covers the failure modes in detail.

This is fundamentally different from IDE-based AI tools that either have full access or no access. Claude Code gives you granular control at the tool level.

settings.json vs settings.local.json

Claude Code permissions are configured in two files, and understanding the difference is critical:

.claude/settings.json: Project-level settings. Checked into version control. Shared with your team. This is where you define the security baseline for the entire project.

.claude/settings.local.json: Personal overrides. Git-ignored. Your individual preferences that don't affect teammates.

The structure is identical:

{
  "permissions": {
    "allow": [
      "Read",
      "Glob",
      "Grep",
      "Bash(git status:*)",
      "Bash(npm test:*)",
      "Bash(npx tsc:*)"
    ],
    "deny": [
      "Bash(rm -rf:*)",
      "Bash(git push --force:*)",
      "Bash(curl * | bash:*)"
    ]
  }
}

The allow list defines tools Claude can use without asking. The deny list defines tools that are blocked entirely: Claude can't use them even if you approve.

Precedence rule: deny always wins. If something appears in both allow and deny, it's denied. And settings.local.json merges with settings.json: your local deny list adds to the project deny list, never subtracts from it.

This means a team lead can set security boundaries in settings.json that individual developers can't override. You can make your personal config more restrictive, but never less.

Tool allowlists in detail

The allowed tools syntax supports pattern matching, which is where the real power lives:

{
  "permissions": {
    "allow": [
      "Read",
      "Write",
      "Edit",
      "Bash(git:*)",
      "Bash(npm:*)",
      "Bash(node:*)",
      "Bash(npx jest:*)",
      "WebSearch"
    ]
  }
}
  • "Read": Allows the Read tool with no restrictions
  • "Bash(git:*)": Allows any Bash command starting with git
  • "Bash(npm test:*)": Allows only npm test and its variants
  • "Bash(npx jest:*)": Allows Jest but not arbitrary npx commands

The pattern syntax is ToolName(command_prefix:*). The asterisk matches anything after the prefix. This lets you allow git status, git diff, and git log with a single Bash(git:*) rule while still blocking Bash(curl:*).

For claude code security, start restrictive and widen as needed. It's much safer to add permissions when Claude asks for them than to start with everything open.

PreToolUse hooks: runtime enforcement

Allowlists handle the common cases. Hooks handle everything else.

PreToolUse hooks run before Claude executes any tool. They can inspect the tool name, the arguments, and the full context, then allow, block, or modify the action.

Here's a hook that prevents writes to production config files:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Write|Edit",
        "command": "bash .claude/hooks/block-prod-config.sh"
      }
    ]
  }
}

And the hook script:

#!/bin/bash
# block-prod-config.sh
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')

if [[ "$FILE" == *"production"* ]] || [[ "$FILE" == *".env"* ]]; then
  echo '{"decision": "block", "reason": "Cannot modify production config files"}'
  exit 0
fi

echo '{"decision": "allow"}'

This is more powerful than static allowlists because you can implement arbitrary logic. Check file paths, validate JSON schemas, enforce naming conventions, require test files to exist before allowing source file changes, anything you can express in a shell script.

PreToolUse hooks are the answer to "but what if Claude tries to..." questions. Whatever the concern, you can write a hook to prevent it.

Dangerous command blocking

Claude Code has built-in protection against destructive operations. Certain commands are flagged as dangerous and always require explicit approval, regardless of your allowlist configuration:

  • rm -rf with broad paths
  • git push --force or git push -f
  • git reset --hard
  • git clean -f
  • Commands piped from curl or wget to bash
  • chmod 777 on sensitive paths
  • sudo commands

Even if you put Bash(rm:*) in your allow list, Claude will still flag rm -rf / as dangerous. The built-in safeguards can't be overridden by configuration: they're hardcoded protections that exist as a last line of defense.

This layered approach means security doesn't depend on any single mechanism. Even if your allowlist is too permissive, the dangerous command detection catches the worst cases.

Command-level permissions in custom commands

When you create custom commands, you can scope their permissions independently:

---
description: Run database migration
allowed-tools:
  - Bash(npx prisma migrate:*)
  - Read
---

Run the pending database migrations and verify the schema is correct.

The allowed-tools field in the command frontmatter restricts which tools are available when that command runs. This is a security boundary: a /deploy command doesn't need Write access, and a /review command doesn't need Bash access beyond git.

This follows the principle of least privilege at the command level. Each workflow gets exactly the tools it needs and nothing more.

Security best practices

1. Start with a minimal allowlist. Only allow Read, Glob, and Grep by default. Add tools as Claude requests them and you verify they're needed.

2. Use deny lists for hard boundaries. If there are commands that should never run, like force-pushing to main or deleting production databases, put them in your deny list explicitly.

3. Scope Bash permissions narrowly. Bash(git:*) is better than Bash(*). Bash(git status:*) is better than Bash(git:*). Be as specific as your workflow allows.

4. Use PreToolUse hooks for file-path restrictions. Allowlists can't distinguish between writing to src/ and writing to .env. Hooks can.

5. Commit settings.json, gitignore settings.local.json. Team security baselines should be shared. Personal preferences should stay personal.

6. Review permission prompts carefully. When Claude asks to run a command, read what it wants to do. The few seconds of review prevent the rare catastrophic mistake.

7. Audit your allowlist quarterly. Permissions accumulate. Review what's allowed and remove anything that's no longer needed.

Permissions for team projects

For teams, the permission system provides a governance model:

  • Project lead configures settings.json with the team's security baseline
  • Each developer customizes settings.local.json for personal preferences
  • CI/CD pipelines use a locked-down configuration with only the tools needed for automated tasks
  • Subagents inherit the parent session's permissions, plus any restrictions from their command definition

This scales from solo developers to large teams without requiring a separate security tool.

FAQ

Can Claude Code access files outside my project directory?

Yes, Claude Code can read files outside your working directory using absolute paths. Use PreToolUse hooks to restrict file access to specific directories if this concerns you. A hook that checks whether the file path starts with your project root takes five lines of bash.

What happens if I deny a permission request?

Claude acknowledges the denial and finds an alternative approach. If no alternative exists, it explains what it can't do and why. Denying permissions never crashes the session: Claude is designed to work within whatever boundaries you set.

Do permissions persist between sessions?

Allowlist and deny list configurations in settings files persist permanently. Session-level approvals (when you click "allow for this session") reset when you close Claude Code. This means you re-approve one-off operations each session, which is the safer default.


Ready to configure Claude Code permissions properly from day one? Claudify ships with production-tested settings.json configurations, PreToolUse hooks for common security patterns, and 21 custom commands, all with scoped permissions. One command to install: npx create-claudify.

More like this

Ready to upgrade your Claude Code setup?

Get Claudify
Featured on Dofollow.Tools AI Toolz Dir