A grep fed by a pipe filters that command's output, not the code, so its
pattern is not a graph lookup the agent skipped.
(cherry picked from commit f9f3c9ce3a)
Sanitise graph text before it reaches the hint (one line, no control
characters or backticks, capped length), skip the graph parse when the call
has nothing to look up, isolate hint failures so they can never cost the
nudge or the strict deny, skip value-taking flags (rg -t/-g, -A/-B/-C...)
and file-name searches (find/fd), drop declaration keywords from terms,
match labels only (no source-path tier, short terms exact-only), resolve a
relative read target against the project root, and align the strict deny's
wording with the narrowed nudge.
(cherry picked from commit f8ba08fe93)
Agents treated the soft nudge as noise: it was the same placeholder text on
every call, and "or to modify/debug specific lines" excused nearly any
targeted read. A firing nudge (and the strict-mode deny) now carries:
- a command built from the call: `graphify explain "<symbol>"` when the
searched word is a node label, `graphify query "<terms>"` otherwise, or
`graphify explain "<file>"` for a read;
- up to five matching graph nodes with file and line, so the nudge shows
what the graph already knows instead of only asking for it.
The lookup scans graph.json labels and paths directly (0.1-0.5 s on 3k-15k
node graphs; the full query stack costs ~1.5 s per hook call), skips graphs
over 32 MiB, and falls back to the plain nudge on any failure or no match.
The escape clause now allows raw reads only once the exact file and lines
are known, and the always-on rule names the moment to query: before the
first grep, search or source-file read.
(cherry picked from commit b51314765c)
The read/search nudge was re-injected on every qualifying Read/Glob/Grep/Bash
call with no dedup or session cap (issue #3435 measured ~8,348 injections,
~651k tokens, for 339 graphify calls), and an agent that just ran
`graphify query` was still nudged on its very next read. `_nudge_allowed`
now gates all three soft-nudge sites with a per-session + per-agent counter
(first nudge always fires; later ones suppressed while the query stamp is
fresh; hard cap via GRAPHIFY_HOOK_NUDGE_CAP, default 5), with 24h GC and
fail-open preserved. Subagents get independent budgets. The strict deny path
is unchanged.
Landed from PR #3443 (squashed). The README strict-mode note is omitted here
because that prose moved to docs.graphify.com in the README trim.
Co-Authored-By: Andre Brait <andrebrait@gmail.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
hook-guard read returned early for .ps1/.psm1/.psd1 targets because the
extension gate (_HOOK_SOURCE_EXTS in graphify/cli.py) did not list them,
even though detect.py classifies those extensions as code and they are
extracted into the graph. In a PowerShell repo the Read/Glob nudge never
fired, only the search one.
FixesGraphify-Labs/graphify#4271.
Signed-off-by: Raphael Malikian <rtmalikian@gmail.com>
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 1ee08e70ba)
Regression tests for #4040: read, stale-read and search reminders name the effective graph.json under a custom GRAPHIFY_OUT, and the default output dir stays byte-identical.
Co-Authored-By: Claude <noreply@anthropic.com>
(cherry picked from commit 31cb3bb1c0)
The hook-guard matched a search command appearing anywhere in the intercepted Bash string
(a comment, a quoted -m message, an echo), firing false nudges. Match only an executed
command position (start-of-segment after a shell operator, wrapper-aware), so prose lookalikes
stay quiet while genuine pipes/&&/;/$() invocations still fire.
Adds a wide matrix over _run_hook_guard covering the search/read detection
boundaries and the gemini BeforeTool contract:
- search: grep-family variants (grep/pgrep/egrep/fgrep/ripgrep), token matches
(rg/find/fd/ack/ag with trailing space), piped commands; and silence for
non-search commands, empty/missing/non-string command, 'find' without a
trailing space, 'ag' mid-word, top-level vs nested tool_input, non-dict
tool_input, and no-graph.
- read: source/framework extensions, uppercase and multi-dot names, Windows
backslash paths, glob patterns; and silence for .json (not .js), lockfiles,
images, extensionless files, an extension on a directory segment, targets
under the (default and custom-named) output dir, and no-graph.
- fail-open: malformed/empty/binary stdin and a throwing graph-existence check
never crash or block.
- gemini: always returns {"decision":"allow"}, nudges only with a graph, and
stays "allow" even if the check throws.
- dispatch/exit/encoding via real subprocess: missing/unknown mode exits 0
silently, every mode exits 0 (never blocks), and the read nudge's em dash
round-trips as valid UTF-8.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>