HookStack
Back to catalogue
ValidationPreToolUse· Bash

Enforce package managers

Forces uv and pnpm, kills pip/npm drift

Blocks pip, poetry, npm and yarn commands; enforces uv (Python) and pnpm (Node.js). Generates an actionable message with the replacement command.

What does the Enforce package managers hook do?

Enforce package managers is a Claude Code PreToolUse hook matching Bash. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Forces uv and pnpm, kills pip/npm drift.

As a PreToolUse hook it runs before the action completes, so it can block or adjust what Claude is about to do. Because it is a deterministic Node.js script, it executes on every matching event without relying on the model to remember — the guarantee that makes agentic workflows safe to automate.

Use cases

  • Standardize package management tooling across a Python+Node monorepo
  • Avoid virtual environment conflicts between pip and uv

Tags

#package-manager#uv#pnpm#python#node#validation

settings.json fragment

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "command": "node .claude/hooks/enforce-package-managers.mjs",
            "type": "command"
          }
        ],
        "matcher": "Bash"
      }
    ]
  }
}

Script · .claude/hooks/enforce-package-managers.mjs

#!/usr/bin/env node
// @hookstack pre-bash-enforce-package-managers
// Bloque npm et yarn, impose pnpm pour ce projet Node.js (PreToolUse Bash)
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const FORBIDDEN = [
	{ pattern: /(^|[;&|\s])npm(\s|$)/, replacement: "pnpm" },
	{ pattern: /(^|[;&|\s])yarn(\s|$)/, replacement: "pnpm" },
];

export function run(input) {
	if (input.tool_name !== "Bash") return null;
	const cmd = input.tool_input?.command ?? "";
	// Supprime le contenu des chaînes entre guillemets pour éviter les faux positifs
	// quand npm/yarn apparaissent comme valeurs d'arguments texte (ex. git commit -m "...npm...",
	// gh pr create --body "...yarn...") tout en continuant à bloquer les vraies invocations
	// de gestionnaire de paquets même après des opérateurs shell (&&, ||, ;).
	const stripped = cmd
		.replace(/"(?:[^"\\]|\\.)*"/g, '""')
		.replace(/'(?:[^'\\]|\\.)*'/g, "''");
	const hit = FORBIDDEN.find(({ pattern }) => pattern.test(stripped));
	return hit
		? {
				decision: "block",
				reason: `Utiliser '${hit.replacement}' à la place. Ce projet impose pnpm (pas npm ni yarn).`,
			}
		: null;
}

/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const input = JSON.parse(readFileSync(0, "utf8"));
	const result = run(input);
	if (result) process.stdout.write(JSON.stringify(result));
}

Learn more

Related hooks