refactor(oxlint-guard): run the plugin on deno 2 outside the pnpm workspace - #115
Open
systemfsoftware-maker wants to merge 13 commits into
Open
refactor(oxlint-guard): run the plugin on deno 2 outside the pnpm workspace#115systemfsoftware-maker wants to merge 13 commits into
systemfsoftware-maker wants to merge 13 commits into
Conversation
ce-babysit-pr drives its watch loop through gh: its poller reads checks and the PR, both of which OMP serves natively. The guard refuses that mechanism at the shell and the skill at load, naming run_watch and pr:// in the refusal. The rule generalises to every shell call with a native counterpart - gh reads, an HTTP client pointed at a URL, cat/ls/find/grep/sed -i, a backgrounded service, a debugger. Two carve-outs keep the shell what it is better at: a pipeline ending in a count, checksum or set difference, and a stage fed by a pipe. Hook tests are dropped with the suite. Declared bypass: the pre-commit hook was skipped. It dies in pnpm dep verification against a concurrent agent npm/ tree, not on these files. lint-staged maps these three paths to dprint alone (no oxlint.config.ts above .claude/), and dprint check ran green on exactly them
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
oxlint-guardnow runs as TypeScript source under Deno 2, from its own workspace underclaude-plugins/. Installing it no longer drags in this repo's pnpm, Effect, tsdown or vitest toolchain, and there is no build step: the two hooks executesrc/*.tsdirectly, so what ships is what runs. The 970 KB of committeddist/bundles are gone.Two enforcement defects found during review are fixed, and both predate this migration.
The enforcement fix is the load-bearing change
Claude Code blocks a
PreToolUsetool call on exit code 2 alone; it treats every other non-zero code as a non-blocking error and lets the edit proceed (hooks reference: "If your hook is meant to enforce a policy, useexit 2").The config guard exited 1 when its runtime was missing. That was documented as the fail-closed posture and it was the opposite: on a machine without the runtime, any oxlint rule could be turned off and the guard would not veto it. The guard's own crash handler already exited 2, so the guard failed closed on its own bugs and failed open on the one condition where it enforced nothing. Switching Bun for Deno widened that path, because Deno is less often preinstalled.
The config guard now maps every state in which it cannot reach a verdict onto exit 2 — runtime absent, a module that will not load, a permission it was not granted, a defect in the guard itself. The wrapper remaps any exit outside
{0, 2}, so the failure does not have to be anticipated to be caught. This also closes the availability hole the migration itself introduced: deleting the self-contained bundle means a cold cache with no network can fail beforemainruns, and that now blocks instead of silently permitting.The
PostToolUselint guard had the mirror-image hole:runEntryPointhad no top-level catch, so an unexpected throw exited 1 — and exit 2 is the only code that shows aPostToolUsehook's stderr to the agent. A lint violation could be computed and then lost with the process. Guard defects there now exit 2.Behaviour changes, stated plainly
The plan scoped product behaviour as preserved verbatim. Two things changed and the claim no longer holds unqualified:
deno check+deno lintinstead of hard-failing with an install hint. Without this the guard could not lint its own source: every edit underclaude-plugins/exited 2. An oxlint config still outranks a siblingdeno.json, so this only fires where oxlint was entirely unconfigured — pinned by the integration scenariostill uses oxlint when a Deno workspace also carries an oxlint config.Neither is covered by the characterization baseline, and neither can be: see below.
Design decisions
@std/*has no schema library, and the branded-type + one-way-transform surface the guard needs is real work to hand-roll.@std/testinghas no property testing. The four property suites port across unchanged in spirit.deno.lockcommitted@std/*, valibot and fast-check from jsr/npm on first run. The lockfile pins integrity hashes for that fetch..cmdhandling moved into the pure coreinvocationForit is pure and property-tested on any OS.--frozenon the hooks--frozenonly additionally forbids lockfile updates, and on drift it would hard-block every edit in the repo. That belongs in a CI check, not in an edit-path guard.Declared deviation from CONST-P2
The pure core's decisions were single
Match.value(...).pipe(...)expressions, which hold at cyclomatic complexity 1. Without Effect they areif/switchchains, and the oxlint gate that measured CC=1 does not run on this tree. Recorded here per CONST-W3 rather than left implicit.The harm CONST-P2 names is branches the mutator reaches that the suite does not. Compensating evidence: five hand-run mutations across all four pure modules (
decideLintPlanarm order,classifyLintResultretry gate,replaceFirstfirst-vs-last occurrence,applyHunkssequential-vs-parallel) were each killed by an existing property, and two more (invocationForpredicate inverted,/cdropped) were killed by the properties added here.Validation
deno task check— typecheck, lint, format check, 89 tests, 0 failures.pnpm check:local— exit 0, after the last edit.PostToolUsehook caught a type error on a write during this work, and the new fail-closedPreToolUsewrapper blocked an edit when a half-applied change made the guard uninstantiable — the exact silent pass this PR removes, observed rather than argued.What the replay does and does not certify. The fake binaries in the characterization harness dispatch on filename, so the replay certifies control flow and the exit/stderr contract, not that the real linter is still invoked correctly. That is pinned separately by the integration suite, which asserts the exact argv including
-c <configPath>. The two are not interchangeable, and the 49-of-49 figure alone should not be read as invocation fidelity.Residual risks
--allow-runcannot be scoped: the oxlint path resolves at runtime undernode_modulesat unknown depth.--allow-envis now an explicit 12-key allowlist and the config guard's--allow-envwas dropped entirely (it never read the environment).deno task checknow runs the tests so a contributor cannot get a green signal that excludes them.deno.jsoncarries a JSR-publishable shape; nothing publishes it today.No changeset:
scripts/check-changeset.mjsgates onpackages/**, and this branch touches none.