Skip to content

fix(hooks): crash-proof PostToolUse read-only enforcement - #92

Merged
ryanleecode merged 8 commits into
masterfrom
feature/hook-posttooluse-gate
Aug 30, 2026
Merged

fix(hooks): crash-proof PostToolUse read-only enforcement#92
ryanleecode merged 8 commits into
masterfrom
feature/hook-posttooluse-gate

Conversation

@systemfsoftware-maker

@systemfsoftware-maker systemfsoftware-maker commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

The PostToolUse hook could not enforce. The Deno launcher crashed with an uncaught NotCapable error whenever a Deno-sensitive env var (LD_*, DYLD_*) was present, and the settings-surface hook ended in a silent || exit 0 — every write passed without being checked.

Now the hook runs plain check mode on both shipped surfaces: PostToolUse (not PreToolUse), read-only (no --strip, no file writes), no silent success. A flagged write exits 2 with the report on stderr; an absent checker exits 1 with a bare status line. The hook reports state — it never instructs the agent to install or run anything.

The hook command scrubs the whole LD_*/DYLD_* env class before starting Deno, and the launcher treats any spawn failure as binary-unavailable instead of throwing, so no environment can turn the hook into a crash or a silent pass.

Session-settled decisions carried from planning: PostToolUse hook (user-directed, over PreToolUse); read-only check mode (user-directed, over --strip in the hook path).

Verification

  • One-shot gate green: fmt, clippy, 120 tests. wire.rs pins PostToolUse, check mode, no || exit 0, class-level env scrub, and the catch-without-rethrow mechanism.
  • Crash-class smoke with unlisted vars LD_PRELOAD_64 + LD_ASSUME_KERNEL + LD_FOR_BUILD set: checker runs, flagged write exits 2 with report.
  • Direnv-present-without-checker smoke: bare status line + exit 1, never 0.

Compound Engineering

- scrub whole LD_*/DYLD_* env class in plugin hook (finite list missed
  unlisted vars, re-triggering the NotCapable crash class)
- wire test asserts the catch mechanism, not a comment token (CHK1)
- direnv branch keeps 'nothing checked' guidance when it exits without
  a verdict
- README: drop strip write-failure entry (hook is read-only), fix the
  --strip example to a real payload form
- settings flake hint matches the launcher guidance (bwrap)
@systemfsoftware-maker

Copy link
Copy Markdown
Collaborator Author

Residual Review Findings

From ce-code-review run 20260830-212446-4b8b0529 (6 reviewers; fixes from the actionable set are applied in 02d226617). Items below were filed or deferred, not applied — they need a human decision or a follow-up PR.

Filed tickets

Severity File Finding Ticket
P1 crates/comment-checker/src/check.rs:25 Gate fails open to silent green on empty or undecodable stdin (decode failure and empty file_path -> exit 0). Decision: keep fail-open vs fail-closed on empty non-TTY stdin — changes the binary contract. #86
P2 .claude/settings.json:5 / hooks/hooks.json:2 Both surfaces now enforce PostToolUse: double-fire, two 30s budgets, duplicated report, divergent locators. Decision: accept/dedupe/document. #87
P2 hooks/run.ts + settings No committed behavioral launcher smoke (wire.rs is text-grep; plan smokes were session-scoped). A regression to throw passes CI. Mechanism pair now pinned; behavioral smoke needs deno in CI. #90
P2 hooks/run.ts catch-all Catch-all conflates spawn-denied (NotCapable) with binary-missing -> wrong guidance on host policy changes; spurious deno Info Failed to resolve ... allow-run noise when binary absent. #91
P3 .claude/settings.json:8 Settings surface locates project via ${PWD:-.}, launcher via CLAUDE_PROJECT_DIR — wrong guidance from nested dirs of a flake monorepo. #88
P3 hooks/hooks.json:8 deno (and now coreutils via the env-sweep) is an undeclared hard prerequisite; raw exit 127 when absent. #89

All tickets linked to this PR.

Residual risks (not filed)

  • Scrub-vs-catch fidelity (P2): the env-sweep lives in the hook command. A host that invokes run.ts via its shebang bypasses it; the catch-all then converts EVERY write into exit-1 guidance ("nothing checked") — the gate stops checking while claiming otherwise. NotCapable is live on this machine (LD_FOR_BUILD=ld).
  • Exit-1 guidance semantics are host-dependent: hosts that treat any non-zero PostToolUse exit as a block will hard-block every write when the checker is absent. Deliberate no-swallow trade-off, unverified blast radius.
  • 30s hook timeout kills deno on large writes; a killed hook is reported as a failed hook with no report content, both surfaces.
  • CLAUDE_PLUGIN_ROOT unset in hosts that know plugins differently expands to /hooks/deno.jsonc and fails every write.
  • stdin plumbing unverified: stdin: 'inherit' on both spawns is asserted nowhere; payload-delivery loss through the launcher chain is untested (reliability P1/75, pre-existing).

Coverage

  • Fixes applied and committed in 02d226617: env class sweep (correctness P1/100), wire test mechanism pair (CHK1 — 3 reviewers), direnv guidance retention (reliability P2/100), README strip contradiction deleted (P2/75 + adversarial P2/100), README --strip example payload-fixed (reliability P2/100), branch renamed to feature/hook-posttooluse-gate.
  • Reviewer agent-native failed during artifact write (exit 1, "Writing the review artifact."); no findings from it.
  • Verdict after apply: Ready to merge with residuals tracked.

A hook reports state; it never instructs the agent. The settings command
and the launcher no longer emit install/action guidance ('pnpm add',
'direnv allow') that the model could act on from hook text. Absent
checker -> bare status line on stderr + exit 1; flagged -> exit 2.
wire.rs pins the no-instruction invariant on both surfaces.
Source-text grep assertions restated the config that contained the
strings; reverting the behavior still passed. Delete the file and the
narrating comments in the launcher. Verification is behavioral (smokes).
@ryanleecode
ryanleecode merged commit ca51c07 into master Aug 30, 2026
12 checks passed
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