HookStackGitHub
Back to catalogue
WorkflowSessionStart· startupSessionStartOn Claude Code session start· non-blocking

Install project dependencies on init

Fresh clone opens with node_modules ready

Detects the project's package manager from lock files (pnpm, yarn, npm) and runs a frozen install on first session startup. Ensures Claude Code always starts with a complete node_modules without requiring a manual install step.

What does the Install project dependencies on init hook do?

Install project dependencies on init is a Claude Code SessionStart hook matching startup. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Fresh clone opens with node_modules ready.

Use cases

  • Auto-install dependencies when Claude Code opens a freshly cloned repository
  • Guarantee node_modules is present before any tool calls run in a new project
  • Unify the dev environment setup across team members via a shared hook

Tags

#setup#npm#pnpm#yarn#dependencies#onboarding

settings.json fragment

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/setup-install-deps.mjs",
            "type": "command"
          }
        ],
        "matcher": "startup"
      }
    ]
  }
}

Script · .claude/hooks/setup-install-deps.mjs

#!/usr/bin/env node
// Installe les dépendances au démarrage si node_modules est absent (SessionStart/WorktreeCreate).
// Dans un worktree distinct, update-deps.mjs gère l'install en mode détaché — ce hook
// s'abstient pour éviter la race condition (deux pnpm install concurrents → ENOTEMPTY).
import { readFileSync, existsSync } from 'fs';
import { execSync } from 'child_process';
import { fileURLToPath } from 'url';

export function run(input, { exec, exists = existsSync } = {}) {
  const cwd = input.cwd;
  const has = (f) => exists(`${cwd}/${f}`);

  // Skip in a distinct worktree: update-deps.mjs already spawned pnpm there.
  try {
    const list = execSync('git worktree list', { cwd, encoding: 'utf8', timeout: 5_000 }).trim();
    const mainDir = list.split('\n')[0]?.split(/\s+/)[0] ?? '';
    if (mainDir && mainDir !== cwd) return null;
  } catch { /* not a git repo — fall through */ }

  if (has('node_modules')) return null; // already installed

  let cmd = null;
  if (has('pnpm-lock.yaml')) cmd = 'pnpm install --frozen-lockfile';
  else if (has('yarn.lock')) cmd = 'yarn install --frozen-lockfile';
  else if (has('package-lock.json')) cmd = 'npm ci';
  else if (has('package.json')) cmd = 'npm install';

  if (!cmd) return null;

  const doExec = exec ?? ((c) => execSync(c, { cwd, stdio: 'inherit', timeout: 180_000 }));
  try {
    doExec(cmd);
    return { cmd, message: `[setup-install-deps] Running: ${cmd}\n` };
  } catch (e) {
    return { cmd, error: e.message, message: `[setup-install-deps] Failed: ${e.message}\n` };
  }
}

/* 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?.message) process.stderr.write(result.message);
}