Compositions
Settings don't exist in isolation. They interact with hooks, CLAUDE.md, MCP servers, subagents, and skills to form the complete Claude Code control surface. Understanding these interactions is essential for building configurations that actually work in production.
Settings + Hooks
Hooks are configured in settings.json under the hooks key. They integrate deeply with the permission system — but the relationship has rules that trip up even experienced users.
Permission-Aware Auto-Formatting
Auto-format every file Claude writes, without prompting:
{
"permissions": {
"allow": ["Edit", "Write"]
},
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [
{
"type": "command",
"command": "npx prettier --write \"$CLAUDE_TOOL_ARG_file_path\""
}
]
}
]
}
}The allow rule lets Claude edit files without prompts. The hook runs Prettier after every edit. Both live in the same settings file, enforced at different layers.
PreToolUse Blocking
Block dangerous SQL operations before they reach Bash:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash -c 'INPUT=$(cat); CMD=$(echo $INPUT | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -iE \"(drop table|truncate|delete from)\" > /dev/null; then echo \"Blocked SQL write\" >&2; exit 2; fi; exit 0'"
}
]
}
]
}
}The Permission-Hook Layering Rule
Hook exit codes:
| Exit Code | Effect |
|---|---|
| 0 | Allow the action |
| 2 | Block the action (stderr sent as feedback to Claude) |
| Other | Allow the action (hook failure doesn't block) |
The critical interaction: hooks can block but cannot override denials. A deny rule blocks even if a hook returns "allow" (exit 0). But a hook that exits with code 2 blocks even if an allow rule would permit the action. This creates a layered system:
Deny rules → always win
Hook exit 2 → blocks even allowed tools
Allow rules → only apply if not denied or hook-blockedManaged-Only Hook Lockdown
In managed settings:
{
"allowManagedHooksOnly": true
}Only hooks from managed settings and force-enabled plugins run. User/project hooks are silently blocked. No error, no warning — the hooks simply don't fire.
Settings + CLAUDE.md
Settings and CLAUDE.md serve complementary roles:
| Layer | Controls | Enforcement |
|---|---|---|
| settings.json | What Claude can do | Hard — cannot be overridden by prompts |
| CLAUDE.md | How Claude should behave | Soft — instructions, not constraints |
Practical Pattern
Enforce the rule in settings.json, document it in CLAUDE.md:
{
"permissions": {
"deny": ["Bash(git push --force *)", "Bash(git push --force)"]
}
}# CLAUDE.md
## Git Workflow
- Never force-push (enforced via settings.json deny rules)
- Always create feature branches from main
- Use conventional commit format
- Run `npm run lint` before committing (allowed in settings.json)
- Format with Prettier (auto-run via PostToolUse hook)The deny rule is the wall. The CLAUDE.md instruction tells Claude why the wall exists and what to do instead. Both are needed — the deny rule prevents the action, the instruction prevents Claude from wasting tokens trying to work around it.
CLAUDE.md Scope Hierarchy
CLAUDE.md files have their own scope system:
| Scope | Location | Loaded When |
|---|---|---|
| User | ~/.claude/CLAUDE.md | Every session |
| Project root | CLAUDE.md or .claude/CLAUDE.md | In this project |
| Local override | CLAUDE.local.md | In this project (gitignored) |
CLAUDE.md loading can be disabled entirely via the CLAUDE_CODE_DISABLE_CLAUDE_MDS environment variable.
Settings + MCP
MCP servers are configured in ~/.claude.json (user) and .mcp.json (project) — NOT in settings.json. But settings.json controls MCP behavior and permissions.
Server Approval
{
"enableAllProjectMcpServers": true
}This auto-approves all MCP servers defined in .mcp.json. Without it, Claude prompts for approval on first use of each project-level MCP server.
Selective Server Control
{
"enabledMcpjsonServers": ["github", "memory"],
"disabledMcpjsonServers": ["filesystem"]
}MCP Server Configuration Files
MCP servers are defined in separate config files, not in settings.json:
// .mcp.json — project-level MCP servers (committed to git)
{
"mcpServers": {
"github": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@modelcontextprotocol/server-github"],
"env": { "GITHUB_TOKEN": "env:GITHUB_TOKEN" }
},
"playwright": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@playwright/mcp@latest"]
}
}
}// ~/.claude.json — user-level MCP servers
{
"mcpServers": {
"memory": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@anthropic/mcp-memory"]
},
"slack": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@anthropic/mcp-slack"],
"env": { "SLACK_TOKEN": "env:SLACK_BOT_TOKEN" }
}
}
}MCP Permission Rules
Control MCP tools through the same permission system as built-in tools:
{
"permissions": {
"allow": [
"mcp__github",
"mcp__slack__post_message"
],
"deny": [
"mcp__filesystem"
]
}
}The pattern is mcp__<server-name> for all tools from a server, or mcp__<server-name>__<tool-name> for specific tools.
Enterprise MCP Lockdown
In managed settings:
{
"allowManagedMcpServersOnly": true,
"allowedMcpServers": [
{ "serverName": "github" },
{ "serverName": "slack" }
],
"deniedMcpServers": [
{ "serverName": "filesystem" }
]
}This blocks all MCP servers except those explicitly allowed by the admin. Users cannot add their own servers.
Settings + Subagents
Subagents are Markdown files in .claude/agents/ (project) or ~/.claude/agents/ (user). Settings control their permissions, model, and behavior.
Subagent Frontmatter Configuration
---
name: safe-reviewer
description: Read-only code reviewer
tools: Read, Grep, Glob
disallowedTools: Write, Edit, Bash
model: haiku
permissionMode: plan
effort: low
maxTurns: 10
---
You are a code reviewer. Analyze code quality without making changes.Permission Inheritance Rules
Subagents inherit the parent's permission context, with important exceptions:
| Aspect | Behavior |
|---|---|
| Permission deny rules | Inherited — subagent cannot override parent denials |
| Permission allow rules | Inherited and merged with frontmatter tools |
permissionMode from frontmatter | Overrides session mode |
Parent uses bypassPermissions | Subagent inherits it — cannot be overridden |
Parent uses auto mode | Subagent inherits auto mode — frontmatter ignored |
CLAUDE_CODE_SUBAGENT_MODEL env var | Overrides ALL subagent model settings |
| Skills | NOT inherited — must be listed explicitly in frontmatter |
| CLAUDE.md | NOT loaded — subagent only gets its frontmatter system prompt |
Disable Specific Subagents
{
"permissions": {
"deny": ["Agent(Explore)", "Agent(my-risky-agent)"]
}
}Subagent with Scoped MCP and Hooks
Subagents can define their own MCP servers and hooks in frontmatter:
---
name: browser-tester
description: Tests features using Playwright
tools: Bash, Read
mcpServers:
- playwright:
type: stdio
command: npx
args: ["-y", "@playwright/mcp@latest"]
hooks:
PostToolUse:
- matcher: "Bash"
hooks:
- type: command
command: "echo 'Test completed' >> /tmp/test-log.txt"
---Plugin subagent restriction: Plugin-provided subagents cannot use hooks, mcpServers, or permissionMode fields — they are silently ignored.
Settings + Skills
Skills are custom prompts in .claude/skills/. Settings control their execution environment.
Disable Shell Execution in Skills
{
"disableSkillShellExecution": true
}This replaces !`...` blocks with [shell command execution disabled by policy]. Only bundled and managed skills are exempt.
Skills can specify their own effort level in frontmatter, which overrides the session level. The CLAUDE_CODE_EFFORT_LEVEL env var overrides skill frontmatter.
Environment-Specific Configurations
Development — Local Settings
.claude/settings.local.json (gitignored):
{
"permissions": {
"defaultMode": "acceptEdits",
"allow": ["Bash(npm run dev)"]
},
"model": "sonnet",
"effortLevel": "medium"
}Staging — Blocking Production Commands
Project .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [{
"type": "command",
"command": "bash -c 'INPUT=$(cat); CMD=$(echo $INPUT | jq -r \".tool_input.command\"); if echo \"$CMD\" | grep -i \"production\" > /dev/null; then echo \"Blocked: no production commands in staging\" >&2; exit 2; fi'"
}]
}
]
}
}Production CI — Environment Variables
export ANTHROPIC_MODEL=sonnet
export CLAUDE_CODE_EFFORT_LEVEL=low
export CLAUDE_CODE_DISABLE_AUTO_MEMORY=1
export CLAUDE_CODE_DISABLE_CLAUDE_MDS=1
export DISABLE_TELEMETRY=1
export DISABLE_AUTOUPDATER=1
export CLAUDE_CODE_SKIP_PROMPT_HISTORY=1This strips Claude Code down to a minimal, deterministic execution environment. No memory, no CLAUDE.md loading, no telemetry, no transcripts. Pure tool execution.
The Full Stack
When all systems are active, the evaluation order for a single tool call is:
1. Managed deny rules (cannot be overridden)
2. Project/local deny rules (cannot be overridden by user)
3. Hook PreToolUse exit 2 (can block allowed tools)
4. Permission mode evaluation (default/acceptEdits/auto/etc.)
5. Allow rules (skip prompts)
6. Tool executes
7. Hook PostToolUse (can log, format, validate)Every layer has a purpose. Deny rules are hard walls. Hooks are programmable gates. Permission modes set the default posture. Allow rules reduce friction. Understanding this stack is the difference between a configuration that works and one that silently fails.