Summary
The soft _READ_NUDGE / _SEARCH_NUDGE payload is re-injected on every qualifying Read/Glob/Grep/Bash call, with no dedup, no session cap, and no suppression while the agent has recently queried the graph. Only the opt-in strict deny is capped (_mark_session_denied).
Across 87 Claude Code transcripts in one repo I measured 8,348 nudge injections producing 339 graphify calls — a ~4% response rate — at a standing cost of roughly 651,000 tokens, which is ~3.7x more than every graphify query in those sessions combined (~176,000 tokens).
This is adjacent to #580 (which fixed the GRAPH_REPORT.md read-first instruction) but is a separate cost: #580 was one expensive read per question, this is a cheap payload paid thousands of times.
Environment
- graphify 0.9.38, installed with
--strict
- Claude Code, macOS, project-scoped
.claude/settings.json
- Repo: Next.js 14 / TypeScript, 293 source files, graph = 3,938 nodes / 9,158 edges, 99% EXTRACTED
- Verified the relevant code is byte-identical in 0.9.56, so this is not fixed on latest
Measurement
Parsed the raw Claude Code transcripts (~/.claude/projects/<project>/*.jsonl), classifying each tool_use as a graphify call or a broad-exploration call (Grep/Glob/Task, Read of a source file, or Bash containing grep/rg/find/fd/ack/ag). Sessions split on session start time.
Results
Adoption — 77 sessions that explored the codebase:
| Metric |
Value |
| graphify ran before first exploration |
8 / 77 (10%) |
| graphify used at all |
46 / 77 (60%) |
| graphify share of exploration-related calls |
329 / 10,424 (3.2%) |
Nudge cost:
|
Count |
Approx tokens |
| Soft nudge injections |
8,348 |
~651,000 |
| Strict deny (capped, for contrast) |
44 |
negligible |
| All graphify command output |
338 calls |
~176,000 |
What follows a graphify call (the 10 calls after each, 48 sessions):
|
Share |
| grep / find / rg |
68.3% |
| Read |
31.7% |
Root cause
In graphify/cli.py::_run_hook_guard, the read branch ends with an unconditional
sys.stdout.write(_READ_NUDGE)
Every guard above it either returns early or falls through to this line. The suppression logic that exists — _query_stamp_fresh() and _mark_session_denied() — gates only the strict deny:
if _hook_strict_enabled(strict) and tool_name in (None, "Read") \
and not _query_stamp_fresh() \
and _target_is_indexed(fp, root) \
and _mark_session_denied(str(d.get("session_id") or "")):
sys.stdout.write(_READ_DENY)
return
sys.stdout.write(_READ_NUDGE) # <- no cap, no stamp check, no dedup
So an agent that has just run graphify query is still told, on its very next read, that it must run graphify query.
Why this matters beyond the token cost
The CHANGELOG already notes that "a nudge is advisory additionalContext the model routinely walks past mid-task", and #1840 documents nudges firing on unrelated files, "training agents to ignore MANDATORY language entirely". Injection volume is plausibly a cause of that, not just a symptom: 8,348 identical high-urgency strings with no consequence for ignoring them is efficient training to filter the string out. The ~4% response rate is consistent with that.
Suggested fixes
Roughly in order of value-to-effort:
- Suppress the soft nudge while
_query_stamp_fresh() — the stamp already exists and is already written by query/explain/path. An agent that just oriented does not need reminding. One-line change.
- Cap per session with backoff — reuse the
_mark_session_denied marker pattern with a timestamp instead of a one-shot lock (e.g. at most once per N minutes per session id). Fail-open as today.
- Dedup per target — do not re-nudge for the same file path within a session.
- Shorten the payload.
_READ_NUDGE is ~480 characters, of which the subagent-propagation sentence is ~40% and cannot be enforced from a hook anyway.
Fixes 1 and 2 alone would have cut the cost here by well over 90% while keeping the nudge present at the moments it is actually informative.
Reproduction
Any project with a graph and the Claude Code hook installed. To count injections in your own transcripts:
grep -o "MANDATORY: graphify-out/graph.json exists" \
~/.claude/projects/<project>/*.jsonl | wc -l
Compare against the number of graphify invocations in the same files.
Not claimed here
I am not claiming the graph itself is low value, and this is not a re-report of #580 — the read-first instruction is correctly gone. The claim is narrow: the nudge's own standing cost is unbounded by design, appears to be the single largest token line item the tool introduces, and is plausibly self-defeating at high volume.
Summary
The soft
_READ_NUDGE/_SEARCH_NUDGEpayload is re-injected on every qualifying Read/Glob/Grep/Bash call, with no dedup, no session cap, and no suppression while the agent has recently queried the graph. Only the opt-in strict deny is capped (_mark_session_denied).Across 87 Claude Code transcripts in one repo I measured 8,348 nudge injections producing 339 graphify calls — a ~4% response rate — at a standing cost of roughly 651,000 tokens, which is ~3.7x more than every graphify query in those sessions combined (~176,000 tokens).
This is adjacent to #580 (which fixed the
GRAPH_REPORT.mdread-first instruction) but is a separate cost: #580 was one expensive read per question, this is a cheap payload paid thousands of times.Environment
--strict.claude/settings.jsonMeasurement
Parsed the raw Claude Code transcripts (
~/.claude/projects/<project>/*.jsonl), classifying eachtool_useas a graphify call or a broad-exploration call (Grep/Glob/Task, Read of a source file, or Bash containing grep/rg/find/fd/ack/ag). Sessions split on session start time.Results
Adoption — 77 sessions that explored the codebase:
Nudge cost:
What follows a graphify call (the 10 calls after each, 48 sessions):
Root cause
In
graphify/cli.py::_run_hook_guard, the read branch ends with an unconditionalEvery guard above it either returns early or falls through to this line. The suppression logic that exists —
_query_stamp_fresh()and_mark_session_denied()— gates only the strict deny:So an agent that has just run
graphify queryis still told, on its very next read, that it must rungraphify query.Why this matters beyond the token cost
The CHANGELOG already notes that "a nudge is advisory
additionalContextthe model routinely walks past mid-task", and #1840 documents nudges firing on unrelated files, "training agents to ignore MANDATORY language entirely". Injection volume is plausibly a cause of that, not just a symptom: 8,348 identical high-urgency strings with no consequence for ignoring them is efficient training to filter the string out. The ~4% response rate is consistent with that.Suggested fixes
Roughly in order of value-to-effort:
_query_stamp_fresh()— the stamp already exists and is already written by query/explain/path. An agent that just oriented does not need reminding. One-line change._mark_session_deniedmarker pattern with a timestamp instead of a one-shot lock (e.g. at most once per N minutes per session id). Fail-open as today._READ_NUDGEis ~480 characters, of which the subagent-propagation sentence is ~40% and cannot be enforced from a hook anyway.Fixes 1 and 2 alone would have cut the cost here by well over 90% while keeping the nudge present at the moments it is actually informative.
Reproduction
Any project with a graph and the Claude Code hook installed. To count injections in your own transcripts:
Compare against the number of
graphifyinvocations in the same files.Not claimed here
I am not claiming the graph itself is low value, and this is not a re-report of #580 — the read-first instruction is correctly gone. The claim is narrow: the nudge's own standing cost is unbounded by design, appears to be the single largest token line item the tool introduces, and is plausibly self-defeating at high volume.