Skip to content

Settings and Configuration

Every lesson so far has touched settings.json: hooks land there, permissions live there, skills discover their paths through it. This lesson pulls back and covers the settings system itself, where settings files live, how they compose, which one wins when they conflict, and how the permissions system (allow/ask/deny) determines what Claude can actually do in your project.

The settings system is a hierarchy, not a single file. A flag you pass on the command line overrides a local setting, which overrides a project setting, which overrides a personal default. Understanding that layering is how you avoid editing the wrong file and wondering why your change didn't take effect.

Gnome mascot

What you'll learn

  • Four settings layers, from highest to lowest priority: CLI flags > .claude/settings.local.json > .claude/settings.json > ~/.claude/settings.json
  • .claude/settings.local.json is gitignored by default, put secrets, personal preferences, and machine-specific config here
  • Permissions use allow, ask, and deny arrays keyed by tool name; --allowedTools and --disallowedTools filter at the CLI level
  • The settings schema includes hooks, permissions, model preferences, and plugin configuration, all in one JSON file per layer

The problem

You set "Bash": "ask" in .claude/settings.json so that every shell command requires your approval before it runs. It works. You commit the file. Your teammate pulls it down, runs Claude, and suddenly their Claude is asking for permission on every Bash call too. They didn't want that. They shouldn't have to edit a committed file to get their preferred behavior back.

Or the reverse: you add an API key to .claude/settings.json, commit it, push it, and now your key is in the repo history. The settings system has a built-in answer to both problems: the local settings layer.

Settings composition is the mechanism that separates personal preference from team policy. The project settings file says "Bash requires approval in this repo." The local settings file says "except for this developer's machine, where git commands are pre-approved." Both files exist. Both apply. The local one wins where they conflict.

Options & when to use each

The settings hierarchy

LayerPathScopeCommitted to git?Use for
CLI flagsCommand lineThis session onlyN/AOne-off overrides, testing settings before committing them
Local settings.claude/settings.local.jsonThis machine, this projectNo (gitignored by default)Personal preferences, secrets, API keys, machine-specific tool paths
Project settings.claude/settings.jsonAll contributors, this projectYesTeam-wide policy: hooks, permissions, model defaults, plugin config
Personal settings~/.claude/settings.jsonAll projects, this machineNoGlobal defaults: preferred model, default effort level, personal hooks

When a setting appears in multiple layers, the highest-priority layer wins. If .claude/settings.json says "model": "sonnet" and ~/.claude/settings.json says "model": "opus", Claude uses Sonnet, the project layer overrides the personal layer. If you then run claude --model haiku, Haiku wins, the CLI flag overrides everything.

A setting absent from a higher layer falls through to the lower layer. If .claude/settings.json sets hooks but says nothing about model, the model setting from ~/.claude/settings.json (or the built-in default) applies. Each setting is resolved independently, it's not an all-or-nothing file replacement.

Permissions

The permissions system controls which tools Claude can use and under what conditions. It uses three arrays in settings.json:

json
{
  "permissions": {
    "allow": ["Read", "Write", "Edit", "Grep", "Glob"],
    "ask": ["Bash", "WebSearch", "WebFetch"],
    "deny": ["Bash(git push:*)", "Bash(rm -rf *)"]
  }
}
PermissionBehaviorUse for
allowClaude can use the tool without askingSafe, read-only operations: Read, Grep, Glob
askClaude must request approval each timePotentially destructive operations: Bash, Write on production files
denyClaude cannot use the tool at allDangerous commands, tools that don't apply to this project

Tool names can include parenthesized argument patterns for fine-grained control. "Bash(git push:*)" denies any Bash call that starts with git push, while "Bash(git status)" allows git status specifically. The * wildcard matches any remaining arguments.

The CLI provides shorthand for permissions:

bash
# Allow only specific tools
claude --allowedTools "Read,Write,Edit,Grep,Bash(git status)"

# Deny specific tools
claude --disallowedTools "Bash(curl *),Bash(wget *)"

CLI-level permissions override all file-based permissions for that session. If you --disallowedTools "Bash", no Bash call runs regardless of what any settings file says.

Build it

Step 1: Layer personal over project settings

Create your personal defaults in ~/.claude/settings.json:

json
{
  "model": "sonnet",
  "effort": "auto",
  "permissions": {
    "allow": ["Read", "Write", "Edit", "Grep", "Glob"],
    "ask": ["Bash", "WebSearch", "WebFetch"],
    "deny": []
  }
}

Then in a specific project, create .claude/settings.json:

json
{
  "model": "opus",
  "effort": "high",
  "permissions": {
    "allow": ["Read", "Write", "Edit", "Grep", "Glob"],
    "ask": ["Bash", "WebSearch", "WebFetch"],
    "deny": ["Bash(rm -rf *)", "Bash(sudo *)"]
  }
}

When you run Claude in this project, it uses Opus with high effort (project settings win) but still asks before running Bash commands (merged permissions). When you run Claude in any other project, it uses Sonnet with auto effort (personal defaults).

Step 2: Add machine-specific overrides

Create .claude/settings.local.json in the project:

json
{
  "permissions": {
    "allow": ["Bash(git status)", "Bash(git diff)", "Bash(git log *)", "Bash(npm test)", "Bash(npm run lint)"],
    "deny": []
  }
}

This file is gitignored. It pre-approves safe git and npm commands on your machine without changing the team-wide policy. Your teammates can add their own pre-approvals in their own local settings files without merge conflicts.

The merged permissions for your session become:

  • Project .claude/settings.json denies Bash(rm -rf *) and Bash(sudo *), asks on all other Bash
  • Local .claude/settings.local.json allows specific git and npm commands
  • Result: git status runs silently, npm test runs silently, git push prompts for approval, rm -rf / is blocked

Step 3: One-off CLI overrides

Test a settings change without committing it:

bash
# Test Opus on a project that defaults to Sonnet
claude --model opus --effort max

# Test Haiku for a CI script without changing any files
claude --model haiku --allowedTools "Read,Write,Edit,Bash(npm test)" -p "Fix lint errors"

If the override works, add it to the appropriate settings file. If it doesn't, the CLI flag leaves no trace.

Step 4: Verify the effective settings

There is no single command that prints the merged, effective settings for a project. To verify which layer a setting is coming from, read each file in priority order:

bash
# Check personal defaults
cat ~/.claude/settings.json 2>/dev/null || echo "No personal settings"

# Check project settings
cat .claude/settings.json 2>/dev/null || echo "No project settings"

# Check local overrides
cat .claude/settings.local.json 2>/dev/null || echo "No local settings"

The highest-priority file that sets a given key is the effective value. If you override model at the CLI, none of these files matter for that setting.

What goes wrong

MistakeHow you notice itThe fix
Edited the wrong settings fileChanged a setting in .claude/settings.json but Claude still behaves the old wayCheck ~/.claude/settings.json , your personal defaults are overriding the project setting. Or check .claude/settings.local.json , a local override has higher priority
Committed secrets to project settingsAPI key shows up in git historyMove secrets to .claude/settings.local.json (gitignored). Rotate the exposed key. Use git filter-branch or BFG Repo-Cleaner to scrub it from history
Invalid JSON breaks the entire fileClaude ignores all settings from that file, not just the broken lineValidate before saving: cat .claude/settings.json | python3 -m json.tool. JSON is strict, trailing commas, unquoted keys, and comments are all syntax errors
Permission deny set too broadlyClaude can't complete tasks because it can't run any commands"deny": ["Bash"] blocks every shell command, including git status, npm test, and ls. Use targeted denies with parenthesized patterns: "Bash(rm *)"
allow and deny both match the same tool callBehavior is unpredictable, Claude might allow, deny, or ask depending on internal orderingThe permissions system resolves conflicts by specificity: Bash(git push) is more specific than Bash, so an explicit allow on Bash(git push:*backtick) takes priority over a broad deny on Bash. Test ambiguous cases

Confirm it worked

Set up a layered configuration and verify the hierarchy resolves correctly:

bash
# 1. Create personal defaults
mkdir -p ~/.claude
echo '{"model": "sonnet", "effort": "auto"}' > ~/.claude/settings.json

# 2. Create project settings that override model but not effort
mkdir -p .claude
echo '{"model": "opus"}' > .claude/settings.json

# 3. Verify: check each file
cat ~/.claude/settings.json
# Expected: {"model": "sonnet", "effort": "auto"}

cat .claude/settings.json
# Expected: {"model": "opus"}

# 4. Effective settings: model=opus (project wins), effort=auto (falls through to personal)

# 5. Test CLI override
claude --model haiku -p "What model are you running on?" --output-format text
# Expected: Claude responds on Haiku, confirming CLI overrides both files

# 6. Clean up project settings
rm .claude/settings.json
rm ~/.claude/settings.json

Then test permissions with allowed and denied patterns:

bash
# Create a settings file that denies force push but allows safe git commands
cat > .claude/settings.json << 'JSONEOF'
{
  "permissions": {
    "allow": ["Read", "Write", "Edit", "Grep", "Glob", "Bash(git status)", "Bash(git diff *)"],
    "ask": ["Bash"],
    "deny": ["Bash(git push --force *)", "Bash(git push -f *)"]
  }
}
JSONEOF

# Test allowed command
claude -p "Run: git status" --output-format text
# Expected: runs without asking (in allow list)

# Test denied command - will be blocked
claude -p "Run: git push --force origin main" --output-format text
# Expected: Claude reports the command is denied or asks for approval

# Clean up
rm .claude/settings.json

If step 4 confirms the project setting overrides the personal default while the untouched setting falls through, and the permissions test shows git status running freely while git push --force is blocked, the settings hierarchy and permissions system are working as designed.

Next: Day 3, Subagents & Teams, where Claude Code starts coordinating parallel workstreams with subagents.