Skip to content

feat(hook): the adjudicator's first slice — envelope, parser, gh policy (CLOUD-202) - #113

Merged
wenzowski merged 1 commit into
mainfrom
claude/committed-vs-docs-alignment-z1pb7a
Aug 7, 2026
Merged

wenzowski merged 1 commit into
mainfrom
claude/committed-vs-docs-alignment-z1pb7a

Conversation

@wenzowski

Copy link
Copy Markdown
Contributor

What

First slice of CLOUD-202 (v0.1 spine item 1): batten hook exists and adjudicates.

  • hook.rs — the normalized envelope (event/tool/command); the wrapper-lookthrough command parser (quoted spans neutralised, segments split on shell separators, env prefixes and env/timeout/sudo/mise exec -- looked through so policy judges the effective program — the CLOUD-181-hardened shape, ported once instead of copy-pasted three times); and the gh lifecycle deny table ported verbatim from gh-guard-check, redirects and bypass hatch included.
  • batten hook --harness <claude-code|exit-code> — the claude-code adapter answers in the host's JSON permissionDecision object with exit 0 (the channel the production shell guards already use); the exit-code adapter is the neutral §7 inversion: exit 2 denies, reason on stderr.
  • effect.rs — hook declared unclassified (its decision mediates writes), so it cannot leak into the derived read-only allowlist; the completeness test enforces the pairing.
  • Fail-open at every boundary: unreadable stdin, undecodable payload, empty command, BATTEN_GH_GUARD_BYPASS — all allow.

Not in this slice (CLOUD-202 items 3–5): the receipt/issue-key/run-shape/protected-path policies and the .claude/settings.json flip. The shell guards stay wired until those port.

Why

The agent-neutral hook layer is the stated scarce property and until now existed only as five single-harness bash programs; the shared tokenizer's triplication has produced two landed bug-fix commits plus CLOUD-181/185. Refs CLOUD-202.

Verification

  • 12 unit tests pin the parser and policy (wrapper look-through, quoted-span non-match, interposed flag values, lookalike verbs, bypass, byte-stable deny shape)
  • 5 behavioural tests over the compiled binary pin both adapters' channels, fail-open, and the bypass hatch
  • Full gate green per commit (mise run ci, including the batten-check consumer-ci: check in the main-branch protection ruleset #1 step); mise run verify before readying

🤖 Generated with Claude Code

https://claude.ai/code/session_01VS5eTnMTruCWytpAk2gW7k

@linear-code

linear-code Bot commented Aug 7, 2026 •

Copy link
Copy Markdown
CLOUD-202 Port the five shell guards into `batten hook` — the neutral envelope, in Rust, from the battle-tested spec

The agent-neutral hook layer is the stated scarce property, and it already runs in production — as five Claude-Code-specific bash programs (mise-tasks/{gh-guard,ready-guard,issue-guard,run-shape-guard,memory-guard}, bats-covered), with the same ~60-line wrapper-lookthrough tokenizer copy-pasted three times. That duplication has produced two landed bug-fix commits (1535406, 6921b28), CLOUD-181 (Urgent) and CLOUD-185 (same class, open). The spec is written and battle-tested; this issue ports it.

Scope, in order:

  1. batten hook verb (effect-table entry in the same change; §7 inversion: exit 2 denies) reading one normalized stdin envelope (event/tool/input/cwd/session? per the contracts doc), with the harness-specific decode/encode isolated to one adapter (--harness claude-code first, per CLOUD-44's shim model).
  2. The shared command parser ported once: wrapper look-through (env/timeout/sudo/mise exec --), quoted-span scrubbing, judging the effective program — the exact code paths CLOUD-181 hardened.
  3. The guard policies as config-driven predicates, not hardcodes: deny-verbs table (gh-guard), receipt predicate (ready-guard, see the receipts issue), issue-key requirement with a configurable key regex (issue-guard generalized beyond CLOUD-<n>), run-shape rules (run-shape-guard), protected-path writes (memory-guard).
  4. Fail-open posture ported exactly: parse failure ⇒ allow + one stderr line; BATTEN_<GUARD>_BYPASS=1 hatches kept, each bypass writing an audit line.
  5. Flip .claude/settings.json from mise run <guard> to batten hook — the moment consumer ci: check in the main-branch protection ruleset #1 becomes true at the hook layer. The bash guards stay until their bats suites are ported, then delete.

Ready

  1. Source of truth: the command spec (house style §2/§5/§7) + the five mise-tasks/ programs as the behavioural spec; the bats suites in tests/*.bats are the port's acceptance corpus.
  2. Mechanism: mise run test:cargo (envelope + parser + policy tests) and the ported guard behaviour asserted end-to-end over the compiled binary; the hk gate already runs both. Policy-engine-first: this IS the engine growing the capability the bash layer stood in for.
  3. Effect: hook is unclassified at minimum (it adjudicates writes); declared in effect.rs in the same change (completeness test enforces).
  4. No generated artifacts.
  5. Output: deny goes to the harness's decision channel via the adapter; stderr carries the reason + recovery command (CLOUD-122's contract applies to every deny path).
  6. Commit type: feat (minor).
  7. Tests: one tests/cli.rs case per guard family porting the sharpest bats cases: wrapper look-through, quoted-span non-match, missing-receipt deny, bypass-env allow + audit line, unparseable-input fail-open.
  8. blockedBy: none — the receipts issue specializes ready-guard's predicate and can land after the envelope+parser skeleton.

Review in Linear

Refs: CLOUD-202

The agent-neutral hook layer existed as five single-harness bash programs
with one ~60-line command parser copy-pasted three times; that
duplication has produced two landed bug fixes and two open defects. This
lands the Rust core the port converges on:

  hook.rs   the normalized envelope (event/tool/command); the
            wrapper-lookthrough parser — quoted spans neutralised,
            segments split, env prefixes and wrapper programs (env,
            timeout, sudo, mise exec --) looked through so policy judges
            the EFFECTIVE program, the exact shape CLOUD-181 hardened;
            and the gh lifecycle table ported verbatim from
            gh-guard-check, reasons and all.
  cli.rs    `batten hook --harness <claude-code|exit-code>`. The
            claude-code adapter answers in the host's JSON decision
            object (exit 0, the channel the production guards already
            use); the exit-code adapter is the neutral §7 inversion —
            exit 2 denies, reason on stderr.
  effect.rs `hook` is unclassified: its own run only reads, but its
            decision mediates writes, so it must not leak into the
            derived read-only allowlist.

Fail-open throughout: unreadable stdin, undecodable payload, empty
command, and the BATTEN_GH_GUARD_BYPASS hatch all allow. Unit tests pin
the parser's sharpest bats cases (wrapper look-through, quoted-span
non-match, interposed flag values, lookalike verbs); behavioural tests
pin both adapters' channels over the compiled binary.

The shell guards stay wired in settings.json until the remaining
policies (receipts, issue-key, run-shape, protected paths) are ported;
flipping the settings entry is CLOUD-202's item 5, not this commit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VS5eTnMTruCWytpAk2gW7k
@wenzowski
wenzowski force-pushed the claude/committed-vs-docs-alignment-z1pb7a branch from fe45691 to 4b2da11 Compare August 7, 2026 20:49
@wenzowski
wenzowski marked this pull request as ready for review August 7, 2026 20:50
@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit 4b2da11 into main Aug 7, 2026
10 checks passed
@wenzowski
wenzowski deleted the claude/committed-vs-docs-alignment-z1pb7a branch August 7, 2026 20:52
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