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.
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
- 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
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
// @hookstack setup-install-deps
import { execSync } from "node:child_process";
// @hookstack setup-install-deps
// 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 { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node: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);
}