Originally published at https://aicoding-guide.com.
Let Claude Code drive Git and sooner or later a conflict resolution ends in git push --force, taking someone else's commits with it. The way to stop that is a permissions.deny entry in settings.json.
Four lines in your project's .claude/settings.json cover the common spellings.
Key point
What you will learn
- The deny rules that block a force push
- How to cover
-f,--force-with-leaseand reordered options- What a deny rule does not protect against
The rules
{
"permissions": {
"deny": [
"Bash(git push --force:*)",
"Bash(git push -f:*)",
"Bash(git push --force-with-lease:*)",
"Bash(git push * --force:*)"
]
}
}
Bash rules match the command text, with * standing in for any text. Bash(git push --force:*) therefore matches git push --force origin main and anything else beginning that way.
Glossary
Deny precedence: rules are evaluated deny, then ask, then allow, and the first match decides. Rule specificity does not change that order, so an allow rule can never carve an exception out of a deny rule. Deny rules also apply in every permission mode, includingbypassPermissions, where allow rules have no effect at all.
Covering the spellings
Because the match is against the command text, a different option order does not hit the same rule. These are the forms worth listing:
| Command | Pattern that matches it |
|---|---|
git push --force origin main |
Bash(git push --force:*) |
git push -f origin main |
Bash(git push -f:*) |
git push origin main --force |
Bash(git push * --force:*) |
git push --force-with-lease |
Bash(git push --force-with-lease:*) |
--force-with-lease is safer than a bare --force, but it still overwrites the remote. If your team allows it, move that one line into ask rather than deny.
A deny rule is a safety net, not a wall
cd repo && git push --force, or a push buried in a shell script, does not start with the text your rule matches. For a branch that genuinely must not be rewritten, set branch protection on the remote — GitHub's "do not allow force pushes" — and treat the deny rule as what it is: a way to reduce local accidents.
Confirming it works
- Save the settings and start (or restart) Claude Code.
- Run
/permissionsand check that the four lines appear under deny, along with the settings file each came from. - Ask Claude to run
git push --force origin test-branch. - The call should be refused, with the reason shown.
Related rules
The same approach covers other commands that destroy work:
{
"permissions": {
"deny": [
"Bash(git reset --hard:*)",
"Bash(git checkout -- .:*)",
"Bash(git clean -f:*)"
]
}
}
For the overall rule design — allow, ask and deny, and how the scopes interact — see Claude Code permissions in settings.json. For a file-oriented example of the same mechanism, see Stop Claude Code reading your .env.
Summary
- Put
Bash(git push --force:*)and its siblings inpermissions.denyin.claude/settings.json - The match is on command text, so
-f,--force-with-leaseand reordered options each need a line - Deny always beats allow, in every mode, and personal settings cannot undo a project deny
- Pair it with branch protection on the remote for anything you truly cannot lose
Top comments (0)