How to stop Claude Code from merging pull requests or pushing without asking

Two tools can stop it: permission deny rules in settings.json and a PreToolUse hook. They match different things, and each has forms of the command it misses. This guide shows which is which.

Written by Kyle, who made the free send gate mentioned at the end and also sells a paid kit (optional, one line at the bottom). Independent project, not affiliated with Anthropic.

The short answer

Add Bash(git push *) and Bash(gh pr merge *) to permissions.deny. That stops the usual form of each command. Then add a PreToolUse hook if you want it to hold when the command is written another way, and for the GitHub MCP merge tool. A line in CLAUDE.md asking Claude to check first shapes what it tries, but nothing enforces it.

Deny rulesCheap, no code. Match the command text Claude writes. Miss some alternate spellings.
PreToolUse hookYour code sees the whole command and the tool name. Can deny or ask for approval.
MCP merge toolsNeed their own rule or hook match. A Bash rule never sees them.

Option 1: deny rules in settings

Rules are evaluated in order: deny, then ask, then allow, and the first match wins. A matching allow rule cannot carve an exception out of a deny. Put this in .claude/settings.json (or ~/.claude/settings.json):

{
  "permissions": {
    "deny": [
      "Bash(git push *)",
      "Bash(gh pr merge *)",
      "mcp__github__merge_pull_request"
    ]
  }
}

Use ask instead of deny if you want a prompt rather than a flat refusal. The MCP rule uses the form mcp__<server>__<tool>, so the server name in your own config decides the exact string. Check the server name in your own MCP config before relying on it. Claude Code skips an mcp__ rule that has parentheses, so you cannot match on a tool's arguments this way.

What the deny rule does not match

Anthropic's permissions page is direct about this: a Bash deny rule covers the form Claude usually writes and "isn't a security boundary around the program." Its own table lists what Bash(git push *) does not stop:

The same table says Bash(curl *) misses /usr/bin/curl and sh -c '...'. The pattern is the same for gh. A rule is a good first layer, not the only one.

Do chained commands slip through?

Not on a plain &&. Claude Code knows the shell separators (&&, ||, ;, |, |&, & and newlines). For deny and ask rules, the docs say a rule applies when any subcommand matches, including one inside a command substitution. So git add . && git commit -m x && git push still hits Bash(git push *). Allow rules work the other way: each subcommand must match on its own, so allowing safe-cmd does not allow safe-cmd && other-cmd.

The gap is not chaining. It is the alternate forms above, and any hook you write yourself: if your script greps the whole command string, make sure it does not assume the push is the first word.

Option 2: a PreToolUse hook

A PreToolUse hook gets the tool call as JSON on stdin. For Bash, the command is at tool_input.command. Exit code 2 blocks the call, and what you print to stderr is shown to Claude as the reason. Here is a short one that needs jq:

#!/bin/bash
# ~/.claude/hooks/no-push-merge.sh
cmd=$(jq -r '.tool_input.command // empty')
if echo "$cmd" | grep -Eq 'git[[:space:]].*push|gh[[:space:]]+pr[[:space:]]+merge'; then
  echo "Blocked: ask the user before pushing or merging." >&2
  exit 2
fi
exit 0

Wire it up in settings.json and make the script executable:

{
  "hooks": {
    "PreToolUse": [
      { "matcher": "Bash",
        "hooks": [ { "type": "command", "command": "~/.claude/hooks/no-push-merge.sh" } ] }
    ]
  }
}

I ran the script against sample inputs: git push origin main, cd x && git -C . push and gh pr merge 3 exit 2; git status and ls exit 0. It is deliberately broad, so it will also block a harmless command that happens to contain both words, such as git log --grep=push. Because it matches the words anywhere in the command text, it also catches git -C . push and git 'push' origin main, the forms the deny rule misses. It cannot see inside a script file that runs git. Test it in your own setup before you trust it.

For the GitHub MCP server, a second hook entry with "matcher": "mcp__github__merge_pull_request" works, because MCP tools are named mcp__<server>__<tool> and a matcher containing only letters, digits, underscores and a few separators is read as an exact name. One thing to know: a hook cannot override a deny rule. A matching deny rule blocks the call whatever the hook returns.

Gating git push and merges with the free send gate

If you do not want to maintain your own script, the claude-code-send-gate repo (MIT, Node 20+, no dependencies) is a PreToolUse hook built around approval: the call is denied until you type a code in your own terminal. What it does for this problem, read from its source and run on 2026-10-26:

{
  "extraShellPatterns": ["\\bgit\\b.*\\bpush\\b"]
}

The pattern in the repo README is \bgit\s+push\b (written \\bgit\\s+push\\b inside the JSON string). It catches git push and git status && git push but not git -C . push. The broader pattern above catches that and git -c push.default=current push. Patterns are tested against each part of a command with its quoted text removed, so git commit -m "docs on git push" passes, and so does git 'push' origin main, which is a known miss. Other pushes, such as the GitHub MCP push_files tool, need an entry in extraGatedTools.

It is pattern matching on tool names and command text, not a sandbox, and I have tested it on macOS only. If you need a hard guarantee that nothing can push, remove the credentials or use branch protection on the remote.

What I would actually do

  1. Turn on branch protection so the remote refuses unreviewed merges. That works whatever the client does.
  2. Add the deny rules above for the common forms.
  3. Add a hook for the odd spellings and for the MCP merge tool.
  4. Run /hooks to confirm the hook is listed, then ask Claude to push to a scratch branch and confirm it is stopped.

More guides

The free gate and the paid kit

The send gate is free and MIT licensed: github.com/Kylefrc/claude-code-send-gate. If you want rule packs, a CLAUDE.md template and a memory system built around it, there is a USD 19 Operator Kit on the homepage. The gate works without it.

Source on GitHub The kit and store