Tips & Tricks

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

A complete guide to Claude Code permissions. Learn allow/deny/ask, automation with Hooks, environment-specific settings.json, and practical patterns—all with working code.

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 written in .claude/settings.json. Placing it at the project root lets the team share it via git, while placing it in ~/.claude.json makes it a global setting.

Priority (highest first):
Project .claude/settings.json
    > Global ~/.claude.json
        > Default (everything is ask)

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": []
  }
}

In production, specify this with CLAUDE_SETTINGS=.claude/settings.production.json claude.

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.

Configuration File Priority and Overrides

When multiple configuration files exist:

~/.claude.json              ← Global (shared across all projects)
    +
.claude/settings.json       ← Project (git-managed)
    +
.claude/settings.local.json ← Personal overrides (recommend gitignore)
    =
Merged settings are applied

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

# ✅ Explicitly switch settings before production work
CLAUDE_SETTINGS=.claude/settings.production.json claude

Registering an alias makes it harder to forget:

# ~/.bashrc or ~/.zshrc
alias claude-prod='CLAUDE_SETTINGS=.claude/settings.production.json claude'

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

Level up your Claude Code workflow

50 battle-tested prompt templates you can copy-paste into Claude Code right now.

Free

Free PDF: Claude Code Cheatsheet in 5 Minutes

Just enter your email and we'll send you the single-page A4 cheatsheet right away.

We handle your data with care and never send spam.

Masa

About the Author

Masa

Engineer obsessed with Claude Code. Runs claudecode-lab.com, a 10-language tech media with 2,000+ pages.