rtk-ai/rtk

bug: hook rewrite of `pnpm lint` discards package.json script body — same false-green as #185 at the hook layer

Open

#2.094 aberto em 26 de mai. de 2026

Ver no GitHub
 (2 comments) (0 reactions) (0 assignees)Rust (2.914 forks)batch import
area:clibughelp wantedpriority:high

Métricas do repositório

Stars
 (48.085 stars)
Métricas de merge de PR
 (Mesclagem média 8d 17h) (49 fundiu PRs em 30d)

Description

Summary

The PreToolUse hook rewrites pnpm lintrtk 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.sh produced by older rtk 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 eslint doesn't default to (--max-warnings 0, --report-unused-disable-directives, etc.).

Suggested fixes (any one resolves)

  1. 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. Substituting rtk <subcommand> discards user intent. Users can still type rtk lint explicitly when they want it. Smallest possible diff — a registry edit.

  2. Resolve the script body before rewriting. When the hook matches pnpm lint, read package.json's scripts.lint, then rewrite as rtk lint <resolved-script> so the user's chain runs faithfully with rtk summarizing on top. Retains rtk's value; more code.

  3. Default rtk lint to --max-warnings 0 when no explicit --max-warnings is given. Matches the de facto CI convention; documents itself in rtk 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 lint exit code, fixed for direct invocations; doesn't cover the hook rewrite path.
  • #186 (closed) — rtk lint summary count.
  • #1772 (open) — rtk tsc --pretty swallows errors.
  • #1581 (open) — rtk git push reports ok on push rejected by GitHub rulesets.
  • #1232 (open) — read-only commands exit code semantics.
  • #1233 (open) — updatedInput mutations 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.

Guia do colaborador