fix(discover): count hook-rewritten commands as coverage, not misses - #3164
albatrossflyon-coder wants to merge 1 commit into
Conversation
Transcripts record commands as the model emitted them, before the PreToolUse hook rewrites them. discover classified every Supported command as a missed opportunity regardless of whether the hook was actually installed and had already rewritten it at runtime, causing a real undercount of RTK usage (measured ~19.5 points on a 30-day/359- transcript window in the field). Reuses the same rewrite engine the hook itself calls (`rtk hook check`) to re-derive, per command, whether the installed hook would have covered this exact instance — respecting exclude_commands/ transparent_prefixes config and the same unattestable-construct defer the hook applies, so genuinely uncovered commands still report as misses. Also fixes a related miscount in the other direction: `rtk proxy <cmd>` deliberately runs unfiltered, but was being counted as coverage just because it starts with "rtk ". Excluded explicitly so the escape hatch can't flatter the audit. Fixes rtk-ai#3148
pszymkowiak
left a comment
There was a problem hiding this comment.
Verified on a compiled build — unit tests (7) cover all branches of covered_by_hook (hook-not-installed → miss, rewritable → covered, unattestable-construct → miss, config-excluded → miss) and is_already_rtk (incl. the rtk proxy exclusion). Also ran it end-to-end with a synthetic transcript via CLAUDE_CONFIG_DIR:
hook absent → Already using RTK: 0 (0%) ; grep / git status counted as MISS
hook present → Already using RTK: 2 (50%) ; git status $(whoami) stays a MISS, rtk proxy not counted
So rewritable commands correctly move from "missed" to "covered" when the hook is installed, while the unattestable-construct case still reports as a genuine miss (the hook would defer) and rtk proxy never flatters the count. Reusing registry::rewrite_command — the same engine the hook calls — is the right call for consistency with rtk hook check.
One minor semantic note (non-blocking): hook_installed reflects the hook's current state, not whether it was installed when each transcript was recorded, so very old pre-install transcripts could be over-counted as covered. Given discover is a heuristic and defaults to a 30-day window (where hook state is usually stable), and the pre-rewrite form is all the transcript preserves, this is a reasonable trade-off — maybe worth a line in the help/docs.
LGTM 👍
|
Nice catch on the root cause (transcripts recording pre-rewrite commands), and reusing the real rewrite engine instead of the
Both stem from the same thing: Given the PR's own motivation is fixing a systematic ~19.5-point miscount, I'd want this to not trade one systematic miscount for another. Suggestions, roughly in order of effort:
Not blocking if there's appetite to land the transcript-parsing fix now and treat the time-travel issue as a fast-follow, but I think it should at least be called out in the PR description / a tracking issue so it doesn't get read as "coverage stats are now trustworthy" when they're only trustworthy for windows where the hook + registry haven't changed. |
| Classification::Supported { .. } => { | ||
| rtk_disabled_count += 1; | ||
| let display = truncate_command(actual_cmd); | ||
| *rtk_disabled_cmds.entry(display).or_insert(0) += 1; | ||
| } |
There was a problem hiding this comment.
it seems this one should also be filtered by covered_by_hook even if less important
|
Good catch, and you're right that this is a real time-travel bug, not just a theoretical edge case — For a minimal fix I can bound the scan by I'd rather not try to solve #2 (registry drift) in this PR — that really needs the hook to log its own rewrite decision + rtk version at runtime, which is a bigger persistence change than a bug-fix PR should carry. I'll open a tracking issue for that and link it here, and call out both limitations explicitly in this PR's description so nobody reads the merged stats as "fully time-accurate." Will push the mtime bound + doc update to this branch — let me know if you'd rather see it split into its own follow-up PR instead of amending this one. |
|
@albatrossflyon-coder I've pushed a complete fix in #3206 that should superseed your PR. Could you check it before spending time updating your PR? |
|
Thanks for pushing through on this — #3206 is the right fix, not just a good-enough one. It solves both gaps you originally flagged (pre-install history via the real hook_decisions log, and registry/permission drift via measuring the actual PermissionVerdict at the moment the hook ran) instead of the mtime-bound approximation I was about to add here, which only ever addressed the first. The tool_use_id join and the hook-decision logging table are a materially better foundation than re-deriving coverage from current state, and the validation (hook-absent vs hook-installed-with-real-decisions, direct sqlite checks) is thorough. Closing this in favor of #3206 — no need to duplicate review effort. Thanks for catching the gap and doing the deeper fix. |
…etroactively rtk discover previously re-derived whether a historical transcript command would have been covered by the hook using *today's* hook-install state and registry, applied uniformly across the whole scan window. That mis-classifies history that predates the hook being installed or a registry/permission change (PR #3164 attempted a fix but had this same gap, plus missed permissions.deny and the RTK_DISABLED= bypass bucket). Log the real PreToolUse decision (allow/ask/deny/defer) at the moment the Claude Code hook actually runs, keyed by tool_use_id -- the same id Claude Code stores on the transcript's tool_use/tool_result blocks -- so discover can join real hook outcomes back to transcript entries instead of guessing. Falls back to a corrected heuristic (now permission-deny aware) only for history that predates logging, and labels that portion of the report as an estimate. Fixes #3148
…etroactively rtk discover previously re-derived whether a historical transcript command would have been covered by the hook using *today's* hook-install state and registry, applied uniformly across the whole scan window. That mis-classifies history that predates the hook being installed or a registry/permission change (PR #3164 attempted a fix but had this same gap, plus missed permissions.deny and the RTK_DISABLED= bypass bucket). Log the real PreToolUse decision (allow/ask/deny/defer) at the moment the Claude Code hook actually runs, keyed by tool_use_id -- the same id Claude Code stores on the transcript's tool_use/tool_result blocks -- so discover can join real hook outcomes back to transcript entries instead of guessing. Falls back to a corrected heuristic (now permission-deny aware) only for history that predates logging, and labels that portion of the report as an estimate. Fixes #3148
…etroactively rtk discover previously re-derived whether a historical transcript command would have been covered by the hook using *today's* hook-install state and registry, applied uniformly across the whole scan window. That mis-classifies history that predates the hook being installed or a registry/permission change (PR #3164 attempted a fix but had this same gap, plus missed permissions.deny and the RTK_DISABLED= bypass bucket). Log the real PreToolUse decision (allow/ask/deny/defer) at the moment the Claude Code hook actually runs, keyed by tool_use_id -- the same id Claude Code stores on the transcript's tool_use/tool_result blocks -- so discover can join real hook outcomes back to transcript entries instead of guessing. Falls back to a corrected heuristic (now permission-deny aware) only for history that predates logging, and labels that portion of the report as an estimate. Fixes #3148
Summary
Fixes #3148.
rtk discoverreads commands from Claude Code transcripts as the model emitted them — before the PreToolUse hook rewrites them. It classified everySupportedcommand as a missed opportunity regardless of whether the hook was actually installed and had already rewritten that exact command at runtime, producing a real undercount of RTK usage (the issue reports ~19.5 points on a 30-day/359-transcript window).Fix: re-derive per-command whether an installed hook would have covered this exact instance, using the same rewrite engine the hook itself calls (
registry::rewrite_command, same function backingrtk hook check). This respectsexclude_commands/transparent_prefixesconfig and the same unattestable-construct defer the hook applies (heredocs, command substitution, etc.) — so commands the hook genuinely couldn't/wouldn't touch still report as misses, only commands it actually would have rewritten move to "already using RTK."Secondary fix, same issue:
rtk proxy <cmd>deliberately runs the raw command unfiltered, but was being counted as coverage just because it starts with"rtk ". Excluded explicitly — the escape hatch shouldn't flatter the audit it's supposed to be exempt from.Did not address the issue's third (explicitly lower-priority) finding —
$(cat …)command substitution shim detection — to keep this PR scoped to the two miscounts the issue calls the actual bug.Test plan
covered_by_hook/is_already_rtkas small pure functions (matching the existing pattern in this codebase —hook_check.rs,rewrite_cmd.rs) with 7 new unit tests covering: hook not installed, rewritable command covered, unattestable construct still a miss, config-excluded command still a miss, plainrtkprefix counted,rtk proxyexcluded, unrelated command not countedcargo test— 2478 passed, 0 failedcargo fmt --check— cleancargo clippy --all-targets— no issues