Skip to content

A branch that carries the five open config.rs and shim changes at once, so the refactor above them has one tree to stand on - #130

Closed
HackingGate wants to merge 12 commits into
mainfrom
integration-2026-09-02
Closed

A branch that carries the five open config.rs and shim changes at once, so the refactor above them has one tree to stand on#130
HackingGate wants to merge 12 commits into
mainfrom
integration-2026-09-02

Conversation

@HackingGate

Copy link
Copy Markdown
Owner

A branch that carries the five open config.rs and shim changes at once, so the refactor above them has one tree to stand on.

Merges, in PR-number order, from origin/main:

Conflicts resolved

None. Every merge applied without a conflict marker:

Nothing was dropped and nothing was rewritten by hand, so each branch's own
intent is on this tree as its author wrote it. In particular this branch edits
no policy allowlist of its own: the claude.ai question that the first
foreign-host reading raised is answered in #124 at 54b4260, not here.

What was checked, not assumed

Note on the installed binary

#124 puts a foreign_hosts line in policy/principles.toml, and an uphold
predating that field refuses the whole file rather than skipping the line.
A git or gh shim resolving to an older binary therefore fails closed in
this tree until cargo install --path . is run from this branch. That is the
trade #124 documents; it is named here because it bites anyone who checks this
branch out with a released binary on PATH.

This is a merge-order aid

It exists so a refactor of src/config.rs has one tree carrying all five
changes, not to be the thing that lands them. The five pull requests are the
reviewable units and each should be merged on its own terms. Once they are all
on main this branch has nothing left to say, and the maintainer should close it
unmerged.

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd

… token

`prevent-author-mismatch` was declared by hand in 8 policy files across one
consuming superproject and its submodules, byte for byte in every one: same id,
same built-in, one `pre-commit` stage, no parameters. The guard is already
compiled in, so what was missing was a set to carry the declaration.

`mismatched-author` is that set, and it is its own rather than a rule inside an
existing one. `unowned-push` asks where a ref is going; `host-identity` reads
the running machine and then searches committed content for what it read, which
is why it ships `stages = []`; `commit-message-residue` is about the text a
commit records. This one asks who is writing, read off `git var` before a
message exists. Its ceiling is the single `pre-commit` stage, so it cannot grow
a second one without a diff on the line that says so.

The header writes down what it costs -- a deliberate `--author` is refused as
readily as an accidental one -- and the one thing it cannot do: with no global
`user.email` there is no expectation to compare against, so it says so and
declines. That is the container case verbatim, and a guard that quietly does
not run in its own scenario reads like one that ran and found nothing.

`no-hardcoded-home-paths` takes the leading `(?:^|[^A-Za-z0-9_.-])` boundary the
same fleet had added to its shadow. Without it the pattern matches `/home`
wherever those five characters land inside a longer token, and each such
finding is a report on a file holding no home path at all. The corpus records
both halves: the two ways a real path still opens a match, and the two shapes
the boundary now lets through.

Not taken: the same shadow's carve-out for a `u` placeholder user. That is a
house convention tied to one repository's own record, and a bundled rule
exempting a one-letter username would be exempting a real one.

`policy/base/sets.lock.json` regenerated from the binary, not edited.

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
`uphold hook` consulted the literal rules and the text-capable guards and
never the prose rules, so every `prose_regexp` rule was dark at the MCP
seam -- the whole bundled `prose-shapes` set among them, and those sets
declare `command.before = ["gh", "git push"]` precisely because the text
they judge is what an agent posts through `mcp__github__*`.

`text::failures_in` excludes the prose rules on purpose, and each of the
other whole-text seams carried its own compensating call to
`prose::over_text` beside it. Four seams assembled the same ingredients by
hand, so a rule kind added to three of them was silent in the fourth with
nothing anywhere to say so.

`text` now owns the answer. `Judged` names every kind of rule a piece of
published text is judged by, `Seam::consults_every` says which kinds each
of the four seams asks, and `text::judged` is the one assembly the three
whole-text seams call. The shim keeps its own loop, because it holds a
subject and a per-rule scope that function is not given, but it classifies
each rule through `Judged::of` and asks the same table -- which also closes
a second hole of the same shape: its editor pass dispatched `regexp` and
`require_regexp` to the pattern checker and left `prose_regexp` to fall
through to the `exec` consultation, where it was refused as a dispatch hole.

Exit codes and report text are unchanged everywhere they already existed.
The kinds run in one order, literals then guards then prose, which is the
order each seam already printed the kinds it ran.

Closes #111

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
Five ways past this seam, reproduced against the built binary standing on
PATH. Four of them ended in exit 0 with nothing printed, which is the shape
of failure this tool exists to refuse.

A renamed verb was one of them. A `match` list names verbs literally and
every command here lets a person rename one, so `git -c alias.p=push p
origin HEAD:refs/heads/x` and a persisted `[alias] p = push` both presented
the verb `p`, matched nothing, and exec'd a push to a public forge
unexamined. Where nothing matches, and only there, the word is resolved once
against the command's own alias mechanism and matched again against the
expansion. Asked of the real command rather than of whatever PATH resolves,
because PATH resolves to this shim. A shell alias and a lookup that failed
are could-not-looks and not absences: exit 2, since what a shell runs is not
an invocation any table can read and it may well be a push.

The refspec collector read one grammar where the matcher reads two. The
matcher tries both arities for an option it cannot classify and matches
under either; the collector took the bare reading and skipped two words, so
`git --attr-source HEAD push origin` checked the remote's nickname while the
branch went out unread. Both readings now, and where they disagree about
which names are being published the answer is exit 2 rather than a guess.
That needed git's own grammar filling in: the value-taking globals it was
missing, and the `push` vocabulary, without which the disagreement fires on
`git push -f origin topic`.

A scope that could not be evaluated stood every checker down. `public-target`
is the one predicate that asks somebody else, and a forge that cannot be
asked answered `false` -- the same word as "the policy decided these checks
do not apply here". So `prevent-unowned-target`, whose own contract is exit 2
on a destination it could not resolve, was skipped exactly where the lookup
failed. Three answers now, and the third refuses before the command runs.
`unresolved = "run"` on a `[[shim]]` opts back into the old behaviour and is
refused at load beside a scope that cannot produce that answer.

`--all`, `--mirror` and `--tags` name no refspec and publish many, and the
fallback read HEAD, so a mirror push of forty branches was checked as one.
They are enumerated with `for-each-ref` and each name is read. A leading `+`
is grammar and no part of a name, and leaving it on made the force-push the
way past a rule standing in front of the branch it names. `send-pack:*`
joins `push:*` in the shipped table and in the checkers that name it: the
plumbing publishes the same names from the same positions.

The two stand-downs that remain -- an unresolvable subcommand, and
`UPHOLD_ALLOW=all` -- keep their behaviour and are now stated as what they
are: exit 0, the command runs, and a stderr line saying no checker did. A
contract nobody wrote down is one a reader can only learn by reading the
source.

Also: `git.hooks` takes five names and one of them is `manual`, which is not
git's. ADR 0001 and the type's own doc comment said githooks(5).

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
… not a pass

Three seams were answering a two-valued question that has three answers, and
each one spent the third answer on a green tick.

WHAT A BLOB TURNED OUT TO BE. `guard::unicode` had the three-answer reader:
byte-order mark first, git's NUL test for binary, and anything else that is not
UTF-8 is unreadable and exit 2. Every other reader of the same blobs decoded
with `String::from_utf8_lossy` or tested for a NUL and skipped. So a UTF-16 file
naming a private repository was replacement characters with NULs between them in
the tracked scan, a skipped image in the staged scan, and a decoded document in
the codepoint guard, all in one run over one blob. The reader moves to
`guard::scope`, beside `read`, because which bytes an operation introduces and
what those bytes turned out to be are two halves of one question. The message
readers go through it too: a message is text by construction, so bytes that will
not decode are a message nobody read, which is exit 2 rather than a skip. What
cannot be decoded joins the guard's could-not-look accounting, beside a name the
forge would not answer for and outranked by a name that was found.

A HOST NOBODY CAN ASK. `gh` answers for github.com and for nothing else, and the
guard listed six other forges by hand and dropped every other host in silence:
`github.acme.com/acme/secret`, a self-hosted forge, anything nobody had thought
of. That is a silent third answer in the guard whose subject is that there is no
such thing. Any `host.tld/owner/repo` this tool cannot ask about is unavailable
now, which is exit 2, and what quiets one is a declaration rather than a list
compiled in: `foreign_hosts`, host globs, at the top of the policy file for the
reason `private_owners_from` is there and on a rule where one corner of the tree
wants its own. Which hosts a repository cites is policy, which is
`parameterize-do-not-enumerate` and what the hand list stood against.

An owner whose escaped name would not compile into a pattern was dropped
mid-search with nothing printed, so the operator's list said one thing and the
guard looked for another. That is a config error at load now, as is a host glob
that will not parse.

EVERY CHECK READS THE SAME TEXT. `allowed_scripts` decoded under a covering
`encoding` rule and stopped the run where nothing declared one. `regexp`,
`forbidden_literals` and `require_regexp` reached the tree through a searcher
with binary detection off and a lossy sink, so the same UTF-16 file was exit 2
for one check and clean for the other three. One reader in `scan` decides now --
UTF-8 where it is UTF-8, the declared charset where a rule declares one, the
byte-order mark otherwise -- and hands text to the engine, which no longer opens
files at all. A file nothing decodes goes into the unreadable list, which is
reported beside the findings rather than instead of them; binary is the one
skip. `require_regexp` gains the most: a file it could not read was a violation
about a marker that may well have been in it.

Beside the fixes: `redact_matches` was the one field a policy file may write
that no document named, so REFERENCE.md documents it and a structural test now
holds every public field of the serde-facing structs to being named there in
code. ROADMAP said the Python script ships the reconcile step, which `uphold
check` has done since it moved into the binary; README says what the script
still does. DESIGN's account of which bytes a guard reads was missing the
commit messages a push publishes.

Closes #112
Closes #113
Closes #114

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
…y no longer does

Twenty-eight host globs at the top of the policy file, and every one of them was
being answered by a list compiled into the binary that nobody could see or edit.
Without them `no-private-repo-names-in-files` reports 55 names on hosts `gh`
cannot be asked about, which is exit 2 at `pre-push` and at `manual`: a licence
URL, a bibliography, a standards reference and a fixture publishing to
`example.com` are all `host.tld/a/b`, which is the shape a repository name has
and is not one.

Four of the entries are documentation rather than citation and are noted as such
in the file: `host.tld`, `github.acme.com` and `gitlab.com` are the worked
examples the reference and this guard's own tests need to be able to write, and
the `*.example.*` family is what every fixture publishes to.

A working tree with an older uphold installed will find its `git` and `gh` shims
failing closed on this file until the binary carrying `foreign_hosts` is
installed, because a shim loads the policy with the uphold that is on PATH and
an unknown field means the policy does not load. That is the same trade a new
bundled set makes, and the cure is the same: install the build from this branch.

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
…nt of

`gh pr edit` was unavailable to an agent whose token lacked read:org, so it
ran `gh api -X PATCH repos/OWNER/REPO/pulls/N -F body=@file` -- the same body,
the same account, the same public tracker -- and no `match` list named `api`,
so the shim exec'd it with nothing printed and exit 0. The same hole stood in
front of `gh api -X POST .../issues`, `.../comments`, `.../releases` and every
`glab api`.

The verb is not one a `match` list can simply name, and that is why it was
missing rather than forgotten. One word is both the read side and the write
side of a forge, so standing in front of `api` outright would stand in front
of every GET on the machine -- including the `gh api repos/OWNER/REPO --jq
.visibility` this shim itself runs to answer `public-target`, which is a shim
standing in front of its own lookup. Which half an invocation is, is a
question about its argv.

So `api:*` is matched, and a second question is asked of the one verb that
has to answer it: does this call carry a body at all. It does when
`-X`/`--method` names something other than GET, or when any field or `--input`
is present, which is when gh switches to POST anyway. A GET with no fields is
left exactly where it was -- unmatched, unexamined, exec'd.

The call is read with the verb's own grammar rather than through the table's
flag lists, because they disagree: `-F` is `--body-file` on `gh pr create` and
`--field` on `gh api`, so one vocabulary answering for both reads a `key=@file`
pair as a body file or a body file as a field. Every field value becomes a
text subject, with `@file` read from the file and `@-` from stdin, replayed to
the command on the way through the way a `--body-file -` already is. An
`--input` file is judged one string value at a time where it is JSON, because
the encoding otherwise hides the sentence a prose rule stands in front of
behind its escapes, and whole where it is not -- and JSON here means a
document that starts with `{` or `[` and parses as one, not YAML's superset
reading, under which an ordinary Markdown body with a `Note:` line parses as a
mapping and most of it reaches no checker.

The endpoint path is the destination. `repos/OWNER/REPO/...`, and GitLab's
`projects/OWNER%2FREPO`, resolve to the same `owner/repo` a `--repo` resolves
to on every other verb, so `prevent-unowned-target` and a `public-target`
scope read a `gh api` call exactly as they read a `gh pr create` one. A path
naming no repository -- `gh api graphql`, `gh api user` -- falls through to
the table's own resolver, which is the honest bound: the destination of a
GraphQL mutation is inside its query and nothing in a path can say.

Five tests: a PATCH whose `-F body=@file` trips a text rule refuses at exit 1,
a GET with no fields runs, an `--input` JSON is refused for one string value
and runs when that value is ordinary, a body bound for an owner this
workspace does not have is refused by the destination guard while the same
call under its own owner runs, and a GitLab project id is the same
destination with its separator escaped.

Closes #125

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
An unaskable host was one answer for every name written against it, and exit 2
for all of them made the cure "enumerate every host you cite" in every consuming
repository. That is the enumeration ADR 0001 refuses, moved out of the binary
and into eighty policy files, which is worse than where it started: a consumer
publishing a pull-request body was refused over `claude.ai/code/<id>`, and this
tree needed twenty-eight host globs to say its bibliography is a bibliography.

The split is by owner, because the owner is what the rule is about. A name under
a declared `private_owners` owner, or under the `owner` this policy says the
workspace is, on a host `gh` cannot be asked about, is could-not-look and exit 2:
that is the name this rule exists to keep off a public target, and silence about
it is not a pass. Every other `host.tld/a/b` goes back to the unresolved bucket
it was in -- reported on the way past, refused only where the repository has set
`refuse_unknown` -- because a DOI, a licence URL and an encyclopaedia article all
have the shape of a repository name and none of them is one.

`foreign_hosts` keeps its job and gains no new one: it says a host is not a forge
at all, and it quiets both rows. Nothing is quieted by default.

The twenty-eight globs come back out of this repository's own policy. They were
load-bearing only under the wider default, the guard passes at `manual` without
them, and a policy field carrying a list nobody needs is the enumeration again
wearing a smaller number.

Claude-Session: https://claude.ai/code/session_01HEudouCNhFHK6UPEWcWqXd
@coderabbitai

coderabbitai Bot commented Sep 2, 2026

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 31 minutes.

Check out review usage here.

View limit details

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.

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Team

Run ID: 6a0e790e-c8fa-4839-9fe2-83079d8091ca

📥 Commits

Reviewing files that changed from the base of the PR and between 001f8ab and d23ae5b.

📒 Files selected for processing (31)
  • README.md
  • ROADMAP.md
  • docs/DESIGN.md
  • docs/REFERENCE.md
  • docs/adr/0001-a-config-surface-a-stranger-can-read-in-one-minute.md
  • policy/base/mismatched-author.toml
  • policy/base/private-names.toml
  • policy/base/process-residue.toml
  • policy/base/sets.lock.json
  • policy/principles.toml
  • src/config.rs
  • src/engine.rs
  • src/guard/message.rs
  • src/guard/mod.rs
  • src/guard/names.rs
  • src/guard/scope.rs
  • src/guard/unicode.rs
  • src/hook.rs
  • src/main.rs
  • src/scan.rs
  • src/shim.rs
  • src/text.rs
  • tests/base_set_corpus.rs
  • tests/base_sets_cli.rs
  • tests/guard_recovered_halves.rs
  • tests/hook_cli.rs
  • tests/scan_cli.rs
  • tests/shim_cli.rs
  • tests/shim_handoff_cli.rs
  • tests/structural_documentation.rs
  • tests/support/syntax.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 82.64550% with 164 lines in your changes missing coverage. Please review.
✅ Project coverage is 93.39%. Comparing base (001f8ab) to head (d23ae5b).

Files with missing lines Patch % Lines
src/shim.rs 80.13% 90 Missing ⚠️
src/config.rs 64.17% 24 Missing ⚠️
src/guard/names.rs 90.96% 14 Missing ⚠️
src/scan.rs 85.26% 14 Missing ⚠️
src/text.rs 88.04% 11 Missing ⚠️
src/guard/scope.rs 75.00% 10 Missing ⚠️
src/engine.rs 90.90% 1 Missing ⚠️

❌ Your patch status has failed because the patch coverage (82.64%) 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     #130      +/-   ##
==========================================
- Coverage   94.12%   93.39%   -0.74%     
==========================================
  Files          37       37              
  Lines       13493    14136     +643     
==========================================
+ Hits        12700    13202     +502     
- Misses        793      934     +141     

☔ 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

Copy link
Copy Markdown
Owner Author

Everything this branch carried has landed on main individually: #116, #117, #123, #124, #126, #129 and the refactor above them in #131.

@HackingGate HackingGate closed this Sep 2, 2026
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