Tips & Tricks (Updated: 6/1/2026)

Complete Guide to Claude Code Permissions | settings.json, Hooks & Allowlist Explained

Claude Code permissions: allow/deny/ask, settings.json, PreToolUse hooks, managed settings, and safe budgets with working code.

Complete Guide to Claude Code Permissions | settings.json, Hooks & Allowlist Explained

Claude Code has extremely powerful file operation and command execution capabilities. Permissions are what allow you to safely control that power. Let’s move beyond “using it without really thinking” and design a Claude Code that works exactly as intended.

This article provides a thorough, working-code walkthrough of every setting in .claude/settings.json, Hook implementation patterns, and environment-specific permission design.

Overview of Permissions

Claude Code permissions are controlled at 3 levels.

LevelKeyBehavior
AllowallowExecutes automatically without a confirmation dialog
AskaskRequires user approval each time
DenydenyCannot be executed at all (blocked with an error)

Settings are officially managed through settings.json. Use ~/.claude/settings.json for user-wide settings, .claude/settings.json for team-shared project settings, and .claude/settings.local.json for personal overrides. Some state, such as MCP configuration, can still live in ~/.claude.json, but permission rules should be discussed as settings.json configuration.

Priority (highest first):
Managed settings
    > Command-line arguments
        > Local .claude/settings.local.json
            > Project .claude/settings.json
                > User ~/.claude/settings.json

Permission rules merge across scopes and evaluate in deny -> ask -> allow order

Basic Structure of settings.json

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Glob(**)",
      "Grep(**)",
      "Bash(npm run *)"
    ],
    "deny": [
      "Bash(rm -rf *)",
      "Bash(git push --force*)"
    ],
    "ask": [
      "Write(**)",
      "Edit(**)",
      "Bash(git commit*)"
    ]
  },
  "hooks": {
    "PreToolUse": [],
    "PostToolUse": []
  }
}

Tool Names and Pattern Syntax

Permissions are written in the format “ToolName(argument pattern)”.

Main Tool List

Tool NameDescription
ReadFile reading
WriteCreating new files
EditPartial modification of existing files
BashShell command execution
GlobFile pattern search
GrepContent search
WebFetchURL fetching
AgentSub-agent launch

Pattern Syntax

"Read(**)"          // Allow reading all files
"Read(src/**)"      // Allow only under src/
"Read(*.md)"        // Allow only .md files
"Bash(npm run *)"   // Allow only commands starting with npm run
"Bash(git *)"       // Allow all git commands
"Bash(rm -rf *)"    // Deny rm -rf

** matches all paths including directories; * matches a single segment.

Practical Patterns

Pattern 1: Solo Development (relatively permissive)

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Glob(**)",
      "Grep(**)",
      "Bash(npm *)",
      "Bash(git log*)",
      "Bash(git diff*)",
      "Bash(git status*)",
      "Bash(git add*)",
      "Bash(node *)",
      "Bash(echo *)",
      "Bash(cat *)",
      "Bash(ls *)"
    ],
    "deny": [
      "Bash(rm -rf /)",
      "Bash(rm -rf ~*)",
      "Bash(git push --force *main*)",
      "Bash(git push --force *master*)"
    ],
    "ask": [
      "Write(**)",
      "Edit(**)",
      "Bash(git commit*)",
      "Bash(git push*)",
      "Bash(rm *)"
    ]
  }
}

Pattern 2: Team Development (security-focused)

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Glob(**)",
      "Grep(**)",
      "Bash(npm run lint)",
      "Bash(npm run test)",
      "Bash(npm run typecheck)",
      "Bash(git log*)",
      "Bash(git diff*)",
      "Bash(git status*)",
      "Bash(git branch*)"
    ],
    "deny": [
      "Bash(rm -rf*)",
      "Bash(git push --force*)",
      "Bash(git push -f*)",
      "Bash(git reset --hard*)",
      "Bash(git rebase *main*)",
      "Bash(git rebase *master*)",
      "Bash(DROP *)",
      "Bash(TRUNCATE *)",
      "Bash(curl * | bash)",
      "Bash(wget * | sh)"
    ],
    "ask": [
      "Write(**)",
      "Edit(**)",
      "Bash(git commit*)",
      "Bash(git push*)",
      "Bash(git add*)",
      "Bash(npm install*)",
      "Bash(*deploy*)"
    ]
  }
}

Pattern 3: Production Environment (read-only)

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Glob(**)",
      "Grep(**)",
      "Bash(git log*)",
      "Bash(git diff*)",
      "Bash(git status*)",
      "Bash(git show*)",
      "Bash(cat *)",
      "Bash(ls *)",
      "Bash(ps *)",
      "Bash(df *)",
      "Bash(top *)"
    ],
    "deny": [
      "Write(**)",
      "Edit(**)",
      "Bash(git push*)",
      "Bash(git commit*)",
      "Bash(git reset*)",
      "Bash(rm *)",
      "Bash(mv *)",
      "Bash(*deploy*)",
      "Bash(*restart*)",
      "Bash(*kill *)"
    ],
    "ask": []
  }
}

For production-like worktrees or repositories, place this as the active .claude/settings.json and start with claude --permission-mode plan or a pre-approved dontAsk setup. That stays closer to the official scope and permission-mode model than relying on an alternate settings file swapped by an environment variable.

Pattern 4: Content Generation Only (the pattern used on this site)

{
  "permissions": {
    "allow": [
      "Read(**)",
      "Glob(**)",
      "Grep(**)",
      "Write(site/src/content/**)",
      "Write(content/**)",
      "Edit(site/src/content/**)",
      "Edit(content/**)",
      "Bash(git log*)",
      "Bash(git diff*)",
      "Bash(git status*)",
      "Bash(node scripts/*)",
      "Bash(QIITA_TOKEN=* node scripts/qiita-publish.mjs)"
    ],
    "deny": [
      "Bash(rm -rf*)",
      "Bash(git push --force*)",
      "Edit(.env*)",
      "Read(.env*)"
    ],
    "ask": [
      "Bash(git add*)",
      "Bash(git commit*)",
      "Bash(git push*)",
      "Bash(bash scripts/deploy.sh*)"
    ]
  }
}

The key is restricting writes to a specific directory like Write(site/src/content/**).

Hooks: Running Processes Before and After Permissions

Hooks are a mechanism that automatically runs commands before and after tool execution. They can be used for security checks and auto-formatting.

PreToolUse: Pre-execution Hook

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(git add*)",
        "hooks": [{
          "type": "command",
          "command": "git diff --cached --name-only | grep -E '^\\.env' && echo '🚨 .env addition detected!' && exit 1 || exit 0"
        }]
      },
      {
        "matcher": "Bash(git commit*)",
        "hooks": [{
          "type": "command",
          "command": "node scripts/secret-scan.mjs"
        }]
      },
      {
        "matcher": "Bash(rm*)",
        "hooks": [{
          "type": "command",
          "command": "echo '⚠️ Delete command detected. Executing in 5 seconds. Press Ctrl+C to abort.' && sleep 5"
        }]
      }
    ]
  }
}

If a hook command returns exit code 1, the tool execution is blocked. This is the most important point.

PostToolUse: Post-execution Hook

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [{
          "type": "command",
          "command": "npx tsc --noEmit 2>&1 | head -20 || true"
        }]
      },
      {
        "matcher": "Bash(git commit*)",
        "hooks": [{
          "type": "command",
          "command": "git log --oneline -3"
        }]
      }
    ]
  }
}

PostToolUse is used for post-execution checks and side effects—for example, automatically running type checks after file edits or showing the latest 3 log entries after a commit.

Practical Hooks Recipe Collection

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash(npm install*)",
        "hooks": [{
          "type": "command",
          "command": "echo '📦 Adding package. Please check package.json.'"
        }]
      },
      {
        "matcher": "Bash(*deploy*)",
        "hooks": [{
          "type": "command",
          "command": "read -p '🚀 About to deploy. Continue? [y/N] ' ans && [ \"$ans\" = 'y' ] || exit 1"
        }]
      }
    ],
    "PostToolUse": [
      {
        "matcher": "Write(*.ts)|Edit(*.ts)",
        "hooks": [{
          "type": "command",
          "command": "npx eslint --fix $CLAUDE_TOOL_INPUT_FILE_PATH 2>/dev/null || true"
        }]
      }
    ]
  }
}

Permission Modes: Permission Level at Launch

You can also specify the mode when launching the claude command.

# Normal mode (follows settings.json)
claude

# Auto-approve all operations (dangerous! only for trusted environments)
claude --dangerously-skip-permissions

# Skip only specific operations
claude --allowedTools "Read,Grep,Glob"

# Non-interactive mode (used in CI/CD)
claude -p "Run tests and report results" --dangerously-skip-permissions

--dangerously-skip-permissions should only be used for CI/CD automation or fully understood automation scripts, and avoided in daily interactive use.

2026: Safe Permission Budget and Team Review

In this article, permission budget is not an official feature name. It means the narrow set of actions you allow Claude Code to run automatically during the first week of real use. As of June 1, 2026, the official docs describe permission rules as evaluating in deny -> ask -> allow order. PreToolUse hooks can add runtime checks, but hook output does not override matching deny or ask rules. That means the first budget should optimize for reversibility, not speed.

A safe starter budget is deliberately small. Put read and verification operations in allow: Read, Glob, Grep, git status, git diff, git log, npm run lint, and npm run test. Put intent-changing operations in ask: Edit, Write, git add, git commit, git push, npm install, and anything deploy-related. Put irreversible or sensitive operations in deny: .env, secrets/**, rm -rf, git reset --hard, git push --force, curl | bash, wget | sh, and commands that touch production databases. Use bypassPermissions only inside disposable containers, VMs, or devcontainers where damage is isolated.

For a team, treat .claude/settings.json like application code. Review every permission change in a PR. The reviewer should ask three questions: is every allow rule safe to run repeatedly, does every ask rule represent a real human-intent checkpoint, and do deny rules cover secrets, forced pushes, deletes, and production operations? In managed organizations, admins can enforce a stricter baseline with server-managed settings: set disableBypassPermissionsMode and disableAutoMode to "disable", then use allowManagedPermissionRulesOnly when local or project permission rules should not be accepted.

A dangerous failure pattern is allowing broad shell access, such as Bash(git *) or Bash(node *), while assuming that Read(.env*) is enough to protect secrets. The official docs note that Read and Edit deny rules apply to Claude Code file tools and recognized file commands, but they do not stop arbitrary subprocesses, such as a Node or Python script, from opening files directly. If secrets matter, avoid broad Bash allows and combine permission rules with sandboxing, OS-level file permissions, and PreToolUse checks.

Decide the rollback path before the incident. If an unwanted edit lands, open /permissions to see the active rule and source, inspect git diff, and revert with git restore -p so you keep good hunks. For untracked files, run git clean -n before deleting anything. If a secret may have been read or pasted into a transcript, fix the rule and rotate the token; changing .claude/settings.json after exposure is not enough.

If you want to turn this article into a working repo policy, start with the permission budget loop and the permission audit checklist. For self-serve templates, compare the Claude Code product guides. For team rollout, permission review, and safe onboarding, the consultation page is the cleaner next step than guessing a policy alone.

Configuration File Priority and Overrides

When multiple configuration files exist:

~/.claude/settings.json     ← User (shared across all projects)
.claude/settings.json       ← Project (git-managed)
.claude/settings.local.json ← Local (personal override, gitignored)
managed-settings.json       ← Managed (organization policy, highest priority)

Permission rules merge, and deny is evaluated before allow

Write personal additional settings in .claude/settings.local.json and add it to gitignore. To prevent team deny lists from being overridden by personal settings, the safe design is to only write deny rules in settings.json.

# Add to .gitignore
.claude/settings.local.json

Top 5 Pitfalls

1. Getting wildcard patterns wrong

// ❌ This matches only the single command "git"
"Bash(git)"

// ✅ Matches git followed by arguments too
"Bash(git *)"
"Bash(git*)"  // Also works without space, but explicit * is safer

2. Forgetting that deny takes priority over ask

// With this config, Bash(rm -rf /tmp/test) is caught by deny and blocked
// It never reaches ask
{
  "deny": ["Bash(rm -rf*)"],
  "ask": ["Bash(rm*)"]  // ← rm -rf is handled by deny
}

3. Not paying attention to hook exit codes

# If the PreToolUse hook command always returns exit 0,
# scanning failures won't block execution

# ❌ Passes even on error
"command": "node scan.mjs"

# ✅ Explicitly control exit code
"command": "node scan.mjs || exit 1"

4. Accidentally gitignoring settings.json

Some teams accidentally add settings.json—which they want to share—to .gitignore. The correct approach is project settings under git, only settings.local.json in gitignore.

5. Forgetting to manually switch the production configuration

# ❌ Working on production with everyday settings

# ✅ Start production work in plan mode and keep the worktree .claude/settings.json production-safe
claude --permission-mode plan

Registering an alias makes it harder to forget:

# ~/.bashrc or ~/.zshrc
alias claude-prod='claude --permission-mode plan'

Debugging Your Configuration

When it’s unclear “why is this command being blocked”:

# Check current settings
claude --print-settings 2>/dev/null || cat .claude/settings.json

# Check which rule is matching (verbose mode)
claude --verbose -p "Run git push"

Summary: Best Practices for Permission Design

1. Start with deny
   → List commands you never want executed
   → rm -rf, git push --force, DROP TABLE are essential

2. Then configure ask
   → Write operations and deploy operations that need confirmation

3. Allow everything else
   → Read operations and CI operations: allow all for efficiency

4. Automate security with Hooks
   → Pre-commit scanning, auto type-check after edits

5. Prepare environment-specific config files
   → settings.json (development), settings.production.json (production)

With proper permission settings, you’ll stop mechanically clicking approve buttons and be able to focus only on operations that truly need review. Spend 30 minutes designing this upfront and hundreds of future work hours will be safer.

References

#claude-code #permissions #security #hooks #settings #configuration
Free

Free PDF: Claude Code Cheatsheet

Enter your email and download the one-page Claude Code cheatsheet for commands, review habits, and safe workflows.

We handle your data with care and never send spam.

Level up your Claude Code workflow

Start with the free PDF, use Gumroad guides when you need repeatable workflows, and book consultation when rollout or revenue paths need human judgment.

Masa

About the Author

Masa

Engineer focused on practical Claude Code workflows. Runs claudecode-lab.com, a 10-language technical media site.