Block Commands with Claude Code Hooks

Block commands with Claude Code hooks, Cybrec blog cover
Author

Author

Piotr Kaluzny

Head Instructor

Oct 16, 2026

You can tell Claude Code never to force push, and most of the time it will listen. But a rule in CLAUDE.md is guidance: Claude reads it as context and decides what to do, and on a long task a rule can slip. Some rules should not depend on that. Here is how to block commands with Claude Code hooks, so a risky command is stopped before it runs, every time, and where a hook's protection ends.

Why a hook and not an instruction

A hook is a command you write that Claude Code itself runs at a fixed point in its work. Claude does not decide whether to run it, so if something must always happen, or must never happen, a hook is how you guarantee it.

The point that matters here is PreToolUse. It runs before Claude uses a tool, such as running a shell command, so it can stop the action before it happens. A blocking PreToolUse hook also works in every permission mode, including when someone starts Claude Code with permission prompts turned off. Changing the mode does not get around it.

How a blocking hook works

Hooks live in the same settings files as the rest of your Claude Code configuration. Put a hook in your project's .claude/settings.json and commit it, and it applies to everyone working on the project. A hook names three things:

  • The event, here PreToolUse.
  • A matcher, which picks the tools the hook applies to. Bash means shell commands.
  • The command to run.

An optional if condition narrows the hook further, to one particular command. It uses the same pattern style as permission rules, such as Bash(git push*--force*), where each * stands for any text.

The command reports back with its exit code. Exit code 0 lets the action continue. Exit code 2 blocks it, and whatever the command wrote as an error goes back to Claude as the reason, so Claude knows why it was stopped and can adjust. Watch out for exit code 1: most programs use it for failure, but in a hook it does not block anything, and the action goes ahead. A hook that is meant to block has to exit with 2.

A hook that blocks a force push

A force push overwrites the history on GitHub, which makes it a good first command to block. Add this to .claude/settings.json in your project, or ask Claude to create the file for you:

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "if": "Bash(git push*--force*)",
            "command": "echo 'Force push is blocked by a project hook. Push normally instead.' >&2; exit 2"
          }
        ]
      }
    ]
  }
}

The hook fires before every shell command, the if condition picks out the force push, and the command prints a short reason and exits with code 2. The wildcards on both sides of --force catch the flag wherever it appears in the push, so git push origin main --force and git push --force-with-lease are blocked too.

The same file works on every system. Claude Code runs hook commands in bash on macOS and Linux, and in Git Bash on Windows. If Git Bash is not installed on a Windows machine, Claude Code falls back to PowerShell, and this command will not work as written.

Test it

Type /hooks inside Claude Code. It lists every active hook, grouped by event, and shows which settings file each one comes from. You do not need to restart after editing a hook: Claude Code picks up the change on its own.

Claude Code hooks menu listing the project hooks

Then ask Claude to force push the current branch. The hook stops the command before it runs, and Claude receives your reason. If Claude declines on its own because it knows a force push is risky, ask it to run the command anyway so you can see the hook at work.

Claude Code PreToolUse hook blocking a force push

Where a hook falls short

A hook is only as good as the patterns you write, so test it the way someone would try to get around it.

The pattern matches only the text you wrote. This one catches --force anywhere in a push, but git push -f is a different spelling of the same command and slips through. So does git push origin +main: the plus sign in front of the branch name forces the push without any flag at all. Add a handler for each form you want to stop, and check that your normal pushes still go through.

The if filter also works on a best-effort basis. When Claude Code cannot tell which commands a shell line will run, for example when it builds part of the command with $() or a variable, it runs your hook anyway. With a hook that blocks whenever it runs, that can stop a harmless command now and then.

For that reason, the Claude Code documentation recommends permission rules, not hooks, for a hard allow or deny. A deny rule such as Bash(git push*--force*) under permissions.deny in the same settings file also blocks in every permission mode. The two work well together: the deny rule as the hard stop, and the hook when you want to tell Claude why, or when the check needs more logic than a single pattern.

Conclusion

To block commands with Claude Code hooks, add a PreToolUse hook on Bash to your project settings, narrow it with an if condition, and make the command exit with code 2 and a clear reason. It runs every time, in every permission mode, and Claude learns why it was stopped. Check it with /hooks, test it the way someone would try to get around it, and pair it with a deny rule for the commands that must never run.

Relevant Training

Hooks are covered in Claude Code Agents and Automation, part of Cybrec's Claude AI training program, together with subagents, parallel sessions, and automation.

0 Comments

Block commands with Claude Code hooks, Cybrec blog cover
Author

Author

Piotr Kaluzny

Head Instructor

Oct 16, 2026