HookStack
Back to catalogue
WorkflowSessionStartSessionStartOn Claude Code session start· non-blocking

Node version mismatch check

Catch a wrong Node version before it bites at build time

At session start, compares the active Node major against the project contract (.nvmrc, then package.json engines.node) and warns on a mismatch — so install/build version-specific bugs surface up front, not 20 minutes in. Reads at most two files, no shell spawn.

What does the Node version mismatch check hook do?

Node version mismatch check is a Claude Code SessionStart hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Catch a wrong Node version before it bites at build time.

As a SessionStart hook it runs after the action, reacting to what just happened rather than blocking it. 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

  • Environment sanity
  • Avoid version-specific bugs
  • Smooth onboarding

Tags

#workflow#node#nvmrc#environment#version

settings.json fragment

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/node-version-check.mjs",
            "type": "command"
          }
        ],
        "matcher": "*"
      }
    ]
  }
}

Script · .claude/hooks/node-version-check.mjs

#!/usr/bin/env node
// Avertit si la version de Node active ne correspond pas à celle attendue (SessionStart)
import { readFileSync, existsSync } from 'fs';
import { fileURLToPath } from 'url';
import { join } from 'path';

function major(v) {
  const m = String(v).match(/(\d+)/);
  return m ? Number(m[1]) : null;
}

// Extrait le major attendu de .nvmrc (ex. "20", "v20.11.1", "lts/iron"→ignoré)
// ou de package.json engines.node (ex. ">=20", "20.x").
function expectedMajor({ cwd, readFile, fileExists }) {
  const nvmrc = join(cwd, '.nvmrc');
  if (fileExists(nvmrc)) {
    try { const m = major(readFile(nvmrc, 'utf8').trim()); if (m) return m; } catch {}
  }
  const pkg = join(cwd, 'package.json');
  if (fileExists(pkg)) {
    try {
      const engines = JSON.parse(readFile(pkg, 'utf8')).engines;
      if (engines?.node) { const m = major(engines.node); if (m) return m; }
    } catch {}
  }
  return null;
}

export function run({
  cwd = process.cwd(),
  nodeVersion = process.version,
  readFile = readFileSync,
  fileExists = existsSync,
} = {}) {
  const want = expectedMajor({ cwd, readFile, fileExists });
  if (want == null) return null;
  const have = major(nodeVersion);
  if (have == null || have === want) return null;
  return (
    `## ⚠️ Node version mismatch\n` +
    `Active: Node ${nodeVersion} — project expects major ${want} (.nvmrc / engines). ` +
    `Run \`nvm use\` (or fnm/volta) before installing or building to avoid version-specific bugs.\n`
  );
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  const result = run();
  if (result) process.stdout.write(result);
}

Learn more

Related hooks