Back to catalogue
WorkflowSessionStart
Worktree dependency update
Every worktree starts with fresh dependencies
On session start inside a worktree that has no node_modules yet, spawns the dependency install as a detached background process so the session never blocks.
What does the Worktree dependency update hook do?
Worktree dependency update is a Claude Code SessionStart hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Every worktree starts with fresh dependencies.
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
- Ensure every worktree starts with up-to-date dependencies
- Automate bootstrapping of a new isolated agent environment
Tags
#worktree#dependencies#bootstrap#session-start
settings.json fragment
{
"hooks": {
"SessionStart": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/update-deps.mjs",
"statusMessage": "Mise à jour des dépendances du worktree...",
"type": "command"
}
],
"matcher": "startup"
}
]
}
}Script · .claude/hooks/update-deps.mjs
#!/usr/bin/env node
// @hookstack worktree-create-update-deps
// SessionStart : si la session démarre dans un worktree fraîchement créé (node_modules
// absent), lance l'install des dépendances en process DÉTACHÉ pour ne pas bloquer le
// démarrage de session, puis rend la main immédiatement.
// NB : ce hook NE s'enregistre PAS sur WorktreeCreate — ce dernier remplace la création
// du worktree, exige un chemin absolu sur stdout et ne supporte pas l'exécution async.
import { execSync, spawn } from "node:child_process";
import { existsSync, readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
/* v8 ignore next 3 */
function defaultExec(cmd, opts = {}) {
try {
return execSync(cmd, { encoding: "utf8", timeout: 10_000, ...opts }).trim();
} catch {
return "";
}
}
/* v8 ignore next 8 */
function defaultDetach(cmd, args, cwd) {
const child = spawn(cmd, args, {
cwd,
detached: true,
stdio: "ignore",
});
child.unref();
}
export function run({
exec = defaultExec,
exists = existsSync,
detach = defaultDetach,
} = {}) {
const worktreeDir = exec("git rev-parse --show-toplevel");
if (!worktreeDir) return;
// Uniquement dans un worktree distinct du dépôt principal.
const mainDir =
exec("git worktree list").split("\n")[0]?.split(/\s+/)[0] ?? "";
if (!mainDir || mainDir === worktreeDir) return;
// Rien à faire sans package.json, ou si les deps sont déjà installées.
if (!exists(`${worktreeDir}/package.json`)) return;
if (exists(`${worktreeDir}/node_modules`)) return;
const hasPnpm = exec("which pnpm");
if (hasPnpm) {
detach(
"pnpm",
["install", "--frozen-lockfile", "--ignore-scripts"],
worktreeDir,
);
} else {
detach("npm", ["ci", "--ignore-scripts"], worktreeDir);
}
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
readFileSync(0, "utf8");
run();
// SessionStart : pas de stdout obligatoire (install lancé en détaché).
}
Learn more
Related hooks
- Worktree guardrail (Edit/Write)Edits never leak back into your main repo
- Worktree env initializationNew worktrees boot with their own env and ports
- Auto-disable Stop hook after N failuresA flaky Stop hook can't trap your session
- Transcript backup before compactionYour full transcript survives every compaction