HookStack
Back to catalogue
SecurityPreToolUse· Write|Edit

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

#git#safety#main-branch#write-guard#branch-discipline

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