HookStack
Back to catalogue
WorkflowSessionStart

Auto-create worktree when starting on main

Start on main and you're moved to a worktree

At session start, detects if the current branch is main/master and automatically creates a dated git worktree (work/session-YYYYMMDD). The agent is informed of the new path and instructed to work there instead of the main repo.

What does the Auto-create worktree when starting on main hook do?

Auto-create worktree when starting on main is a Claude Code SessionStart hook. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Start on main and you're moved to a worktree.

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

  • Branch discipline
  • Isolated sessions
  • Accidental main commits prevention
  • Multi-session workflows

Tags

#git#worktree#safety#session#main-branch

settings.json fragment

{
  "hooks": {
    "SessionStart": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/session-start-worktree-if-main.mjs",
            "type": "command"
          }
        ]
      }
    ]
  }
}

Script · .claude/hooks/session-start-worktree-if-main.mjs

#!/usr/bin/env node
// @hookstack session-start-worktree-if-main
// SessionStart: crée un worktree isolé unique si la session démarre sur main/master.
// Chaque session obtient un worktree frais avec un suffixe aléatoire — jamais de
// réutilisation d'un worktree existant (un worktree désynchronisé provoquerait des conflits).
// Le nettoyage reste manuel (`git worktree prune`).
import { execSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { existsSync } from "node:fs";
import { fileURLToPath } from "node:url";

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

function defaultAddWorktree(path, branchName) {
	execSync(`git worktree add "${path}" -b "${branchName}"`, {
		encoding: "utf8",
		timeout: 15_000,
		stdio: ["ignore", "ignore", "ignore"],
	});
}

function defaultRandom(len = 6) {
	return randomBytes(Math.ceil(len / 2))
		.toString("hex")
		.slice(0, len);
}

export function run({
	exec = defaultExec,
	addWorktree = defaultAddWorktree,
	exists = existsSync,
	now = () => new Date(),
	random = defaultRandom,
} = {}) {
	const branch =
		exec("git branch --show-current") ||
		exec("git rev-parse --abbrev-ref HEAD");
	if (!branch || !/^(main|master)$/.test(branch)) return null;

	const currentRoot = exec("git rev-parse --show-toplevel");
	if (!currentRoot) return null;

	// Ne pas agir si on est déjà dans un worktree secondaire
	const worktreeList = exec("git worktree list");
	const mainRoot = worktreeList.split("\n")[0]?.split(/\s+/)[0] ?? "";
	if (mainRoot !== currentRoot) return null;

	// Synchroniser main avec le remote avant de créer le worktree
	exec("git fetch --quiet origin main");
	exec("git merge --ff-only origin/main");

	// Nom unique par session : date + suffixe hex aléatoire (même format que Claude Code App)
	const date = now().toISOString().slice(0, 10).replace(/-/g, "");
	const suffix = random(6);
	const branchName = `claude/session-${date}-${suffix}`;
	const worktreePath = `${currentRoot}/.claude/worktrees/session-${date}-${suffix}`;

	try {
		addWorktree(worktreePath, branchName);
	} catch {
		return `${[
			`## ⚠️  Session démarrée sur \`main\``,
			`- Impossible de créer un worktree automatiquement.`,
			`- Créez manuellement un worktree ou une branche avant de modifier des fichiers.`,
		].join("\n")}\n`;
	}

	if (!exists(worktreePath)) return null;

	return `${[
		`## Worktree isolé créé automatiquement`,
		`- Session démarrée sur \`main\` : un worktree unique a été créé pour cette session.`,
		`- **Chemin** : \`${worktreePath}\``,
		`- **Branche** : \`${branchName}\``,
		`- Travaillez dans ce worktree — évitez de modifier des fichiers dans le dépôt principal.`,
	].join("\n")}\n`;
}

/* v8 ignore next 4 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
	const result = run();
	if (result) process.stdout.write(result);
}

Learn more

Related hooks