Skip to content

uphold hook never consults the prose rules, so prose-shapes is dark at the MCP seam #111

Description

@HackingGate

What is dark

uphold hook <harness> is the MCP seam: it stands in for the shim when an agent
publishes through an MCP server, where there is no process to intercept and no
argv[0] to decide by. It consults text::failures (the literal rules plus the
running-host fallback) and guard::over_text (the text-capable built-in
guards). It never consults prose::over_text.

text::failures_in deliberately excludes the prose rules, and says so: folding
them in would report each of them twice at the shim seam, where text-literals
is a checker in its own right. Every other whole-text seam therefore carries a
compensating call to prose::over_text beside the literal or guard dispatch --
text::check for uphold scan --text, guard_command in main.rs for
uphold guard --text. The hook seam was never given one.

So every prose_regexp rule is dark at uphold hook. That includes the whole
bundled prose-shapes set and the prose half of published-text -- sets whose
rules declare command.before = ["gh", "git push"] precisely because the text
they judge is a pull-request body, an issue title, a release note. Those are
exactly what an agent posts through mcp__github__*.

Reproduce

policy/principles.toml:

[rule.no-empty-hedge-here]
message = "State the claim."
prose_regexp = '(?i)\barguably\b'
files.include = ["."]
command.before = ["gh"]

[[shim]]
command = "gh"
match = ["pr:create"]
text_flags = ["-b", "--body"]
$ echo '{"tool_name":"mcp__github__create_pull_request","tool_input":{"body":"This is arguably fine."}}' | uphold hook claude-code
$ echo $?
0

$ printf 'This is arguably fine.\n' | uphold scan --text -
policy check failed: no-empty-hedge-here
State the claim.
line 1: This is arguably fine.
$ echo $?
1

The same sentence is refused when git commit records it and allowed when an
MCP server publishes it.

Why it happened, and the fix that is not a fifth copy

There are four assemblies of the same ingredients -- literal rules, text
guards, prose rules -- in text::check, main::guard_command --text,
hook::run, and the shim's text path. Each one is hand-assembled, so a rule
kind added to three of them is dark in the fourth with nothing to say so. The
prose call reached three seams and missed this one; the same shape is already
visible one layer down, where the shim's editor path dispatches regexp and
require_regexp to its pattern checker and leaves prose_regexp to fall
through to the exec consultation, which errors out as a dispatch hole.

The fix is to hoist "everything a piece of published text is judged by" into
one function, with one table saying which seam consults which kind, and have
every seam reach it -- so a future rule kind cannot be added to three of four.

Documentation

docs/REFERENCE.md currently records the exclusion as deliberate, on the
grounds that a tool call carries joined argument strings rather than a
paragraph. That holds for a call carrying flags and ids; it does not hold for
the field an agent puts a pull-request body in, which is the field these sets
were written for. The section needs to say that prose rules are consulted.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions