Skip to content

feat(plugin): answer in the deny — denied greps return actual results#22

Merged
sdsrss merged 1 commit into
mainfrom
feat/deny-with-answer
Jun 10, 2026
Merged

feat(plugin): answer in the deny — denied greps return actual results#22
sdsrss merged 1 commit into
mainfrom
feat/deny-with-answer

Conversation

@sdsrss

@sdsrss sdsrss commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

When pre-grep-guide.js denies a symbol-shaped raw grep, run the AST-aware equivalent (code-graph-mcp grep "<pattern>" [path], ~20ms warm, 2s timeout) synchronously inside the hook and embed the actual results in the deny reason. Measured recommend→use transfer of suggestion-style interventions is ~0% — the model rarely initiates a new tool call because a message told it to, but it will use results already in front of it.

Behavior

cg grep outcome Hook behavior recommendations.jsonl
≥1 hit deny + embedded results (≤4KB, line-boundary truncation) {action:"deny", answered:true}
CLI missing / error / timeout v0.46 static deny {action:"deny", answered:false}
0 hits ALLOW + one-line FYI (regex-dialect ≠ proof of absence) {action:"hint", fallthrough:"no-hits"}

Opt-out: CODE_GRAPH_NO_ANSWER_IN_DENY=1 (restores v0.46 static deny). CODE_GRAPH_NO_BLOCK_GREP=1 unchanged.

Metric integrity

  • Verified the CLI grep subcommand does NOT write usage.jsonl → hook-initiated runs cannot inflate the deny→use funnel.
  • Reading note: an answered deny satisfies the need in-place, so Deny→use reads LOW by design — segment by answered.
  • Rust untouched; answered/fallthrough are additive JSONL fields existing readers ignore.

Also

  • Hook stdin hardening: read fd 0 instead of /dev/stdin (path form fails silently on socketpair stdin, e.g. spawnSync({input}) harnesses; real Claude Code pipes unaffected).
  • 35 new tests (cg-answer 14, pre-grep-guide additions incl. 4 stub-binary stdin-spawn e2e); both new test files added to the CI curated list.
  • Live smoke against this repo's real index: denied grep -rn "extract_relations" src/parser/ produced a deny embedding real hits with containing fn/module; smoke residue cleaned.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Raw grep command denials now embed actual search results directly in the denial message instead of only suggesting the command
    • Added environment variables to opt out of enhanced denial behavior

When pre-grep-guide denies a symbol-shaped raw grep, run the AST-aware
equivalent (code-graph-mcp grep, ~20ms warm, 2s timeout) synchronously
inside the hook and embed the results in the deny reason. Measured
recommend->use transfer of suggestion-style interventions is ~0%; results
already in front of the model bypass that choice entirely.

Three outcomes: hits -> deny with embedded results (4KB line-boundary
truncation, recs answered:true); CLI unavailable/error/timeout -> v0.46
static deny (answered:false); 0 hits -> ALLOW with one-line FYI
(fallthrough:"no-hits") since regex-dialect differences mean 0 hits is
not proof of absence. Opt-out: CODE_GRAPH_NO_ANSWER_IN_DENY=1.

Verified non-polluting: CLI grep does not write usage.jsonl, so
hook-initiated runs cannot inflate the deny->use funnel. Rust untouched
(answered/fallthrough are additive JSONL fields readers ignore).

Also hardens hook stdin: read fd 0 instead of /dev/stdin (path form
fails silently on socketpair stdin, e.g. spawnSync({input}) harnesses).

additive: new-test-first, no prior failing path (35 new tests RED->GREEN;
121/121 pre-grep-guide + 14/14 cg-answer; live smoke against real repo
index embedded real extract_relations hits, residue cleaned).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8f8a7fbf-6933-4519-b9db-7e75f40e0340

📥 Commits

Reviewing files that changed from the base of the PR and between dcb1b6d and 3aacb7e.

📒 Files selected for processing (6)
  • .github/workflows/ci.yml
  • CHANGELOG.md
  • claude-plugin/scripts/cg-answer.js
  • claude-plugin/scripts/cg-answer.test.js
  • claude-plugin/scripts/pre-grep-guide.js
  • claude-plugin/scripts/pre-grep-guide.test.js

📝 Walkthrough

Walkthrough

This PR implements the v0.47.0 "deny-with-answer" feature for the pre-grep-guide hook. A new cg-answer.js utility runs AST-aware grep queries synchronously with timeout and truncation safeguards. The hook is updated to call this utility during deny decisions, embedding actual grep results in denial reasons instead of just suggesting the command. An opt-out environment variable allows fallback to static deny messages.

Changes

Deny-with-Answer Feature

Layer / File(s) Summary
Answer utility module
claude-plugin/scripts/cg-answer.js, claude-plugin/scripts/cg-answer.test.js
New module exports runGrepAnswer to run synchronous grep queries with pattern validation, binary resolution, timeout, and output truncation via truncateAtLine. Tests validate all status outcomes (hits, no-hits, unavailable), timeout handling, truncation behavior, and input constraints via a temporary stub binary.
Hook helpers and stdin hardening
claude-plugin/scripts/pre-grep-guide.js (helpers section)
Stdin reading changed from /dev/stdin to fd 0 for alternate-transport compatibility. Introduces extractSearchPath (first indexed path token), pickBlockPattern (first identifier-like pattern), buildBlockReasonWithAnswer (inline deny with embedded results), buildNoHitsFyi (fallthrough FYI message), and isAnswerDisabled (CODE_GRAPH_NO_ANSWER_IN_DENY gating). Refactors SRC_PATH regex to use token-anchored matching.
Block tier deny-with-answer integration
claude-plugin/scripts/pre-grep-guide.js (block tier and exports)
Integrates answer utility into block tier: extracts pattern/search path, runs runGrepAnswer unless disabled, handles no-hits by recording hint and emitting FYI while allowing fallthrough, and emits deny decision with reason conditionally chosen between buildBlockReasonWithAnswer (hits found) and buildBlockReason (other statuses). Records recommendation with answered flag. Updates module.exports to expose all helpers.
Comprehensive hook tests
claude-plugin/scripts/pre-grep-guide.test.js (new v0.47.0 section)
Unit tests validate helper functions across edge cases (quoted/unquoted patterns, redirects, path traversal, ./ prefixes), message builders with and without truncation markers, and isAnswerDisabled gating. End-to-end tests spawn hook with stubbed CG binary to assert denied decisions embed hits, no-hits fallthrough as hints with FYI text, failure fallback to static deny, and env-var opt-out behavior, with recommendations.jsonl validation of answered and fallthrough fields.
Documentation and CI
CHANGELOG.md, .github/workflows/ci.yml
CHANGELOG documents v0.47.0 deny-with-answer feature, env-var opt-outs (CODE_GRAPH_NO_ANSWER_IN_DENY, CODE_GRAPH_NO_BLOCK_GREP), deny/hint funnel outcomes, and stdin fd 0 hardening. CI workflow updated to include cg-answer.test.js and pre-grep-guide.test.js in plugin-tests job with updated comments.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🐇 With whiskers twitched and paws held high,
We grep and answer, do not deny!
Smart AST-aware results now flow,
Inline where deny messages glow—
A feature hops to v-point-four-seven!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.25% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(plugin): answer in the deny — denied greps return actual results' directly and clearly describes the main feature: denied grep commands now return actual results instead of just a denial message.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/deny-with-answer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@sdsrss sdsrss merged commit f15e0e9 into main Jun 10, 2026
9 checks passed
@sdsrss sdsrss deleted the feat/deny-with-answer branch June 10, 2026 18:35
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.

1 participant