bug: hook rewrite of `pnpm lint` discards package.json script body — same false-green as #185 at the hook layer
#2.094 geöffnet am 26. Mai 2026
Repository-Metriken
- Stars
- (48.085 Stars)
- PR-Merge-Metriken
- (Durchschn. Merge 8T 17h) (49 gemergte PRs in 30 T)
Beschreibung
Summary
The PreToolUse hook rewrites pnpm lint → rtk lint, discarding the body of the project's package.json lint script. For projects whose script enforces flags eslint doesn't default to (e.g. --max-warnings 0) or chains commands (e.g. pnpm check:foo && eslint ...), the rewritten rtk lint invokes eslint with defaults and exits 0 even when the original pnpm lint would have exited 1.
This is the same false-green symptom as #185 (closed) but originates at the hook-rewrite layer, not inside rtk lint. rtk lint --max-warnings 0 exits 1 correctly — #185's fix still holds for explicit invocations.
Affected version
rtk 0.42.0(Homebrew, macOS arm64)- Confirmed in both the legacy shell-script hook (
~/.claude/hooks/rtk-rewrite.shproduced by olderrtk init) and the current in-binary hook (rtk hook claude).
Reproduction
package.json:
{ "scripts": { "lint": "eslint --max-warnings 0" } }
a.ts (one unused-import warning):
import { randomUUID } from 'node:crypto';
export const x = 1;
# What CI runs (raw pnpm script):
pnpm lint a.ts; echo "raw=$?"
# raw=1 ← eslint --max-warnings 0 fails the run
# What Claude Code runs (hook rewrites pnpm lint → rtk lint):
rtk hook check "pnpm lint a.ts"
# → rtk lint a.ts
rtk lint a.ts; echo "hooked=$?"
# hooked=0 ← false green; --max-warnings 0 was dropped
For confirmation that rtk lint itself is fine when flags are explicit:
rtk lint --max-warnings 0 a.ts; echo $?
# 1 ← propagation is correct when the flag is passed
Impact
Reaches every user who hasn't typed rtk lint themselves. The hook makes pnpm lint the de facto path through which agentic CLI tools (Claude Code, Cursor, etc.) validate before pushing. Local pre-push reports green; CI fails on the dropped flag.
In our team this caused a hotfix PR after a clean local check (keryx#2824 — "Local lint reported the same warning but the wrapper masked the non-zero exit code"). Net cost: ~15 minutes per occurrence plus an additional PR to unblock the deploy.
Affects any project whose lint script:
- chains commands (
pnpm check:foo && eslint ...), or - enforces flags
eslintdoesn't default to (--max-warnings 0,--report-unused-disable-directives, etc.).
Suggested fixes (any one resolves)
-
Stop rewriting
pnpm lint/pnpm tsc/pnpm test/npm run <X>. These names point at project-defined script bodies whose contents the hook can't see. Substitutingrtk <subcommand>discards user intent. Users can still typertk lintexplicitly when they want it. Smallest possible diff — a registry edit. -
Resolve the script body before rewriting. When the hook matches
pnpm lint, readpackage.json'sscripts.lint, then rewrite asrtk lint <resolved-script>so the user's chain runs faithfully with rtk summarizing on top. Retains rtk's value; more code. -
Default
rtk lintto--max-warnings 0when no explicit--max-warningsis given. Matches the de facto CI convention; documents itself inrtk lint --help. Minor behavior change for users with permissive eslint setups.
Option 1 is the cheapest and matches the principle of least surprise — pnpm lint is project-defined, not a known shape rtk can transform safely.
Local workaround
For users hitting this today, a 15-line shell shim around rtk hook claude resolves it without touching rtk:
#!/bin/bash
# rtk-hook-shim.sh — short-circuits the `pnpm lint` rewrite, delegates
# everything else to `rtk hook claude`.
if ! command -v rtk &>/dev/null || ! command -v jq &>/dev/null; then
exit 0
fi
INPUT=$(cat)
CMD=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
case "$CMD" in
"pnpm lint"|"pnpm lint "*|"pnpm run lint"|"pnpm run lint "*|"npm run lint"|"npm run lint "*)
exit 0
;;
esac
echo "$INPUT" | rtk hook claude
Point ~/.claude/settings.json PreToolUse:Bash hook at this shim instead of rtk hook claude.
Related
Same class of bug recurring at the wrapper layer:
- #185 (closed) —
rtk lintexit code, fixed for direct invocations; doesn't cover the hook rewrite path. - #186 (closed) —
rtk lintsummary count. - #1772 (open) —
rtk tsc --prettyswallows errors. - #1581 (open) —
rtk git pushreports ok on push rejected by GitHub rulesets. - #1232 (open) — read-only commands exit code semantics.
- #1233 (open) —
updatedInputmutations dropped in permissive Claude Code modes.
There may be value in a wrapper-level invariant test: for every command in the rewrite registry, assert rtk <cmd> exit code matches the raw invocation across documented flag combinations. Happy to contribute that test pattern if welcome.