Skip to content

fix(hook): give Bash file reads (cat, head, sed -n, ...) the read nudge - #5010

Open
rohit-jsfreaky wants to merge 1 commit into
Graphify-Labs:v8from
rohit-jsfreaky:fix/hook-bash-read
Open

rohit-jsfreaky wants to merge 1 commit into
Graphify-Labs:v8from
rohit-jsfreaky:fix/hook-bash-read

Conversation

@rohit-jsfreaky

Copy link
Copy Markdown
Contributor

Closes #5009

What does this PR do?

A Bash command that runs a file reader now goes through the same Read path as the Read tool: the same read nudge with the file hint, the stale variant, and the same per-agent budget (GRAPHIFY_HOOK_NUDGE_CAP). The readers are cat, head, tail, nl, less, more, bat, and sed with -n / --quiet / --silent. They must run in an executed position and target an existing in-project code file.

To find the reader and its file, I moved the existing command parsing out of _bash_invokes_search into a shared _bash_commands helper. That is the parsing that skips comments and quoted text and only looks at executed positions (#3121). Search detection is unchanged. The new parts are _bash_read_target and a small dispatch in _run_hook_guard.

It never blocks. Bash stays nudge-only, also with --strict, GRAPHIFY_HOOK_STRICT=1, or a missing tool_name. The Read tool is still denied as before.

It stays silent (and spends no budget) for:

Diff: graphify/cli.py +157 / -38 (most of it is the moved parser), a few README words, 51 tests in the new tests/test_hook_bash_read.py.

Before / after (the issue's repro)

command v8 this PR
cat src/app.py nothing read nudge, the same text as a Read of that file (with its node hint when the graph has nodes for it)
sed -n 1,80p src/app.py search nudge, no hint read nudge + hint
head -50 / tail / nl / less / more / bat on src/app.py nothing read nudge + hint
cat README.md, cat > src/x.py, sed -i, echo x | cat, cat graphify-out/graph.json, outside file nothing nothing
grep -rn run src search nudge search nudge (unchanged)

Limitations

  • cd src && cat app.py: a cd earlier in the same command is not tracked, so it stays silent.
  • Quoted paths (cat "src/app.py") and globs (cat src/*.py) are not detected. The shared parser blanks quoted text before reading arguments.
  • When a reader and a search share one command (cat a.py | grep x), the read nudge wins.
  • Bash skips the doc extensions (.md, .txt, .rst, .mdx) that the Read tool nudges on, to keep the existing cat README.md behaviour.

Type of change

  • Bug fix
  • New feature
  • Documentation
  • Tests or CI
  • Refactor
  • Security fix

Verification & Invariants

Invariant: a Bash read gets exactly what a Read of the same file would get in soft mode, from the same budget, and Bash never blocks a tool call.

  • Read the CONTRIBUTING.md guide.
  • Reproduced the issue and identified the invariant.
  • Made the smallest fix necessary.
  • Added a regression test (if bug fix) or isolated boundary test.
  • Kept the PR description synchronized with the final implementation.
  • Documented any limitations / unsupported cases explicitly.

How was this tested?

uv run --frozen pytest tests/test_hook_bash_read.py -q           # 51 passed; all 51 fail with v8's cli.py
uv run --frozen pytest tests -q                                  # 10042 passed, 0 failed (v8: 9991 passed, 0 failed)
uv run --frozen pytest <29 hook/install test files> -q           # 908 passed on Windows
uv run --frozen ruff check .                                     # clean
uv run --frozen pyright graphify/cli.py graphify/install.py      # the same 5 errors as v8, none new
graphify hook-guard search with the issue's repro                # table above, on Linux and Windows

This branch and #5008 (the compaction fix) touch different parts of cli.py and merge cleanly together.

Graphify-specific checklist

  • I updated generated skill artifacts (uv run python -m tools.skillgen --bless) when changing their source fragments. (Not needed: the generated docs don't list hook matchers.)
  • I confirmed that AST/structural extraction remains deterministic (no ambient state dependencies like ENV variables).
  • I reviewed changes for security implications (no unsafe interpolation into shell/Python).
  • I confirmed no API keys or local-only graph data are included.
  • (If applicable) I disclosed AI authorship in my commit messages.

πŸ€– Generated with Claude Code

The Read|Glob matcher gives the read nudge, but Bash goes to `hook-guard
search`, which only reacted to search commands. `cat src/app.py`,
`head -50 src/app.py`, `tail`, `nl`, `less`, `more` and `bat` read indexed
code with no nudge at all, and `sed -n 1,80p src/app.py` got the generic
search nudge without the file's symbols. Claude Code reads files this way a
lot, especially with the Bash tool in auto mode.

`hook-guard search` now checks Bash commands for a file reader in an executed
position (the Graphify-Labs#3121 parsing, factored out of _bash_invokes_search into
_bash_commands so both use it) naming an existing in-project code file. Such a
call goes through the Read path: the same Read nudge with the file hint, the
stale variant, and the same per-agent budget. It is never denied, also in
strict mode. Targets outside the project or inside the graph output are
skipped (Graphify-Labs#1840, Graphify-Labs#4961), as are docs (`cat README.md` stays silent, as the
existing tests pin), non-graphed extensions, writes (`cat > x.py`, a stdout
redirect, `sed -i`), stdin reads (`echo x | cat`) and paths with shell
expansion. sed counts as a reader only with -n/--quiet/--silent; without it a
sed on a file stays a search, as before.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added the contributor Pull request from a returning community contributor label Nov 4, 2026
@github-actions

github-actions Bot commented Nov 4, 2026

Copy link
Copy Markdown

Thanks for the pull request, @rohit-jsfreaky. A maintainer will review it soon.

Want to talk it through while it is in review? Come join us on our Discord server. For longer-form discussion there is also GitHub Discussions.

A couple of things that speed up review: make sure the test suite passes on Python 3.10 and 3.13, and that the change keeps extraction deterministic.

@graphify-labs graphify-labs Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graphify reviewed this change.

Worth a look β€” the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.

Not checked: tests were not run; formal verification proved 0 of 2 changed function(s) (2 sampled, not proven).

Formal verification. PR-changed functions: 2/2 verified (0 proven, 2 may-equivalent, 0 distinguished) Β· 0 not verified.


Graphify review β€” findings

Adds a read nudge for Bash commands that print an in-project code file (cat, head, tail, sed -n, nl, less, more, bat), so the hook now points agents at the graph the same way it already does for the Read tool. _bash_read_target resolves relative paths against the hook payload's cwd, skips docs, graph output and writes like cat > x.py or sed -i, and draws on the shared nudge budget without ever denying; its hand-rolled redirect and operand scanning is where a misread command would silently drop or misfire the nudge. Refactors the #3121 command parsing into a reusable _bash_commands generator that yields each executed command's name, args and pipe status, so _bash_invokes_search and the new reader detection share one parser and keep ignoring prose, quotes and heredoc bodies. The README hook-platform bullet is updated to mention the Bash readers.

Worth a look

  • Bash read detection misses quoted file operands β€” graphify/cli.py Β· Escalate Β· medium
    • NOT verified (no proof, no reproducing execution) β€” needs human review
Analysis details β€” impact, health, verification

Impact & health

Graphify review

Impact β€” 502 functions depend on the 135 functions this change touches.

Health β€” this change adds coupling hotspots:

  • worse: _run_hook_guard() β€” 7 callers, 17 callees

Verification β€” 502 functions in the blast radius were not formally verified this run (proofs are advisory here).

Gate & verification

graphify gate

PASS β€” objectively clean (no health regressions, tests not run β€” proofs not run this pass (advisory)). Grounded, not self-assessed.

Advisory (not blocking):

  • verification_scope: 442 function(s) in the blast radius were not formally verified this run

Test selection

Test selection

519 of 519 test file(s) selected (100%) via static blast radius.

Escalated to a full run for safety β€” the selection is not trustworthy on its own (see below). CI should run the whole suite.

  • tests/pi.test.ts β€” full-run-safety
  • tests/test_4874_jvm_resolution.py β€” full-run-safety
  • tests/test_4909_reference_binding_scale.py β€” full-run-safety
  • tests/test_affected_cli.py β€” impact, full-run-safety
  • tests/test_affected_defines.py β€” full-run-safety
  • tests/test_affected_member_seed.py β€” full-run-safety
  • tests/test_affected_overrides.py β€” full-run-safety
  • tests/test_affected_path_file_node.py β€” full-run-safety
  • tests/test_affected_seed_reference_forms.py β€” impact, full-run-safety
  • tests/test_agents_platform.py β€” impact, full-run-safety
  • tests/test_alias_folding.py β€” full-run-safety
  • tests/test_analyze.py β€” full-run-safety
  • tests/test_anthropic_custom_endpoint.py β€” full-run-safety
  • tests/test_antigravity_install.py β€” full-run-safety
  • tests/test_apm_fallback_version.py β€” full-run-safety
  • tests/test_architecture_doc.py β€” full-run-safety
  • tests/test_astro_extraction.py β€” full-run-safety
  • tests/test_astro_import_ids.py β€” full-run-safety
  • tests/test_atomic_canvas_export.py β€” full-run-safety
  • tests/test_atomic_version_stamp.py β€” full-run-safety
  • tests/test_atomic_writes.py β€” full-run-safety
  • tests/test_backend_env_isolation.py β€” full-run-safety
  • tests/test_backend_extras.py β€” full-run-safety
  • tests/test_backend_opt_in.py β€” full-run-safety
  • tests/test_benchmark.py β€” full-run-safety
  • tests/test_benchmark_raw_graph.py β€” full-run-safety
  • tests/test_benchmark_real_query.py β€” full-run-safety
  • tests/test_binary_media_words.py β€” full-run-safety
  • tests/test_blade_extractor.py β€” full-run-safety
  • tests/test_build.py β€” full-run-safety
  • tests/test_build_located_semantic_identity.py β€” full-run-safety
  • tests/test_build_merge_dedup_scope.py β€” full-run-safety
  • tests/test_build_merge_ghost_fold_4955.py β€” full-run-safety
  • tests/test_build_merge_hyperedges_and_prune.py β€” full-run-safety
  • tests/test_build_merge_shrink_guard.py β€” full-run-safety
  • tests/test_builtin_global_type_refs.py β€” full-run-safety
  • tests/test_c_include_resolution.py β€” full-run-safety
  • tests/test_cache.py β€” full-run-safety
  • tests/test_cache_long_paths.py β€” full-run-safety
  • tests/test_cache_stale_import_target.py β€” full-run-safety
  • tests/test_cache_windows_case_4925.py β€” full-run-safety
  • tests/test_callflow_html.py β€” full-run-safety
  • tests/test_cargo_introspect.py β€” full-run-safety
  • tests/test_cargo_missing_manifest.py β€” full-run-safety
  • tests/test_carried_hyperedge_remap.py β€” full-run-safety
  • tests/test_case_only_decl_ids_4904.py β€” full-run-safety
  • tests/test_case_sensitive_resolution.py β€” full-run-safety
  • tests/test_charmap_encoding.py β€” full-run-safety
  • tests/test_chunking.py β€” full-run-safety
  • tests/test_cjs_module_extension.py β€” full-run-safety
  • … and 469 more

non-code file(s) changed (README.md) β†’ running the full suite for safety (a code graph can't see config/fixture/data deps)

changed code file(s) with no mapped test (README.md) β€” a coverage gap or a missing link β€” running the full suite rather than only the selected tests

Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory β€” it never changes the check verdict.

Docs that may be stale (advisory)

Formal verification

No difference found (not proven): No behavior difference found in \_bash\_invokes\_search (not a proof).

The verifier ran both versions of \_bash\_invokes\_search on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

No difference found (not proven): No behavior difference found in \_run\_hook\_guard (not a proof).

The verifier ran both versions of \_run\_hook\_guard on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.

Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.

Note: An input the sampler did not try could still differ.

Β· 1 more finding(s) on lines outside this diff (see the check run).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

contributor Pull request from a returning community contributor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Claude Code hook: reading code with cat / head / sed -n in Bash gets no read nudge

1 participant