HookStack
Back to catalogue
WorkflowPreToolUse· Bash

Block push on closed PR

prevents accidentally pushing to a merged or closed PR branch

Intercepts git push commands and checks via gh CLI if the current branch has a closed or merged PR. Blocks the push with a message pointing to create a new branch from main.

What does the Block push on closed PR hook do?

Block push on closed PR is a Claude Code PreToolUse hook matching Bash. It fires automatically at that lifecycle event — outside the model, so it can't be skipped or forgotten. prevents accidentally pushing to a merged or closed PR 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

  • Prevent pushing fixes on a branch whose PR was already merged or closed
  • Guide the agent to always branch from an up-to-date main

Tags

#git#github#safety#pr

settings.json fragment

{
  "hooks": {
    "PreToolUse": [
      {
        "hooks": [
          {
            "command": "node $CLAUDE_PROJECT_DIR/.claude/hooks/block-push-closed-pr.mjs",
            "type": "command"
          }
        ],
        "matcher": "Bash"
      }
    ]
  }
}

Script · .claude/hooks/block-push-closed-pr.mjs

import { execSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";

const defaultExec = (cmd) => execSync(cmd, { encoding: "utf8", timeout: 5000 });

export function run(input, { exec = defaultExec } = {}) {
	const command = input?.tool_input?.command ?? "";
	if (!/\bgit\s+push\b/.test(command)) return null;

	let branch;
	try {
		branch = exec("git rev-parse --abbrev-ref HEAD").trim();
	} catch {
		return null;
	}

	if (!branch || branch === "HEAD" || branch === "main" || branch === "master")
		return null;

	let state;
	try {
		state = exec(`gh pr view "${branch}" --json state --jq '.state'`).trim();
	} catch {
		return null;
	}

	if (state === "CLOSED" || state === "MERGED") {
		const label = state === "MERGED" ? "mergée" : "fermée";
		return {
			decision: "block",
			reason: `La PR de la branche '${branch}' est ${label}. Créez une nouvelle branche depuis main : git checkout -b fix/... origin/main`,
		};
	}

	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