Skip to content

Drive each declared hook to a refusal, in a tree nobody is standing in - #26

Merged
HackingGate merged 1 commit into
mainfrom
feat/probe
Aug 14, 2026
Merged

Drive each declared hook to a refusal, in a tree nobody is standing in#26
HackingGate merged 1 commit into
mainfrom
feat/probe

Conversation

@HackingGate

Copy link
Copy Markdown
Owner

The other half of #11. uphold probe drives each declared hook to a refusal, so a gate whose rejection path has never been demonstrated stops reading like a gate that keeps finding nothing.

The case is not hypothetical: an entry declared as gofmt -l . can never exit non-zero, because gofmt -l prints its findings and exits 0. Two repositories in the fleet that produced #11 had one, and every report anybody reads showed them green.

What one probe does

  1. plant the fixture the hook must refuse, run that hook alone, expect non-zero;
  2. put the clean fixture in its place, run it again, expect zero.

Isolation is what makes step 1 an answer about the hook rather than about the stage: the runner is asked for one id, so a non-zero exit is that hook refusing and not a neighbour. Nothing has to parse a report to find out who spoke.

# policy/hooks.toml
[[probe]]
id = "gofmt"
path = "probe/fixture.go"
refuses = "package main\nfunc  main( ){}\n"
allows = "package main\n\nfunc main() {}\n"

Four verdicts, and they are four different things to do next: a demonstrated gate; a gate demonstrated in one direction only, because no allows fixture was written; a hook that accepted what it is declared to refuse; and a hook that refused the clean fixture too, whose refusal therefore says nothing about what it was given.

In a tree nobody is standing in

The fixture is planted in a git worktree at HEAD, never in the working tree. A probe that planted it where somebody is working would leave one behind the first time it was interrupted -- in a tree whose hooks would then refuse the next commit for a reason nothing in the tree explains. A test asserts the working tree is untouched and clean afterwards.

Fixtures are written down, not generated

uphold knows what its own rules match and knows nothing about gofmt, ruff, or a hook somebody wrote this morning -- and the hooks worth probing are exactly the ones it knows nothing about. A fixture in a file is also reviewable, which matters more than the typing it saves.

An empty refuses is refused: a hook that accepted an empty fixture would be reported as unable to fail. A probe naming a hook nothing declares is refused: it would drive nothing while reading as coverage. The count of declared hooks with no probe prints every run -- this repository's own says two of twenty-seven.

One finding, from running it where it will be used

The first version could not run from inside a hook at all. A hook runner exports GIT_INDEX_FILE, GIT_DIR and friends, several of them relative to the repository the hook fired in; inherited, they point every git this module runs at the wrong index. The worktree could not be created, and where it could, the staging would have gone into somebody else's index -- the same accident with none of the noise.

Git's environment is stripped from every child now, stripped rather than overridden: the list of what git puts in an environment is git's, and an override answers only for the names somebody remembered. There is a regression test that runs probe with a hook runner's environment set.

Verified on consumers

scan and check byte-identical under the released binary and this build, all exit 0. probe driven on a consumer with no probes declared reports the denominator and leaves the tree clean.

404 tests pass, 10 of them new. The nine driving the verdict logic use a stub runner on PATH, so no case depends on which runner is installed on the machine running the suite.

With this in, #11 has both halves.

`uphold probe`. A hook that cannot fail reports the same green tick as a hook
that keeps finding nothing, run after run, for as long as nobody plants what it
is supposed to catch. No report anywhere tells the two apart. The case this was
written for is not hypothetical: an entry declared as `gofmt -l .` can never
exit non-zero, because `gofmt -l` PRINTS its findings and exits 0, and two
repositories in the fleet that produced the request had one.

Each probe drives one hook to both verdicts: plant the fixture it must refuse,
run that hook alone, expect non-zero; put the clean fixture in its place, run it
again, expect zero. Asking the runner for ONE id is what makes the first half an
answer about the hook rather than about the stage -- a non-zero exit is that
hook refusing and not a neighbour, so nothing here has to parse a report to find
out who spoke.

Four verdicts, and they are four different things to do next: a demonstrated
gate; a gate demonstrated in one direction because no `allows` fixture was
written; a hook that ACCEPTED what it is declared to refuse; and a hook that
refused the clean fixture too, whose refusal therefore says nothing about what
it was given.

It happens in a `git worktree` at HEAD and never in the tree the operator is
standing in. A probe that planted a fixture in the working tree would leave one
behind the first time it was interrupted -- in a tree whose hooks would then
refuse the next commit for a reason nothing in the tree explains. A test asserts
the working tree is untouched and clean afterwards.

Fixtures are written down in `policy/hooks.toml` rather than generated. This
binary knows what its own rules match and knows nothing about `gofmt`, `ruff`,
or a hook somebody wrote this morning -- and the hooks worth probing are exactly
the ones it knows nothing about. A fixture in a file is also reviewable, which
matters more than the typing it saves. An empty `refuses` is refused, because a
hook that accepted an empty fixture would be reported as unable to fail; a probe
naming a hook nothing declares is refused, because it would drive nothing while
reading as coverage.

The denominator is printed every run. "Two hooks were probed" means one thing
beside two declarations and another beside twenty, and this repository's own run
says two of twenty-seven.

Two probes ship here, chosen for what each demonstrates: `content-policy`
against a rule that arrives from a bundled set, which is the half of an
inherited set that no file in this tree shows; and `trailing-whitespace`, a
third-party hook, because every hook that is not this binary's own is one whose
failure path nobody here has ever seen.

One finding, and it arrived the way the good ones do -- from running the suite
in the environment the command will actually be used in. A hook runner exports
`GIT_INDEX_FILE`, `GIT_DIR` and friends, several of them RELATIVE to the
repository the hook fired in. Inherited, they point every `git` this module runs
at the wrong index: the worktree could not be created at all, and where it
could, the staging would have gone into somebody else's index -- the same
accident with none of the noise. Git's environment is stripped from every child
now, stripped rather than overridden, because the list of what git puts in an
environment is git's and an override answers only for the names somebody
remembered.

The runner is detected from the configuration AND from PATH, because either
alone is a wrong answer: a repository with a lefthook config and no lefthook
installed cannot be probed, and a machine with three runners installed says
nothing about which one a repository uses. No runner is exit 2 -- a hook that
could not be run has not been shown to refuse anything.

Verified on consumers: `scan` and `check` byte-identical under the released
binary and this build, all exit 0. `probe` driven on a consumer with no probes
declared reports the denominator and leaves the tree clean.
@coderabbitai

coderabbitai Bot commented Aug 14, 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: 79 minutes

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?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8c48179d-0ea3-4d4c-9958-56643b601db3

📥 Commits

Reviewing files that changed from the base of the PR and between 1d7ad5c and fdb5260.

📒 Files selected for processing (5)
  • docs/REFERENCE.md
  • policy/hooks.toml
  • src/main.rs
  • src/probe.rs
  • tests/probe_cli.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

❌ Patch coverage is 89.56835% with 29 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.73%. Comparing base (1d7ad5c) to head (fdb5260).

Files with missing lines Patch % Lines
src/probe.rs 90.29% 26 Missing ⚠️
src/main.rs 70.00% 3 Missing ⚠️

❌ Your patch status has failed because the patch coverage (89.56%) is below the target coverage (100.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files
@@            Coverage Diff             @@
##             main      #26      +/-   ##
==========================================
+ Coverage   88.70%   88.73%   +0.02%     
==========================================
  Files          27       28       +1     
  Lines        8633     8911     +278     
==========================================
+ Hits         7658     7907     +249     
- Misses        975     1004      +29     

☔ 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.

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