Settings, Permissions and Hooks: Controlling What Claude Can Do
- Configure settings.json at user and project level with the correct file precedence
- Choose the right permission mode for each type of task and know how to switch between them
- Write allow/deny rules and create hooks that enforce safety and automate formatting
Where Settings Live
Claude Code's behaviour is controlled by settings.json files. Like CLAUDE.md, there are multiple levels, each overriding the one below:
Priority (highest to lowest):
1. Managed — /Library/Application Support/ClaudeCode/managed-settings.json (macOS)
/etc/claude-code/managed-settings.json (Linux)
C:Program FilesClaudeCodemanaged-settings.json (Windows)
Set by IT/DevOps. Cannot be overridden. Use for org-wide policy.
2. Local — ./.claude/settings.local.json
Your personal overrides. Gitignore this file.
3. Project — ./.claude/settings.json
Team-shared settings. Commit this to git.
4. User — ~/.claude/settings.json
Your defaults across all projects.
Start by creating .claude/settings.json in your project root. This is where most configuration lives.
The Six Permission Modes
Permission modes control how much Claude asks before taking action. Choosing the right mode for the right task is one of the most practical skills in this course.
| Mode | Auto-approves | Use when |
|---|---|---|
default |
File reads only | New projects, sensitive code, first time on a codebase |
acceptEdits |
Reads + file edits + safe filesystem commands | Active development, refactoring, feature iteration |
plan |
Reads only (writes blocked entirely) | Risky changes: migrations, infrastructure, destructive refactors |
auto |
ML classifier decides per action | Long autonomous tasks, overnight work (Max/Team/Enterprise only) |
dontAsk |
Only pre-approved tools in your allow list | CI/CD pipelines, automated scripts, headless use |
bypassPermissions |
Everything, no prompts at all | Isolated containers and VMs only — never your local machine |
How to switch modes: Press Shift+Tab in the CLI to cycle through default → acceptEdits → plan. Or set it at startup:
claude --permission-mode acceptEdits
claude --permission-mode plan
Or set a default in settings.json:
{
"permissions": {
"defaultMode": "acceptEdits"
}
}
Allow and Deny Rules
For fine-grained control, use permissions.allow and permissions.deny lists. These specify exactly which tools and commands Claude can use without asking, and which it can never use.
Syntax
{
"permissions": {
"allow": [
"Bash(npm run *)", // Any npm run script
"Bash(git status)", // Git status (exact match)
"Bash(git diff *)", // Git diff with any args
"Read(src/**)", // Read anything in src/
"Edit(src/**)", // Edit files in src/
"Edit(tests/**)" // Edit files in tests/
],
"deny": [
"Bash(rm -rf *)", // No bulk deletions
"Bash(git push *)", // No auto-push to remote
"Bash(curl *)", // No arbitrary downloads
"Bash(wget *)", // No arbitrary downloads
"Bash(npm install *)", // Must approve new packages
"Read(.env)", // Secrets off-limits
"Read(.env.*)", // All .env variants
"Read(secrets/**)" // Secrets directory
]
}
}
What gets blocked vs what gets asked
Denied tools are silently blocked — Claude will be told it cannot use them and must find another approach. Allowed tools run without any prompt. Everything else triggers a permission request where you can approve or deny in the moment.
A practical starting configuration for most web projects:
{
"permissions": {
"allow": [
"Bash(npm run test)",
"Bash(npm run lint)",
"Bash(npm run build)",
"Bash(git status)",
"Bash(git diff)",
"Bash(git diff *)",
"Bash(git log *)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push *)",
"Bash(curl *)",
"Read(.env*)",
"Read(secrets/**)"
]
}
}
Protected Paths
Regardless of permission mode or allow rules, Claude Code always treats certain paths with extra caution. These never auto-approve in default or acceptEdits mode:
.git/and.gitignore.claude/(except the commands and skills subdirectories).env,.env.*- Shell initialization files:
.bashrc,.zshrc,.profile - IDE configuration:
.vscode/,.idea/ .mcp.json
Add your own sensitive paths to the deny list for project-specific protection.
The Hooks System
Hooks execute shell commands at specific lifecycle events in a Claude Code session. They are your most powerful tool for automating safety, formatting, and observability.
Available hook events
- SessionStart — Runs once when the session begins. Use it to print a status summary, check git state, or set up environment variables.
- UserPromptSubmit — Runs before Claude processes your prompt. Can validate or modify the prompt before Claude sees it.
- PreToolUse — Runs before any tool executes. Can inspect the tool and its arguments, and block execution by returning a non-zero exit code.
- PostToolUse — Runs after a tool succeeds. Perfect for auto-formatting edited files, running linters, or logging.
- Stop — Runs when the session ends. Good for cleanup or audit logging.
Hook structure in settings.json
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/validate-bash.py",
"timeout": 5000
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write $FILE"
}
]
}
]
}
}
The matcher is a regular expression matched against the tool name. $FILE is replaced with the path of the file being operated on.
Three Practical Hook Examples
Hook 1: Auto-format every file Claude edits
Instead of manually running prettier after every Claude edit, let a hook do it automatically:
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write "$FILE" 2>/dev/null; exit 0"
}
]
}
]
}
}
Now every file Claude touches gets formatted to your project's style automatically, even if Claude's output was not perfectly formatted.
Hook 2: Block dangerous bash commands
Save this as ~/.claude/hooks/validate-bash.py and make it executable (chmod +x):
#!/usr/bin/env python3
import json, sys
data = json.load(sys.stdin)
command = data.get("tool_input", {}).get("command", "")
dangerous_patterns = [
"rm -rf",
"git push --force",
"DROP TABLE",
"DROP DATABASE",
":(){:|:&;:}", # fork bomb
]
for pattern in dangerous_patterns:
if pattern in command:
result = {"decision": "block", "reason": f"Blocked dangerous pattern: {pattern}"}
print(json.dumps(result))
sys.exit(0)
# Allow the command
print(json.dumps({"decision": "allow"}))
Register it in settings.json under PreToolUse with "matcher": "Bash". Claude will be told the command was blocked and must find an alternative approach.
Hook 3: Audit log of all changes
Save this as ~/.claude/hooks/audit-log.sh:
#!/bin/bash
echo "$(date -u '+%Y-%m-%d %H:%M:%S UTC') | TOOL: $CLAUDE_TOOL | FILE: $FILE | USER: $USER | DIR: $PWD"
>> ~/.claude-audit.log
Register it under PostToolUse with "matcher": "Edit|Write|Bash". Now every action Claude takes is logged with a timestamp. Useful for tracking what changed in a long session and for team accountability.
A Complete settings.json for a Web Project
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"model": "claude-sonnet-4-6",
"permissions": {
"defaultMode": "acceptEdits",
"allow": [
"Bash(npm run test)",
"Bash(npm run lint)",
"Bash(npm run build)",
"Bash(git status)",
"Bash(git diff)",
"Bash(git diff *)",
"Bash(git log *)",
"Bash(git add *)",
"Read(src/**)",
"Edit(src/**)",
"Edit(tests/**)"
],
"deny": [
"Bash(rm -rf *)",
"Bash(git push *)",
"Bash(git push --force *)",
"Bash(curl *)",
"Bash(wget *)",
"Bash(npm install *)",
"Read(.env)",
"Read(.env.*)",
"Read(secrets/**)"
]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "prettier --write "$FILE" 2>/dev/null; exit 0"
}
]
}
]
}
}
This configuration: uses acceptEdits mode so Claude does not ask for every file edit; pre-approves safe read and diff commands; blocks dangerous operations; and auto-formats every file Claude touches.
Checking Your Active Configuration
At any point in a session, run /status to see what settings are active, which settings files were loaded, and what the current permission mode is. Run /mcp to see connected MCP servers. These commands are your debugging tools when something is not behaving as expected.
- settings.json has 4 levels: managed (locked), local (personal), project (team), user (personal global)
- The 6 permission modes range from fully manual (default) to fully automatic (bypassPermissions) — match the mode to the risk
- Deny rules silently block tools; allow rules pre-approve them; everything else prompts
- Hooks run shell commands at lifecycle events — use them to auto-format, validate, and audit
- Use /status to inspect active settings and debug unexpected behaviour