Back to catalogue
WorkflowSessionStart
Worktree env initialization
New worktrees boot with their own env and ports
On session start inside a freshly created worktree, copies the main repo's .env files into the worktree so it is ready to run.
What does the Worktree env initialization hook do?
Worktree env initialization is a Claude Code SessionStart hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. New worktrees boot with their own env and ports.
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
- Isolate environment variables (ports, secrets) per worktree
- Start multiple agents in parallel without port collisions
Tags
#worktree#env#ports#multi-agent#session-start
settings.json fragment
{
"hooks": {
"SessionStart": [
{
"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"
}
],
"matcher": "startup"
}
]
}
}Script · .claude/hooks/setup-worktree-env.mjs
#!/usr/bin/env node
// @hookstack worktree-create-setup-env
// SessionStart : si la session démarre dans un worktree, copie depuis le dépôt principal
// les fichiers d'environnement et secrets locaux. Deux passes :
// 1. Liste statique de fichiers racine connus (multi-écosystème)
// 2. Scan récursif (find, profondeur 4) pour couvrir les monorepos (apps/web/.env…)
import { execSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
function defaultExec(cmd) {
try {
return execSync(cmd, { encoding: "utf8", timeout: 10_000 }).trim();
} catch {
return "";
}
}
// Fichiers racine copiés explicitement si présents.
// Couvre Node/Bun, Vite, Next.js, CRA, Python dotenv, Ruby on Rails, direnv, Docker Compose.
const ROOT_FILES = [
// Dotenv standard — tous frameworks JS/TS/Python
".env",
".env.local",
".env.development",
".env.development.local",
".env.test",
".env.test.local",
".env.staging",
".env.staging.local",
".env.production",
".env.production.local",
".env.override", // convention docker-compose
// direnv
".envrc",
// Ruby on Rails master key
"config/master.key",
];
// Répertoires exclus du scan récursif monorepo.
const SKIP_DIRS = [
"node_modules",
".git",
"dist",
"build",
".next",
"out",
"coverage",
".turbo",
".cache",
"__pycache__",
"target",
".venv",
"venv",
];
/**
* Scan récursif des sous-répertoires (profondeur 2–4) pour trouver les fichiers
* `.env*` et `.envrc` dans les structures monorepo (apps/web/.env, packages/api/.env…).
* Utilise `find` via execSync. Injecté en dépendance pour rester testable.
*/
function defaultScanEnvFiles(dir) {
const excludes = SKIP_DIRS.map((d) => `-not -path "*/${d}/*"`).join(" ");
const cmd = `find "${dir}" -mindepth 2 -maxdepth 4 -type f \\( -name ".env" -o -name ".env.*" -o -name ".envrc" \\) ${excludes} 2>/dev/null`;
try {
const out = execSync(cmd, { encoding: "utf8", timeout: 10_000 }).trim();
if (!out) return [];
return out.split("\n").map((abs) => abs.slice(dir.length + 1));
} catch {
return [];
}
}
export function run({
exec = defaultExec,
exists = existsSync,
copy = copyFileSync,
mkdir = mkdirSync,
scanEnvFiles = defaultScanEnvFiles,
} = {}) {
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) return;
// Fusion liste statique + résultats du scan monorepo (déduplication)
const candidates = [...ROOT_FILES];
for (const rel of scanEnvFiles(mainDir)) {
if (!candidates.includes(rel)) candidates.push(rel);
}
for (const rel of candidates) {
const src = join(mainDir, rel);
const dst = join(worktreeDir, rel);
if (exists(src) && !exists(dst)) {
const dstDir = dirname(dst);
if (!exists(dstDir)) mkdir(dstDir, { recursive: true });
copy(src, dst);
process.stderr.write(`Copié : ${rel} → ${worktreeDir}\n`);
}
}
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
readFileSync(0, "utf8");
run();
// SessionStart : stdout vide = aucun contexte ajouté.
}
Learn more
Related hooks
- Worktree guardrail (Edit/Write)Edits never leak back into your main repo
- Worktree dependency updateEvery worktree starts with fresh dependencies
- Auto-disable Stop hook after N failuresA flaky Stop hook can't trap your session
- Transcript backup before compactionYour full transcript survives every compaction