Block file writes on main branch
Blocks edits on main until you branch
Blocks any Write or Edit tool call when the current branch is main/master and the working directory is the principal repo (not a worktree). Forces the agent to create a branch or worktree before making file changes.
What does the Block file writes on main branch hook do?
Block file writes on main branch is a Claude Code PreToolUse hook matching Write|Edit. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. Blocks edits on main until you branch.
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
- Accidental main commits prevention
- Branch discipline enforcement
- Pair with worktree auto-create hook
- Strict repository governance
Tags
settings.json fragment
{
"hooks": {
"PreToolUse": [
{
"hooks": [
{
"command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/pre-write-main-guard.mjs",
"type": "command"
}
],
"matcher": "Write|Edit"
}
]
}
}Script · .claude/hooks/pre-write-main-guard.mjs
#!/usr/bin/env node
// @hookstack pre-write-main-guard
import { execSync } from "node:child_process";
// @hookstack pre-write-main-guard
// PreToolUse Write|Edit: bloque la première écriture sur main si aucun worktree n'est actif
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
function defaultExec(cmd) {
try {
return execSync(cmd, { encoding: "utf8", timeout: 5_000 }).trim();
} catch {
return "";
}
}
export function run(input, { exec = defaultExec } = {}) {
const branch =
exec("git branch --show-current") ||
exec("git rev-parse --abbrev-ref HEAD");
if (!branch || !/^(main|master)$/.test(branch)) return null;
const worktreeList = exec("git worktree list");
const currentRoot = exec("git rev-parse --show-toplevel");
const mainRoot = worktreeList.split("\n")[0]?.split(/\s+/)[0] ?? "";
if (mainRoot !== currentRoot) return null;
const filePath = input.tool_input?.file_path ?? "(fichier inconnu)";
// Autoriser les écritures vers des fichiers hors du repo principal
if (filePath !== "(fichier inconnu)" && !filePath.startsWith(`${mainRoot}/`))
return null;
// Autoriser les écritures dans un worktree secondaire (ex: .claude/worktrees/session-xxx/…)
const secondaryWorktrees = worktreeList
.split("\n")
.slice(1)
.map((line) => line.split(/\s+/)[0])
.filter(Boolean);
if (secondaryWorktrees.some((wt) => filePath.startsWith(`${wt}/`)))
return null;
return {
decision: "block",
reason: `Écriture sur \`${branch}\` bloquée : vous êtes sur la branche principale.\nCréez un worktree (\`git worktree add ../mon-fix -b feat/mon-fix\`) ou changez de branche avant de modifier \`${filePath}\`.`,
};
}
/* 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
- Secret detection before Bash executionCatch a leaked API key before it ever runs
- Destructive command blockingStops a disk-wiping shell command before it runs
- Sensitive file write protectionYour .env and keys stay untouched by the agent
- Lock file write protectionLock files stay intact - package manager only