Skip to main content

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 CodeEffect
0Allow the action
2Block the action (stderr sent as feedback to Claude)
OtherAllow 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-blocked

Managed-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:

LayerControlsEnforcement
settings.jsonWhat Claude can doHard — cannot be overridden by prompts
CLAUDE.mdHow Claude should behaveSoft — 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:

ScopeLocationLoaded When
User~/.claude/CLAUDE.mdEvery session
Project rootCLAUDE.md or .claude/CLAUDE.mdIn this project
Local overrideCLAUDE.local.mdIn 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:

AspectBehavior
Permission deny rulesInherited — subagent cannot override parent denials
Permission allow rulesInherited and merged with frontmatter tools
permissionMode from frontmatterOverrides session mode
Parent uses bypassPermissionsSubagent inherits it — cannot be overridden
Parent uses auto modeSubagent inherits auto mode — frontmatter ignored
CLAUDE_CODE_SUBAGENT_MODEL env varOverrides ALL subagent model settings
SkillsNOT inherited — must be listed explicitly in frontmatter
CLAUDE.mdNOT 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=1

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