Learn Claude Code: Hooks Deep Dive The Configuration System

The Configuration System

Advanced 🕐 11 min Lesson 2 of 12
What you'll learn
  • Map the four-level settings hierarchy to real deployment scenarios and choose the right scope for a given hook
  • Write a complete hooks configuration block with event, matcher group, and handler fields
  • Apply path placeholders and understand deduplication to avoid common configuration mistakes

Why Configuration Scope Matters

A hook that blocks destructive commands should probably apply everywhere you work, not just in one project. A hook that runs a project-specific linter should live with the project so everyone on the team gets it automatically. A hook that sends Slack notifications to your personal account should never be checked into a shared repository. These different needs are why Claude Code provides four distinct configuration scopes.

Getting the scope right determines who sees the hook, whether it travels with the code, and who can override it. Put everything in user settings and your personal hooks pollute every project. Put security rules in project settings and they disappear when someone switches projects. Put audit hooks in managed settings and no one can accidentally disable them.

The Four Settings File Locations

Claude Code loads hooks from four settings files, each at a different scope.

~/.claude/settings.json
User scope. Applies to all projects on your machine. Not shared via git. Good for personal preferences: your notification hooks, your personal logging, your development environment setup.
.claude/settings.json
Project scope. Lives in the repository root and is committed to git. Shared with all contributors. Good for project-wide rules: the linter hook, the test gate, the security guards that should apply to everyone working on this codebase.
.claude/settings.local.json
Local scope. Project directory but gitignored. Your personal overrides for this specific project. Good for hooks that reference local paths, personal API tokens, or development-only behaviors you don't want in the shared config.
Managed settings
Organization scope. Deployed via MDM, OS-level policies, or a managed-settings.json. Applied before user or project settings and cannot be overridden by them. Good for compliance requirements, audit logging, and security policies that must apply to every developer in the organization.

All four files are active simultaneously. Claude Code merges them, with managed settings taking the highest priority and local settings the lowest.

The Hook Event → Matcher → Handler Hierarchy

Inside any settings file, hooks are structured as a three-level object. Understanding this hierarchy is essential for writing correct configuration.

The top level is the hook event name — the lifecycle event you want to intercept. Valid values include PreToolUse, PostToolUse, Stop, SessionStart, and every other event in the system. Each event maps to an array of matcher groups.

Each matcher group contains a matcher string and a hooks array. The matcher filters which invocations of the event trigger the handlers. For PreToolUse, the matcher filters by tool name. For SessionStart, it filters by session source. An empty string or "*" matches everything.

Each entry in the hooks array is a handler object with a required type field and type-specific fields. A minimal configuration looks like this:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/check-bash.sh"
          }
        ]
      }
    ]
  }
}

This registers one command hook on PreToolUse, targeting only Bash tool calls, running a project-local script.

Path Placeholders and Deduplication

Hard-coding absolute paths in hook commands breaks portability. A script path that works on your machine will fail for a teammate with a different home directory. Claude Code provides path placeholders to solve this.

${CLAUDE_PROJECT_DIR} expands to the project root directory at runtime. Use it for any script that lives inside the repository. ${CLAUDE_PLUGIN_ROOT} expands to the plugin installation directory when a hook is defined inside a plugin. ${CLAUDE_PLUGIN_DATA} points to the plugin's persistent data directory for storing state between runs.

Deduplication prevents the same handler from running twice when it appears in multiple settings files. Claude Code compares handlers by their effective command string and arguments. If the same command appears in both user settings and project settings, it runs once. This means you can safely add a hook to your personal settings as a fallback without worrying about duplicating the project's hook.

All matching handlers for a given event run in parallel, not sequentially. If you have three PreToolUse handlers that all match a Bash call, all three run simultaneously and Claude waits for all of them before proceeding.

Global Controls

Three settings govern hook behavior globally, independent of individual hook configuration.

disableAllHooks: true is an emergency circuit breaker. It disables every hook across all scopes without deleting any configuration. If hooks are causing unexpected behavior in a session, this lets you disable them instantly and diagnose the problem, then re-enable by removing the flag.

allowManagedHooksOnly: true is an organizational enforcement tool, available only in managed settings. When set, it blocks all user, project, and plugin hooks, leaving only the organization-deployed managed hooks active. This ensures that the hooks running in production belong to the organization, not to individual contributors.

allowedHttpHookUrls is an allowlist of URL patterns for HTTP hooks. An undefined value places no restrictions. An empty array blocks all HTTP hooks. A list of patterns (with * as a wildcard) restricts HTTP hooks to those matching URLs. This prevents hooks from POSTing data to arbitrary external services.

Key takeaways
  • Four settings scopes stack in priority order — managed overrides project overrides user; local is project-scoped but gitignored for personal customization
  • Every hook entry follows a three-level structure — event name, matcher group with a matcher string, and an array of handler objects
  • Path placeholders keep hook scripts portable — use ${CLAUDE_PROJECT_DIR} for project-relative paths instead of hardcoded absolute paths
  • Identical handlers deduplicate automatically — the same command string appearing in multiple settings files runs only once per event
  • disableAllHooks is an emergency circuit breaker — one boolean disables all hooks across all scopes without deleting any configuration