Worktree guardrail (Edit/Write)
Edits never leak back into your main repo
Blocks any Edit/Write whose absolute path targets the main repo instead of the active worktree. Suggests the corrected path. Detects the worktree via .claude/active-worktree or via git rev-parse.
What does the Worktree guardrail (Edit/Write) hook do?
Worktree guardrail (Edit/Write) is a Claude Code PreToolUse hook matching Edit|Write. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Edits never leak back into your main repo.
As a PreToolUse hook it runs before the action completes, so it can block or adjust what Claude is about to do. 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
- Prevent accidental changes to the main repo in worktree mode
- Parallel multi-agent workflows with per-worktree isolation
Tags
settings.json fragment
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/worktree-guard.mjs",
"type": "command"
}
],
"matcher": "Edit|Write"
}
]
}
}Script · .claude/hooks/worktree-guard.mjs
#!/usr/bin/env node
// @hookstack pre-edit-worktree-guard
import { execSync } from "node:child_process";
// @hookstack pre-edit-worktree-guard
// Empêche l'édition de fichiers hors du worktree courant (PreToolUse Write|Edit)
import { readFileSync } from "node:fs";
import { homedir } from "node:os";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
// Répertoires internes des agents — légitimes hors worktree
const AGENT_DIRS = [".claude", ".codex"];
function defaultExec(cmd) {
return execSync(cmd, { encoding: "utf8", timeout: 5_000 }).trim();
}
export function run(input, { exec = defaultExec, home = homedir() } = {}) {
const filePath = input.tool_input?.file_path ?? "";
if (!filePath) return null;
try {
const worktreeRoot = exec("git rev-parse --show-toplevel");
const worktreeLines = exec("git worktree list").split("\n");
const mainRoot = worktreeLines[0]?.split(/\s+/)[0] ?? "";
// N'applique le garde que dans un worktree non principal
if (!mainRoot || worktreeRoot === mainRoot) return null;
const absFile = resolve(filePath);
// Autoriser les répertoires internes des agents (plans, mémoire, config…)
if (AGENT_DIRS.some((d) => absFile.startsWith(`${resolve(home, d)}/`)))
return null;
if (!absFile.startsWith(`${worktreeRoot}/`)) {
return {
decision: "block",
reason: `Écriture hors du worktree courant (${worktreeRoot}). Vérifiez le chemin cible.`,
};
}
} catch {
// git absent ou pas dans un repo — laisser passer
}
return null;
}
/* v8 ignore next 5 */
if (process.argv[1] === fileURLToPath(import.meta.url)) {
const input = JSON.parse(readFileSync(0, "utf8"));
const result = run(input);
if (result) process.stdout.write(JSON.stringify(result));
}
Learn more
Related hooks
- Worktree env initializationNew worktrees boot with their own env and ports
- 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