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_DIRfor 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:
- Enterprise managed policy settings (highest priority)
~/.claude/settings.json(user-level).claude/settings.json(project-level).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
~/.claudevia dotfiles repo - No duplication: Write once, use everywhere
- Override flexibility: Projects can add their own hooks too
Try it
Prompts:
npm i lodash