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 hooks (.claude/settings.local.json):
User hooks (~/.claude/settings.json):
Create the directory and initialize:
mkdir ~/.claude/hooks && cd ~/.claude/hooks
bun init
bun i @anthropic-ai/claude-agent-sdk
git init
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.
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.
cd ~/projects/any-repo
claude
npm install lodash
# Hook denies: "Never use npm. Always use pnpm"
Claude Code merges hooks from multiple locations:
~/.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.
~/.claude via dotfiles repoPrompts:
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.