Summary
The PreToolUse WebFetch hook denies unconditionally. For URLs whose retrieval depends on credentials held by the host agent rather than the sandbox — concretely https://claude.ai/code/artifact/<uuid> — the suggested replacement cannot succeed, so the redirect does not relocate the work, it removes the capability.
In Claude Code this breaks a first-party workflow outright: updating an existing Artifact requires reading its current version first, and that read path is WebFetch.
Version: 1.0.169 (also present in 1.0.162). Platform: Claude Code.
Why the redirect can't work for this host
ctx_fetch_and_index and ctx_execute fetch from the sandbox with no access to the host's authenticated claude.ai session. claude.ai/code/artifact/* is a private, client-rendered page behind that session, so the sandbox receives only the SPA shell.
Measured, same URL, same moment:
ctx_fetch_and_index(url: "https://claude.ai/code/artifact/<uuid>")
→ Fetched and indexed 1 sections (0.1KB)
→ content: "Claude Artifact\n\n[](/code/artifacts) Content is user-generated and unverified."
0.1 KB of shell. The actual page is ~30 KB of HTML. ctx_search over that index returns nothing useful because nothing useful was indexed.
WebFetch retrieves the same URL correctly — Claude Code's own tool documentation calls this out explicitly:
Fails on authenticated/private URLs — use an authenticated MCP tool or gh for those instead. Exception: claude.ai/code/artifact/{uuid} URLs ARE fetchable via your claude.ai login — use WebFetch, not curl (curl gets the SPA shell or a Cloudflare 403).
So the host tool has a documented capability here that the sandbox tools structurally do not, and the hook redirects from the one that works to the ones that can't.
Impact
Artifact updates are read-before-write. The publish call refuses with:
This session hasn't viewed the latest version of the artifact. Read it first (WebFetch the URL), reapply your edits, then publish. force:true discards the other session's changes — use it only if the user asks to overwrite.
With WebFetch denied, that precondition is unsatisfiable. The remaining options are both bad:
- Mint a second artifact — the user asked for one page at a stable link and now has two, one of them stale.
force: true — overwrite a page you were never able to read, which is exactly what that flag warns against.
I hit this on a real task. The user's instruction was "update the status artifact"; I ended up creating a duplicate specifically because I could not satisfy the read, then had to go back and force-overwrite after confirming with them. The failure mode is quiet, too — ctx_fetch_and_index reports success ("Fetched and indexed 1 sections"), so an agent that doesn't check the byte count will confidently summarize an empty shell as the page's contents.
Reproduce
- Claude Code, context-mode 1.0.169 enabled.
- Publish any artifact, note its URL.
- In a new session (so the read-tracking is not already satisfied), attempt
Artifact with url: pointing at it → refused, "hasn't viewed the latest version".
WebFetch that URL → denied by the context-mode hook.
ctx_fetch_and_index that URL → "1 sections (0.1KB)", SPA shell only.
- No path to a successful update remains except
force.
Cause
hooks/core/routing.mjs, WebFetch branch (~line 874 in 1.0.169):
// ─── WebFetch: deny + redirect to sandbox ───
if (canonical === "WebFetch") {
const url = getWebFetchUrl(toolInput);
return mcpRedirect({
action: "deny",
reason: `context-mode: WebFetch redirected. Call ${t("ctx_fetch_and_index")}(...)`,
...
});
}
url is already in scope and used only for the message — nothing consults it before denying. There is no host/URL allowlist and no env or config escape hatch (I checked configs/, and the only process.env reads in routing.mjs are the security-bundle path and warning suppressors).
Suggested fix
Minimal, and it uses the value that's already bound:
if (canonical === "WebFetch") {
const url = getWebFetchUrl(toolInput);
// Retrieval depends on the host agent's session; sandbox fetches get a login
// shell, so redirecting here removes the capability rather than relocating it.
if (/^https:\/\/claude\.ai\/code\/artifact\//i.test(String(url ?? ""))) return null;
return mcpRedirect({ /* … unchanged … */ });
}
null is already this function's no-decision path, so the call proceeds normally.
Better still, make it configurable rather than hardcoding one host — a user-supplied list of URL patterns exempt from WebFetch routing. The general rule worth encoding: if the sandbox cannot obtain the same bytes the host tool can, don't redirect. Other credentialed-host URLs will hit this too.
Worth considering separately: ctx_fetch_and_index reporting success on what is obviously a login/shell response is its own hazard. A warning when an indexed page is implausibly small, or when the content matches a known shell pattern, would stop agents from treating an empty fetch as a real one.
Workaround, for anyone else hitting this
A SessionStart hook that re-applies the patch above each session, so it survives ctx upgrade. One caveat that cost me a debugging cycle: the executing copy is version-pinned at ~/.claude/plugins/cache/context-mode/context-mode/<version>/hooks/core/routing.mjs. Patching ~/.claude/plugins/marketplaces/context-mode/... changes nothing at runtime and looks like it worked. Discover the files rather than hardcoding paths.
Happy to open a PR for the narrow fix or the config-driven version — say which you'd prefer.
Summary
The
PreToolUseWebFetch hook denies unconditionally. For URLs whose retrieval depends on credentials held by the host agent rather than the sandbox — concretelyhttps://claude.ai/code/artifact/<uuid>— the suggested replacement cannot succeed, so the redirect does not relocate the work, it removes the capability.In Claude Code this breaks a first-party workflow outright: updating an existing Artifact requires reading its current version first, and that read path is
WebFetch.Version: 1.0.169 (also present in 1.0.162). Platform: Claude Code.
Why the redirect can't work for this host
ctx_fetch_and_indexandctx_executefetch from the sandbox with no access to the host's authenticated claude.ai session.claude.ai/code/artifact/*is a private, client-rendered page behind that session, so the sandbox receives only the SPA shell.Measured, same URL, same moment:
0.1 KB of shell. The actual page is ~30 KB of HTML.
ctx_searchover that index returns nothing useful because nothing useful was indexed.WebFetchretrieves the same URL correctly — Claude Code's own tool documentation calls this out explicitly:So the host tool has a documented capability here that the sandbox tools structurally do not, and the hook redirects from the one that works to the ones that can't.
Impact
Artifact updates are read-before-write. The publish call refuses with:
With WebFetch denied, that precondition is unsatisfiable. The remaining options are both bad:
force: true— overwrite a page you were never able to read, which is exactly what that flag warns against.I hit this on a real task. The user's instruction was "update the status artifact"; I ended up creating a duplicate specifically because I could not satisfy the read, then had to go back and force-overwrite after confirming with them. The failure mode is quiet, too —
ctx_fetch_and_indexreports success ("Fetched and indexed 1 sections"), so an agent that doesn't check the byte count will confidently summarize an empty shell as the page's contents.Reproduce
Artifactwithurl:pointing at it → refused, "hasn't viewed the latest version".WebFetchthat URL → denied by the context-mode hook.ctx_fetch_and_indexthat URL → "1 sections (0.1KB)", SPA shell only.force.Cause
hooks/core/routing.mjs, WebFetch branch (~line 874 in 1.0.169):urlis already in scope and used only for the message — nothing consults it before denying. There is no host/URL allowlist and no env or config escape hatch (I checkedconfigs/, and the onlyprocess.envreads inrouting.mjsare the security-bundle path and warning suppressors).Suggested fix
Minimal, and it uses the value that's already bound:
nullis already this function's no-decision path, so the call proceeds normally.Better still, make it configurable rather than hardcoding one host — a user-supplied list of URL patterns exempt from WebFetch routing. The general rule worth encoding: if the sandbox cannot obtain the same bytes the host tool can, don't redirect. Other credentialed-host URLs will hit this too.
Worth considering separately:
ctx_fetch_and_indexreporting success on what is obviously a login/shell response is its own hazard. A warning when an indexed page is implausibly small, or when the content matches a known shell pattern, would stop agents from treating an empty fetch as a real one.Workaround, for anyone else hitting this
A
SessionStarthook that re-applies the patch above each session, so it survivesctx upgrade. One caveat that cost me a debugging cycle: the executing copy is version-pinned at~/.claude/plugins/cache/context-mode/context-mode/<version>/hooks/core/routing.mjs. Patching~/.claude/plugins/marketplaces/context-mode/...changes nothing at runtime and looks like it worked. Discover the files rather than hardcoding paths.Happy to open a PR for the narrow fix or the config-driven version — say which you'd prefer.