Appearance
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.

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.jsonis gitignored by default, put secrets, personal preferences, and machine-specific config here- Permissions use
allow,ask, anddenyarrays keyed by tool name;--allowedToolsand--disallowedToolsfilter 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
| Layer | Path | Scope | Committed to git? | Use for |
|---|---|---|---|---|
| CLI flags | Command line | This session only | N/A | One-off overrides, testing settings before committing them |
| Local settings | .claude/settings.local.json | This machine, this project | No (gitignored by default) | Personal preferences, secrets, API keys, machine-specific tool paths |
| Project settings | .claude/settings.json | All contributors, this project | Yes | Team-wide policy: hooks, permissions, model defaults, plugin config |
| Personal settings | ~/.claude/settings.json | All projects, this machine | No | Global 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 *)"]
}
}| Permission | Behavior | Use for |
|---|---|---|
allow | Claude can use the tool without asking | Safe, read-only operations: Read, Grep, Glob |
ask | Claude must request approval each time | Potentially destructive operations: Bash, Write on production files |
deny | Claude cannot use the tool at all | Dangerous 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.jsondeniesBash(rm -rf *)andBash(sudo *), asks on all other Bash - Local
.claude/settings.local.jsonallows specific git and npm commands - Result:
git statusruns silently,npm testruns silently,git pushprompts 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
| Mistake | How you notice it | The fix |
|---|---|---|
| Edited the wrong settings file | Changed a setting in .claude/settings.json but Claude still behaves the old way | Check ~/.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 settings | API key shows up in git history | Move 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 file | Claude ignores all settings from that file, not just the broken line | Validate 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 broadly | Claude 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 call | Behavior is unpredictable, Claude might allow, deny, or ask depending on internal ordering | The 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.jsonThen 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.jsonIf 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.