Skip to content

Refuse two shim tables naming one command, which silently dropped one of them - #79

Merged
HackingGate merged 1 commit into
mainfrom
refuse-duplicate-shim-command
Aug 21, 2026
Merged

Refuse two shim tables naming one command, which silently dropped one of them#79
HackingGate merged 1 commit into
mainfrom
refuse-duplicate-shim-command

Conversation

@HackingGate

Copy link
Copy Markdown
Owner

shim::run resolves the declared shims into a map keyed by command name, so a second [[shim]] naming a command already declared replaced the first and said nothing.

Reproduced

A policy declaring gh twice -- once for pr:create with --body, once for issue:close with --comment:

uphold scan policy checks passed
uphold shim gh issue close 1 --comment X refused by the checker
uphold shim gh pr create --body X exec'd the real gh, unexamined

Text the author had written a table for reached a forge with nothing in front of it, and the run that was supposed to notice reported a clean policy. The surviving table is whichever the map happened to keep.

Why refused rather than merged

Merging is a guess about which vocabulary wins where two tables disagree, and they disagree exactly where it matters:

  • -c on gh pr review is a boolean -- "Comment on a pull request"
  • -c on gh issue close takes a value -- "Leave a closing comment"

A merged text_flags misreads one of those whichever way it is built: either -c swallows the next argument on a review, or a closing comment is published unread.

That collision is the reason somebody writes a second table in the first place, so the refusal has to make it visible to the person writing it rather than resolve it silently.

What this does not fix

text_flags is per-command and gh's flag meanings are per-verb. Closing that is a schema question, not a load check, and it is filed separately -- along with the verbs that publish text and are matched by no table today.

Blast radius

No policy in the surveyed workspace declares one command twice. Nothing that loads today stops loading.

Verification

  • cargo test -- 558 pass, 0 fail (556 + 2)
  • cargo clippy --all-targets, cargo fmt --check -- clean
  • every lefthook pre-commit command run directly, Python suite included

New tests: the duplicate is refused and the message says what the silence cost; one table may name every verb for its command; and two tables for two different commands still load.

… of them

`shim::run` resolves the declared shims into a map keyed by command name, so a
second `[[shim]]` naming a command already declared replaced the first and said
nothing. The surviving table is whichever the map happened to keep, and the lost
one is a set of verbs the repository believes are guarded.

Reproduced before this was written. A policy declaring `gh` twice -- once for
`pr:create` with `--body`, once for `issue:close` with `--comment`:

  uphold scan                              policy checks passed
  uphold shim gh issue close 1 --comment X refused by the checker
  uphold shim gh pr create --body X        exec'd the real gh, unexamined

Text the author had written a table for reached a forge with nothing in front of
it, and the run that was supposed to notice reported a clean policy.

WHY REFUSED RATHER THAN MERGED.

Merging is a guess about which vocabulary wins where two tables disagree, and
they disagree exactly where it matters: `-c` is a BOOLEAN on `gh pr review`
("Comment on a pull request") and takes a VALUE on `gh issue close` ("Leave a
closing comment"). A merged `text_flags` misreads one of those whichever way it
is built -- either `-c` swallows the next argument on a review, or a closing
comment is published unread.

That collision is the reason somebody would write a second table in the first
place, so the refusal has to make it visible to the person writing it rather
than resolve it silently. What the refusal cannot do is fix it: `text_flags` is
per-command and gh's flag meanings are per-verb, and closing that gap is a
schema question rather than a load check.

No policy in the surveyed workspace declares one command twice, so nothing that
loads today stops loading.

558 tests pass, clippy and fmt clean, every lefthook pre-commit command run
directly.
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@HackingGate, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 36 minutes

Limit details: You’ve used the included review currently available.

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

Wait for the limit to reset, then comment @coderabbitai review or push new commits to the PR.

An organization admin can change what happens after included review limits in Billing.

How do review limits work?

CodeRabbit enforces per-developer PR review limits within each organization.

For paid Pro and Pro+ reviews, CodeRabbit uses a developer's included PR review attempts over the past 7 days to set the current hourly allowance. At typical activity levels, the full plan allowance applies. Higher sustained activity can lower the allowance until earlier attempts leave the 7-day window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 0eabb1f2-ed9b-4c91-9992-63f14f7cb6c5

📥 Commits

Reviewing files that changed from the base of the PR and between 08ef511 and 948600c.

📒 Files selected for processing (1)
  • src/config.rs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.45%. Comparing base (08ef511) to head (948600c).

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #79      +/-   ##
==========================================
+ Coverage   90.42%   90.45%   +0.02%     
==========================================
  Files          35       35              
  Lines       11225    11259      +34     
==========================================
+ Hits        10150    10184      +34     
  Misses       1075     1075              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@HackingGate
HackingGate merged commit 4b9aeb9 into main Aug 21, 2026
12 checks passed
@HackingGate
HackingGate deleted the refuse-duplicate-shim-command branch August 21, 2026 11:03
HackingGate added a commit that referenced this pull request Aug 21, 2026
…ommand does (#81)

A `[[shim]]` names one `text_flags` for a whole command, and a command's flags do
not mean one thing. On `gh`, `-c` is a BOOLEAN on `pr review` -- "Comment on a
pull request" -- and TAKES A VALUE on `issue close` -- "Leave a closing comment".
Name it once for the table and one of the two is read wrong:

  named      `gh pr review -c -b "body"` reads `-c` as consuming `-b`, and the
             body being published goes unread
  not named  `gh issue close --comment "text"` publishes with nothing in front
             of it

Both are false negatives in the seam that exists to prevent one, and #79 closed
the workaround -- a second `[[shim]]` for the same command -- because merging two
vocabularies is the same guess in a different place.

So a table may now carry entries:

    [[shim]]
    command = "gh"
    match = ["pr:review", "issue:close"]
    text_flags = ["-t", "--title", "-b", "--body"]

      [[shim.verbs]]
      match = ["issue:close"]
      text_flags = ["-c", "--comment"]

The entry's lists REPLACE the table's for the verbs it names rather than adding
to them, which is the rule `allowed_scripts` already follows: what is declared
beside the narrower thing is the whole truth for it. A union would mean a
vocabulary nobody wrote -- here `issue close --body`, which the real command does
not accept, and reading a flag a command will not take is the shim claiming to
have checked a subject that was never published.

`target_flags` is deliberately not overridable. `-R`/`--repo` means the same
thing on every verb, and a per-verb answer to "which repository is this going
to" would be a way to publish somewhere the table did not expect.

WHY THE VERB IS KNOWN IN TIME. `reading` locates the subcommand by trying both
arities for every option it does not know and matching under either -- "matching
under either reading errs towards checking" -- so identification never needed the
vocabulary it is about to select. Only collection does, and `for_verb` runs
between them. A table with no entries borrows itself, which is every shim
written before this.

Load refuses an entry naming a verb the table's own `match` does not cover: the
shim never stands in front of that invocation, so the flags classify nothing.
That is usually a verb somebody meant to add to `match` and added here instead.

WHAT THIS DOES NOT DO, AND THE ORDER THAT FORCED IT. uphold's own policy does not
adopt this yet, and the attempt is why. Writing `[[shim.verbs]]` into
policy/principles.toml made every `git` command in this tree fail closed: the
installed binary IS the shim, it predates the field, and a policy it cannot parse
is a policy that refuses everything. Its own .pre-commit-config.yaml pins the
same version, so CI would have failed identically. The capability ships first and
policies adopt after a binary that understands it is pinned -- the same ordering
a new bundled set needs, for the same reason.

564 tests pass, clippy and fmt clean, every lefthook pre-commit command run
directly.
HackingGate added a commit that referenced this pull request Aug 21, 2026
A `[[shim]]` named one `text_flags` for a whole command, and a command's flags
do not all mean one thing. On `gh`, `-c` is a boolean on `pr review` and takes a
value on `issue close`; naming it once for the table reads one of the two wrong,
and either way the mistake is a false negative in the seam that exists to prevent
one.

`[[shim.verbs]]` lets the verbs whose grammar differs say so. The entry's lists
replace the table's for those verbs rather than adding to them -- the rule
`allowed_scripts` already follows, because a union would mean a vocabulary
nobody wrote. `target_flags` stays table-wide: a per-verb answer to which
repository something is going to would be a way to publish somewhere the table
did not expect.

Also in this release, from #76 and #78: a baseline entry may be signed
`path | owner | reason` and required to be, and a baseline line that does not
parse is refused rather than skipped -- the skip removed a ratchet silently, and
the staleness check could not see it because a dropped entry was never listed.
#79 refuses two shim tables naming one command, which used to drop one of them.

TAKING IT. Nothing existing changes shape: a policy written for 1.7.0 resolves to
exactly the rules it resolved to before, and no default changed. But a policy
that USES `[[shim.verbs]]` cannot be read by an older binary, and the installed
binary is the shim -- so a tree whose policy adopts the field before its shims
are reinstalled has every git command failing closed. Bump the pin, reinstall,
then adopt.

Documented pins move to v1.8.0 in README.md and hooks/lefthook.yml.
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.

2 participants