Back to catalogue
WorkflowSessionStartSessionStartOn Claude Code session start· non-blocking
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.
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": [
{
"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, 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 'child_process';
import { existsSync, copyFileSync, mkdirSync, readFileSync } from 'fs';
import { join, dirname } from 'path';
import { fileURLToPath } from '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é.
}