Back to catalogue
WorkflowSessionStartSessionStartOn Claude Code session start· non-blocking
Worktree dependency update
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.
Use cases
- Ensure every worktree starts with up-to-date dependencies
- Automate bootstrapping of a new isolated agent environment
Providers & tags
Claude Code
#worktree#dependencies#bootstrap#session-start
settings.json fragment
{
"hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/update-deps.mjs",
"statusMessage": "Mise à jour des dépendances du worktree...",
"type": "command"
}
]
}
]
}
}Script · .claude/hooks/update-deps.mjs
#!/usr/bin/env node
// 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 'child_process';
import { existsSync, readFileSync } from 'fs';
import { fileURLToPath } from '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é).
}