Enforce Global Rules with User-Level PreToolUse Hooks

John Lindquist
InstructorJohn Lindquist

Social Share Links

Tweet

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

[00:00] When you have a situation where you want to deny a tool across all of your projects, you can take your configuration from your local settings, and we're going to open up our cursor in our home directory.cloud.slash settings, and then create a hooks section in here. And then just simply copy and paste the pre-tool use hook. Make sure to clean up any stray formatting issues, then hop back over to our user settings and inside of here we'll paste this. Now this is no longer a relative path and this is where you have to make a decision for either yourself or your team is where do you want to store hooks for all of your developers. You could store them in your home directory under the .clod folder, which makes a lot of sense.

[00:48] And since Anthropic currently doesn't have a hooks convention, you could version control a hooks directory inside of there. So that's the approach I'm going to take, But I can't guarantee that Anthropic isn't going to use a directory structure like this or conventions like this in the future. So you could decide on a path somewhere on the system to point to. That's just something you'll have to discuss with your team. But for me, I'm going to make a directory in clod slash hooks.

[01:15] I'm going to open up an instance of cursor in clod slash hooks. And then in this directory I will bun init to set this up as a bun project. I will bun install the agent SDK. And then I can just move over my pre-tool use. I'll git init this project, then just stage all the files, generate a commit message, let it commit, then you could create a repo and back it up and everything.

[01:42] But for now we just have to remember that this settings is inside of our .clod directory and the command is pointing to this hook here. So I'm going to configure this so that if a command starts with npm I'm going to deny and say never use npm always use pnpm. And this way no matter where we run our Clod tool, so let's change to a directory like a YouTube download project I have, I'll launch Clod, I'll say npm install lodash to ask it to install lodash, and you'll see that npm was blocked. And I did run into a bug here where the reason that this did not continue and swap over to pnpm is I was playing around with some other properties when testing out these lessons. I had continue false enabled here, so that prevented it from reading our follow-up instructions.

[02:36] So I'll say slash new and then try it again. You'll see that it blocks npm, we get the message back to always use pnpm, and now it's asking me to use PNPM to install this package.