Skip to content

fix(claude-permissions): drop the --force-with-lease deny, enforce it in the hook - #272

Merged
kyle-sexton merged 2 commits into
mainfrom
fix/claude-permissions-drop-lease-deny
Jul 25, 2026
Merged

fix(claude-permissions): drop the --force-with-lease deny, enforce it in the hook#272
kyle-sexton merged 2 commits into
mainfrom
fix/claude-permissions-drop-lease-deny

Conversation

@kyle-sexton

@kyle-sexton kyle-sexton commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Summary

The permission floor denied every --force-with-lease spelling — including the forms that are actually safe. This removes those four patterns and moves enforcement to the one place the distinction can be expressed.

Why deny cannot do this job

Claude Code's Bash rules are whole-string globs with * as the only metacharacter, and precedence is fixed (permissions):

Rules are evaluated in order: deny, then ask, then allow. The first match in that order determines the outcome, and rule specificity doesn't change the order.

A broad deny rule like Bash(aws *) blocks every matching call, including calls that also match a narrower allow rule like Bash(aws s3 ls), so a deny rule can't carry allowlist exceptions.

No negation, no exceptions. So a deny here is all-or-nothing.

Why that matters — the forms are not equivalent

git-push(1), "A general note on safety":

supplying this option without an expected value, i.e. as --force-with-lease or --force-with-lease=<refname> interacts very badly with anything that implicitly runs git fetch … this is trivially defeated if some background process is updating refs in the background.

Only --force-with-lease=<refname>:<expect> states the expectation, and it is the one form git does not mark experimental. A single glob cannot deny the unsafe spellings and permit the safe one — so the floor denied all of them, which is why a correctly-formed safe push was blocked in practice.

What replaces it

The guardrails plugin's block-dangerous-git PreToolUse hook, which parses the argv and can therefore make the distinction:

  • Blocks any lease that resolves against something movable at push time — bare, =<refname>, or an =<refname>:<expect> whose <expect> is a name like origin/main, HEAD, or refs/remotes/....
  • Permits an expectation git cannot resolve to something newer: an object id, or the empty string (asserting the ref must not exist).
  • Honors --force-if-includes (git 2.30+) as git's documented mitigation for the unpinned forms.
  • Tracks the last-wins negations git documents as --[no-]force-with-lease and --[no-]force-if-includes.

Claude Code's own docs name a PreToolUse hook as the mechanism for exactly what globs cannot express.

Ordering — this lands second, deliberately

Removing the deny on its own would have been a net widening. The hook permitted every lease form until melodic-software/claude-code-plugins#1275, so dropping the blunt rule first would have exposed the unsafe spellings with nothing catching them. The hook hardens first; the blunt rule comes out after.

README

"Force/destructive spellings stay covered by deny, which always wins" is corrected rather than deleted — it still holds for every other force spelling. The carve-out is recorded beneath it with the reason and both upstream citations, so the next reader does not re-add the patterns.

Verification

  • claude-permissions.json validates; the four removed entries are the only change to it (2 Bash, 2 PowerShell mirrors).
  • Pre-commit gates green: biome, editorconfig, gitleaks, typos, markdownlint.

Related

No linked issue: this is the second half of the melodic-software/claude-code-plugins#1275 change, not a separately tracked defect.

… in the hook

The floor denied every `--force-with-lease` spelling, including the forms
that are actually safe. It cannot do otherwise: Claude Code's Bash rules
are whole-string globs with `*` as the only metacharacter, and precedence
is fixed at deny then ask then allow, so a deny entry cannot carry an
allowlist exception.

That matters because the forms are not equivalent. git-push(1) warns that
`--force-with-lease` and `--force-with-lease=<refname>` lease against the
remote-tracking ref and are "trivially defeated if some background process
is updating refs in the background", while
`--force-with-lease=<refname>:<expect>` states the expectation and is the
one form git does not mark experimental. No single glob denies the unsafe
spellings and permits the safe one.

So the four patterns (two Bash, two PowerShell) come out, and the precise
check lives where it can actually be expressed: the `guardrails` plugin's
`block-dangerous-git` PreToolUse hook, which parses the argv, blocks any
lease that resolves against something movable at push time, honors
`--force-if-includes`, and tracks the last-wins negations. Claude Code's
own docs name a PreToolUse hook as the mechanism for what globs cannot
express.

Ordering note: this lands AFTER the hook, not before. Removing the deny on
its own would be a net widening — the hook permitted every lease form
until melodic-software/claude-code-plugins#1275, so dropping the blunt
rule first would have exposed the unsafe spellings with nothing catching
them.

The README's "Force/destructive spellings stay covered by `deny`, which
always wins" is corrected rather than deleted: it still holds for every
other force spelling, and the carve-out is recorded beneath it with the
reason and both upstream citations.
@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 59s —— View job


Code Review

  • Gather context (CLAUDE.md, REVIEW.md, PR diff)
  • Read changed files
  • Review claude-permissions.json changes
  • Review README.md changes
  • Post findings

The reasoning here is technically sound and the documentation is thorough. Two observations worth considering:


🟡 Nit — Consumer prerequisite is implicit, not stated

README.md lines 42–46

The carve-out section explains why the deny rules were removed and what the hook does, but it doesn't state that the guardrails plugin's block-dangerous-git hook is a consumer-facing prerequisite for this carve-out to remain safe. A reader encountering only this README could understand the hook as an implementation detail of the plugin rather than a requirement they must satisfy. If a consumer syncs this component without having deployed the hook, --force-with-lease in any form is unrestricted in their environment.

Suggest one sentence along the lines of: "Consumers of this component must have the guardrails plugin's block-dangerous-git PreToolUse hook active for this carve-out to be safe — without it, all lease spellings, including the unsafe bare and =<refname> forms, pass through unblocked."


🟡 Nit — deny description still implies force-push coverage is complete

README.md line 18

The deny bullet in the Shape section lists "destructive git verbs (force-push, …)" without qualification. A reader skimming that prose wouldn't know force-push coverage has a hole until they reach the carve-out heading several paragraphs later. Adding a parenthetical like (force-push — except lease forms, see below) keeps the high-level description accurate without redundancy.


Everything else looks correct. The core argument — glob precedence is fixed at deny→ask→allow with no exception mechanism, so the per-form distinction --force-with-lease=<refname>:<oid> vs bare lease needs a hook — is accurate per the Claude Code permissions docs. The removed patterns (2 Bash + 2 PowerShell) are exactly the entries that were over-blocking the safe form. The existing deny entries for --force and -f remain intact and don't accidentally match --force-with-lease (the glob --force * requires a space after --force, which the option name doesn't have). The schemaVersion and JSON structure are untouched. No secrets or credentials introduced.

The merge-ordering dependency on plugins#1275 is correctly identified and noted in the PR body; the one documentation gap is that this repository's README is the long-lived artifact, and the ordering rationale lives only in the ephemeral PR description.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 27c52c034a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/claude-permissions/README.md Outdated
An explicit `--force-with-lease=<refname>:<expect>` is not categorically
safe. A movable name in the `<expect>` slot (`origin/main`, `HEAD`,
`@{u}`) is resolved when the push runs, so a background fetch advances it
first and the lease passes while clobbering unseen work -- the same hole
the bare and `=<refname>` forms have.

Narrow the claim to what git actually guarantees: an object id, or the
empty string asserting the ref must not already exist. This also matches
the hook that now carries the enforcement, which rejects named
expectations.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012nZuFpcEQqj9kpVew4imts
@claude

claude Bot commented Jul 25, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 56s —— View job


Code Review

  • Gather context (CLAUDE.md, REVIEW.md, PR diff)
  • Read changed files (both commits)
  • Check if prior nits were addressed
  • Check if Codex finding was addressed
  • Post findings

Reviewing HEAD 7573687 against base.

Codex P2 — Addressed ✅

Commit 7573687 correctly resolved the Codex finding. The prior text said --force-with-lease=<refname>:<expect> "states the expectation and is safe" without qualification. The current text at README.md lines 39–44 now reads:

A --force-with-lease=<refname>:<expect> is safe only when <expect> is something git cannot resolve to a newer value — an object id, or the empty string asserting the ref must not already exist. A movable name in that slot (--force-with-lease=refs/heads/main:origin/main, :HEAD, :@{u}) is resolved when the push runs, so a background fetch advances it first and the lease passes while clobbering unseen work: the same hole the bare form has.

This is accurate, matches the hook's enforcement logic ("requires an immutable <expect>"), and gives concrete examples of the unsafe movable forms. Well done.

Prior nit #2 — Addressed ✅

The allow bullet now ends with "with one deliberate carve-out, below" (line 28), which routes readers to the carve-out section before they'd miss it.

Prior nit #1 — Still open 🟡

README.md lines 46–49

The hook is described as where the enforcement lives, but it's still framed as an implementation fact rather than a consumer requirement. A consumer who syncs this component without having deployed guardrails/block-dangerous-git will have no coverage for any --force-with-lease spelling — including the unsafe bare and =<refname> forms. The carve-out section explains the why well; it still doesn't say the hook is a prerequisite the consumer must satisfy.

Suggested addition after the current last sentence of the carve-out section:

Consumers of this component must have the guardrails plugin's block-dangerous-git PreToolUse hook active; without it, all lease spellings — including the unsafe bare and =<refname> forms — pass through unblocked.

Fix this →

JSON — Correct ✅

All four --force-with-lease patterns (2 Bash, 2 PowerShell) are absent from claude-permissions.json. The --force, -f, and other destructive entries are intact. schemaVersion is unchanged. No --force-with-lease allow entries were introduced. The file is well-formed JSON.


Everything is technically accurate. The one remaining item is the nit above — a consumer reading only this README still can't tell that the hook is a hard dependency, not an optional enhancement.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7573687245

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread components/claude-permissions/README.md
@kyle-sexton
kyle-sexton merged commit 39fe599 into main Jul 25, 2026
44 of 45 checks passed
@kyle-sexton
kyle-sexton deleted the fix/claude-permissions-drop-lease-deny branch July 25, 2026 23:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant