Claude Code can run shell commands for you, including git commit. Many developers prefer to review changes and write the commit themselves. An instruction in CLAUDE.md such as "never commit" is only a request, and users report that Claude sometimes ignores it.
The reliable approach is to enforce the rule in the tool itself. This article covers two mechanisms: permission deny rules and a PreToolUse hook. It also covers where each one falls short.
Block Commits with a Permission Deny Rule
Claude Code has a permissions system that decides which tools and commands it may run. A deny rule rejects a matching command outright, and deny rules take priority over allow rules. Put the rule in .claude/settings.json to share it with a team, or in .claude/settings.local.json for yourself only.
{
"permissions": {
"deny": [
"Bash(git commit)",
"Bash(git commit *)",
"Bash(git push *)"
]
}
}The space before the asterisk matters. Bash(git commit *) matches git commit -m "msg" and any other arguments. The older :* suffix does the same thing but is deprecated, so use the space form in new configurations. The example also denies git push, a common companion rule.
Watch for broad allow rules
Some setups reduce permission prompts with a wide allow rule such as Bash(git:*). That rule matches every Git command, including git commit. With the deny rule above it is still blocked, but only because deny rules take priority. Do not rely on allow-list wording alone to keep commits out.
Deny Git tools from MCP servers too
If you have connected a Git tool through the Model Context Protocol (MCP), Claude can commit through it without ever running a Bash command. The Bash rules do not cover those tools. Add deny rules for the commit tools that server exposes, using the tool names Claude Code lists for it.
Add a PreToolUse Hook for Stricter Matching
A hook is a command that Claude Code runs automatically at defined points. A PreToolUse hook runs before a tool call. It receives the call details as JSON on standard input, and if it exits with code 2, Claude Code blocks the call and passes the hook's error message back to Claude.
This is useful because you can match with a regular expression and give Claude a clear reason. First, create .claude/hooks/block-commit.sh. It requires jq, a command-line JSON tool.
#!/usr/bin/env bash
# Reads the tool call JSON from stdin and blocks git commit.
cmd=$(jq -r '.tool_input.command // ""')
if echo "$cmd" | grep -Eq '\bgit\b.*\bcommit\b'; then
echo "Blocked: commits are made by the user, not by Claude. Leave changes staged or unstaged." >&2
exit 2
fi
exit 0Make it executable with chmod +x .claude/hooks/block-commit.sh. Then register it in .claude/settings.json:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/block-commit.sh"
}
]
}
]
}
}The pattern is deliberately broad. It also catches forms such as git -C subdir commit, which a simple prefix rule can miss. The cost is occasional false positives, for example git log --grep commit. Tighten the expression if that bothers you.
Test it
Start a session and ask Claude to commit a small change. The call should be refused and the hook's message should appear. Guides on blocking dangerous Git commands use the same pattern and recommend testing with a command that should match, such as git push origin main. Hook behavior can change between releases, so check the current Claude Code documentation. One guide on this pattern referred to version 2.1.284.
Limitations to Understand
Both mechanisms inspect the command Claude asks to run, not what that command does afterwards. If Claude writes a script that calls git commit internally and then runs the script, the rule sees only the script name. A hook that reads script contents narrows this gap but does not close it.
Other gaps are worth knowing about:
- Claude may work in its own Git worktree, where ordinary Git commands, including
commit, are still available to it. - Rules in a local settings file apply only to your machine, so teammates need the shared project file.
- Git tools reached through MCP need their own deny rules.
If you need a hard guarantee, add a boundary outside Claude Code, such as a sandbox or container without write access to the repository's .git directory, or credentials that cannot push.
Conclusion
Do not rely on CLAUDE.md to stop commits. Add Bash(git commit) and Bash(git commit *) to the deny list, deny any Git tools from MCP servers, and add a PreToolUse hook for variants the prefix rules miss.
These controls reliably stop the direct route. They do not stop indirect execution through scripts or a separate worktree, so use environment-level isolation if that matters for your project.
