Lesson

Enforce Global Rules with User-Level PreToolUse Hooks

Move PreToolUse hooks to ~/.claude/settings.json to enforce conventions like "always pnpm, never npm" across every project automatically.

Access
Included
Transcript
Needs source

Enforcing global rules with user-level hooks eliminates repetition. Move PreToolUse logic to ~/.claude/settings.json and apply conventions across all projects from one central location.

Project vs. User hooks

Project hooks (.claude/settings.local.json):

  • Specific to one repository
  • Live in project directory
  • Good for project-specific rules

User hooks (~/.claude/settings.json):

  • Apply to all projects
  • Live in home directory
  • Perfect for personal conventions

Set up global hooks

Create the directory and initialize:

mkdir ~/.claude/hooks && cd ~/.claude/hooks
bun init
bun i @anthropic-ai/claude-agent-sdk
git init

Configure user-level settings

Create or update ~/.claude/settings.json:

{
  "hooks": {
    "PreToolUse": [{
      "matcher": "",
      "hooks": [{
        "type": "command",
        "command": "bun run ~/.claude/hooks/PreToolUse.ts"
      }]
    }]
  }
}

Use absolute path (~/) or $CLAUDE_PROJECT_DIR for project-relative scripts. Global hooks need absolute paths to work from any directory.

Write the global hook

Create ~/.claude/hooks/PreToolUse.ts:

import type { PreToolUseHookInput, HookJSONOutput } from "@anthropic-ai/claude-agent-sdk"

const input = await Bun.stdin.json() as PreToolUseHookInput

type BashToolInput = {
  command: string
  description: string
}

if (input.tool_name === "Bash") {
  const toolInput = input.tool_input as BashToolInput

  if (toolInput.command.startsWith("npm")) {
    const output: HookJSONOutput = {
      hookSpecificOutput: {
        hookEventName: "PreToolUse",
        permissionDecision: "deny",
        permissionDecisionReason: "Never use npm. Always use pnpm"
      }
    }
    console.log(JSON.stringify(output, null, 2))
  }
}

This hook applies to every Claude Code session, regardless of which project directory you're in.

Test from any project

cd ~/projects/any-repo
claude
npm install lodash
# Hook denies: "Never use npm. Always use pnpm"

Settings hierarchy

Claude Code merges hooks from multiple locations:

  1. Enterprise managed policy settings (highest priority)
  2. ~/.claude/settings.json (user-level)
  3. .claude/settings.json (project-level)
  4. .claude/settings.local.json (local project, gitignored)

User-level hooks run in every project. Project-level hooks run only in that specific repository.

Why this pattern works

  • Universal enforcement: One hook applies to all projects
  • Team alignment: Share ~/.claude via dotfiles repo
  • No duplication: Write once, use everywhere
  • Override flexibility: Projects can add their own hooks too

Try it

Prompts:

npm i lodash