Skip to content

PreToolUse denies and nudges are defeated by parallel tool batches; WebFetch deny has no opt-out #1003

Description

@rrva

Summary

Two related problems observed in real use on 1.0.169 with Claude Code.

  1. A deny or nudge issued for one tool in a parallel tool batch competes with the successful results of its sibling calls, and reliably loses. The model consumes whatever succeeded and never follows the redirect.
  2. The WebFetch deny has no opt-out, unlike every other routing behavior.

The first is the more interesting one, because in the observed case context-mode did not save context. It degraded the answer.

What happened

The task was to check whether Google Geocoding's components filter and bounds viewport biasing are compatible. The model emitted two calls in a single parallel block:

  • WebFetch on the official Google docs page
  • WebSearch for the same question

WebFetch was denied with the standard redirect reason (call ctx_fetch_and_index, then ctx_search). WebSearch succeeded and returned blog-grade summaries of the same documentation.

Both results arrived together. The model read the search results, answered from them, and never called ctx_fetch_and_index. The suggested path was not refused, it was simply never reached: by the time the deny was visible, an alternative answer was already in hand.

Net effect:

  • The authoritative source (the official docs) was the one thing that got blocked.
  • A lower-quality secondary source was substituted silently.
  • No bytes were saved, because WebSearch output entered the context window anyway.
  • The redirect's bytesAvoided: 16384 accounting recorded a saving that did not occur, since the model still ingested comparable content from the sibling call.

The answer happened to survive only because the claim was independently verified against the live API afterwards. Without that, a documentation claim would have been reported without the documentation ever being read.

Why this is structural, not a one-off

The redirect reason is written as though it is the only signal the model receives:

context-mode: WebFetch redirected. Call ctx_fetch_and_index(...)

In a serial flow that holds: the deny is the only new information, so the model acts on it. In a parallel batch the deny is one entry among several, and it is the only unsuccessful one. Models resolve that by using what worked. Any guidance whose efficacy depends on being the sole signal is fragile in exactly the batches that agents are encouraged to emit for latency.

This likely affects the Bash / Read / Grep nudges too, though the effect is milder there because those are advisory rather than a hard deny.

Second problem: no opt-out for the WebFetch deny

hooks/core/routing.mjs:874-887 returns action: "deny" for WebFetch unconditionally. There is no env guard, in contrast to the knobs that exist elsewhere in the same file:

  • CONTEXT_MODE_BASH_NUDGE_MIN_COMMAND_BYTES
  • CONTEXT_MODE_EXTERNAL_MCP_NUDGE_EVERY
  • CONTEXT_MODE_REQUIRE_SECURITY
  • CONTEXT_MODE_SUPPRESS_SECURITY_WARNING

WebFetch is the strongest intervention in the routing table and the only one with no switch. A user who wants to keep the Bash/Read/Grep nudges but drop the WebFetch deny has to delete the "matcher": "WebFetch" block from hooks/hooks.json inside the versioned cache directory (.../context-mode/<version>/hooks/), which an upgrade silently reverts.

Suggestions

  1. Add an opt-out, e.g. CONTEXT_MODE_DISABLE_WEBFETCH_REDIRECT=1, consistent with the existing CONTEXT_MODE_* conventions. Cheapest fix, independent of the rest.
  2. Make the reason batch-aware. Add an explicit line: results from other tools in this same batch do not satisfy this request; retry this URL via ctx_fetch_and_index before continuing. Models follow explicit negative instructions here better than implied ones.
  3. Close the loop in PostToolUse. A redirect is already tagged with redirectMeta.type = "webfetch-redirected" and the URL. If no ctx_fetch_and_index for that URL follows within N tool calls, re-inject the nudge. That converts a one-shot suggestion into something observable, and would also make the bytesAvoided accounting honest, since a redirect that was never followed avoided nothing.
  4. Consider guidanceOnce for WebFetch, as Grep already does, or make the severity configurable. A hard deny on the only authenticated fetch path is a large hammer, and WebFetch redirect has no exemption for claude.ai/code/artifact URLs, breaks artifact fetches #984 is another case where the unconditional deny removes capability with no workaround.

Related: #984 (WebFetch deny has no exemption for claude.ai/code/artifact URLs). Same unconditional branch, different symptom.


Filed from a Claude Code session where this occurred; the transcript detail above is the actual observed behavior, not a reconstruction.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions