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

Worktree env initialization

On session start inside a freshly created worktree, copies the main repo's .env files into the worktree so it is ready to run.

Use cases

  • Isolate environment variables (ports, secrets) per worktree
  • Start multiple agents in parallel without port collisions

Providers & tags

Claude Code
#worktree#env#ports#multi-agent#session-start

settings.json fragment

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/setup-worktree-env.mjs",
            "statusMessage": "Copie des .env du worktree au démarrage de session...",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/setup-worktree-env.mjs

#!/usr/bin/env node
// SessionStart : si la session démarre dans un worktree (distinct du dépôt principal),
// copie les fichiers .env du dépôt principal vers le worktree.
// NB : ce hook NE s'enregistre PAS sur WorktreeCreate — ce dernier remplace la création
// du worktree et exige un chemin absolu sur stdout. Le post-setup va sur SessionStart.
import { execSync } from 'child_process';
import { existsSync, copyFileSync, readFileSync } from 'fs';
import { join } from 'path';
import { fileURLToPath } from 'url';

function defaultExec(cmd) {
  try { return execSync(cmd, { encoding: 'utf8', timeout: 10_000 }).trim(); } catch { return ''; }
}

export function run({
  exec = defaultExec,
  exists = existsSync,
  copy = copyFileSync,
} = {}) {
  const worktreeList = exec('git worktree list');
  const mainDir = worktreeList.split('\n')[0]?.split(/\s+/)[0] ?? '';
  const worktreeDir = exec('git rev-parse --show-toplevel');

  if (mainDir && worktreeDir && mainDir !== worktreeDir) {
    for (const envFile of ['.env', '.env.local', '.env.development.local']) {
      const src = join(mainDir, envFile);
      const dst = join(worktreeDir, envFile);
      if (exists(src) && !exists(dst)) {
        copy(src, dst);
        process.stderr.write(`Copié : ${envFile} → ${worktreeDir}\n`);
      }
    }
  }
}

/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
  readFileSync(0, 'utf8');
  run();
  // SessionStart : pas de stdout obligatoire (stdout vide = aucun contexte ajouté).
}