Skip to content

PreToolUse soft nudge has no session cap — measured 8,348 injections (~651k tokens) in a single project #3435

Description

@olbersparadox

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:

  1. 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.
  2. 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.
  3. Dedup per target — do not re-nudge for the same file path within a session.
  4. 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.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions