HookStackGitHub
Back to catalogue
ValidationPreToolUse· BashPreToolUseBefore tool execution · can block⚡ blocking

Enforce package managers

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

Use cases

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

Providers & tags

Claude Code
#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
// Bloque npm et yarn, impose pnpm pour ce projet Node.js (PreToolUse Bash)
import { readFileSync } from 'fs';
import { fileURLToPath } from '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));
}