
Claude Web Fetch url_not_in_prior_context Error, Fixed
url_not_in_prior_context means Claude tried to fetch a URL that nobody handed it. The web fetch tool only fetches URLs that came from a user message, a search or fetch result, or (on the Messages API) one of your own tool results. A URL that lives only in your system prompt, in Claude's own reply or in an MCP result gets refused. Put the exact URL in the user message and it works.
What Does url_not_in_prior_context Mean?
It's a refusal, not a network failure. Claude decided to call web_fetch, the API checked where that URL came from, and the answer was "nowhere it trusts." The web fetch docs put it plainly: Claude can only fetch URLs that have previously appeared in the conversation, and "cannot fetch URLs that appear only in its own output."
The reason is data exfiltration. Picture an agent that reads a customer's inbox and can fetch web pages. One email contains hidden text telling Claude to fetch https://attacker.example/?d= plus whatever it just read. If Claude could fetch any URL it assembles, that single GET request leaks the data, and nobody ever sees a suspicious reply. Refusing URLs Claude made up on its own shuts that door. I covered the wider version of this attack in MCP tool poisoning, and it's the same reason I sandbox fetch steps in n8n agents that read untrusted input.
One thing trips people up first: the request still returns HTTP 200. The error sits inside a web_fetch_tool_result block as {"type": "web_fetch_tool_result_error", "error_code": "url_not_in_prior_context"}. Claude reads it and carries on, often with a polite "I wasn't able to access that page." If your logging only watches status codes, you'll never see it.
Which URLs Count as Prior Context?
This is where the Messages API and Claude Managed Agents part ways. Managed Agents got its own, stricter version of the rule on October 7, 2026, per the Claude Platform release notes. Here's the side-by-side from both docs pages:
| Where the URL appears | Messages API | Managed Agents |
|---|---|---|
| Text of a user message | Yes | Yes (user.message event) |
| web_search results | Yes | Yes |
| Links inside a page web_fetch returned | Yes | Yes |
| Your client-side tool results | Yes, even if they echo Claude's text | Not listed; don't rely on it |
| System prompt | No | No |
| Claude's own output | No | No |
| Code execution, MCP connector, tool search results | No | No (bash, read, MCP tools) |
| Attached documents | Not listed as a source | No |
The big difference is tool output. On the Messages API, your own client-side tool results count. On Managed Agents, output from bash, read and MCP tools doesn't. So an agent that runs cat urls.txtin its sandbox and then tries to fetch each line will fail on every single one. The release note's own fix is to send the URL in the text of a user.message event.
Two more rules sit right next to this one. A URL that looks like it carries a credential (an API key, a password) is refused with url_not_allowedunless that credential also appears in the system prompt or the user's message text. And any URL over 250 characters gets url_too_long, no matter where it came from.
Why Am I Getting It? The Five Usual Causes
1. The URL is only in the system prompt.This is the most common one. You wrote "our docs are at https://…" in the system prompt, the user asks a question, Claude tries to fetch the docs and gets refused. It feels wrong because you wrote that prompt, but the rule doesn't care who wrote it.
2. Claude built the URL.Ask about "the anthropic-sdk-python README" and Claude will happily assemble a GitHub URL from memory. It's probably right, and it's still refused, because it only exists in Claude's output. Give it the exact URL, or enable web_search so it finds the page first.
3. The URL came from a server tool.Code execution, the MCP connector and tool search results don't count on the Messages API. That includes dynamic filtering: Python that pulls links out of a page doesn't make them fetchable. The links have to be in the fetched page itself.
4. On Managed Agents, the URL came from a file or a tool. An attached CSV of competitor pages, a read of a config file, an MCP tool that lists sources. None of these count since October 7.
5. The URL got slightly changed. This one is my guess from how matching usually works, not something the docs spell out: if Claude adds a query string, swaps http for httpsor normalizes a path, assume it's a new URL. When a fetch you expected to pass gets refused, diff the requested URL against the one you supplied before you blame the rule.
How Do You Fix url_not_in_prior_context?
Start by catching it. Because the status code is 200, you need to walk the content blocks and pair each fetch result with the URL Claude asked for:
resp = client.messages.create(
model="claude-sonnet-5-5",
max_tokens=2048,
tools=[{"type": "web_fetch_20260318", "name": "web_fetch",
"allowed_domains": ["docs.example.com"], "max_uses": 5}],
messages=[{"role": "user", "content": prompt}],
)
urls = {b.id: b.input.get("url") for b in resp.content
if b.type == "server_tool_use" and b.name == "web_fetch"}
for b in resp.content:
if b.type == "web_fetch_tool_result" and b.content.type == "web_fetch_tool_result_error":
log.warning("web_fetch %s -> %s", urls.get(b.tool_use_id), b.content.error_code)Then move the URL to a source the tool accepts. For the system-prompt case, the fix is one line: say the URL again in the user turn.
DOCS = "https://docs.example.com/api/changelog"
system = "You answer questions about our API. Official docs: " + DOCS # not fetchable
messages = [{
"role": "user",
"content": f"Using {DOCS}, what changed in the last release?", # fetchable
}]For the other cases, pick the option that matches where the URL lives:
- You know the URLs ahead of time:list them in the user message. It works on both products and it's the easiest thing to reason about.
- Claude has to discover them: enable
web_searchnext toweb_fetch. Search results are an allowed source, so search-then-fetch works without you naming anything. - They live in your database (Messages API): expose a client-side tool like
get_source_urls(topic)that reads from your own table. Its result counts, and you decide what goes in it. - You're on Managed Agents: have your orchestrator read the file or MCP output and post the URLs as a
user.messageevent. It's an extra hop, but it's the documented fix.
Whatever you choose, keep allowed_domains and max_uses on the tool. Failed fetches count against max_uses, and there's no default limit, so a confused agent can burn through retries. I go deeper on tool definitions in my Claude tool use notes.
Should You Build a Tool That Echoes URLs Back?
The docs leave a door open. Client-side tool results count "even when a result echoes text that Claude generated." So you could write a remember_url(url) tool that returns its input, and every URL Claude invents would become fetchable on the next turn.
Don't. An echo tool rebuilds the exact exfiltration path the rule exists to close. Anthropic isn't pretending that loophole doesn't exist. They're telling you that what your tools return is your responsibility. If a tool hands URLs back, it should get them from somewhere you trust: your database, an allowlist, a sitemap you crawled yourself. Never from Claude's arguments.
If you really do need Claude to browse freely, use a real browser you control instead. I compared the options in Claude's browser use tool vs computer use. Web fetch doesn't render JavaScript anyway, so for a lot of modern pages it was never the right tool.
How Is It Different From the Other Web Fetch Errors?
All of these arrive the same way, as an error block inside a 200 response. Quick triage:
- url_not_in_prior_context: wrong source. Move the URL.
- url_not_allowed: your
allowed_domainsorblocked_domains, org settings, robots.txt, a private address, or a URL that looks like it holds a credential. - url_too_long: over 250 characters. Strip tracking parameters.
- url_not_accessible:the site returned an HTTP error. The rule let it through; the server didn't.
- unsupported_content_type: only text, HTML and PDF are supported.
- max_uses_exceeded, too_many_requests, unavailable: limits and internal errors. Back off and retry the last two.
On Managed Agents with limited networking there's one more layer. Since October 7, allowed_hosts also applies to web tools: a fetch outside it returns url_not_allowed, and creating a session fails with a 400 if a web tool's allowed_domains lists a host that allowed_hosts doesn't cover. Note that example.com does not cover docs.example.com; you need *.example.com.
How I Use Web Fetch Without Hitting It
My research agents (including the one that drafts posts on this blog) follow the same shape: search first, fetch only what search returned, never fetch from a list the model typed out. When I already know the sources, like a vendor's release notes page, the URL goes in the task text the agent receives, not in its system prompt. That habit predates this rule. It just happens to be exactly what the rule wants.
My take: treat url_not_in_prior_context as a design review, not a bug. Every time it fires, it's pointing at a spot where a URL entered your agent from a place you weren't checking. Fix the plumbing so trusted URLs come in through trusted channels, and the error goes away on its own.
How I checked this: the source lists, error codes and limits above are quoted from Anthropic's web fetch tool page and the October 7, 2026 release notes, read on October 12, 2026. The detection snippet uses the documented response shape; adjust attribute access if you're on raw JSON instead of the Python SDK.
Claude Web Fetch Error FAQ
What does url_not_in_prior_context mean?
It means Claude tried to fetch a URL that no allowed source put into the conversation. The web fetch tool only fetches URLs from user messages, search or fetch results, and (on the Messages API) client-side tool results. It's a security rule, not an outage.
Why can't Claude fetch a URL from my system prompt?
The system prompt isn't an allowed source for web fetch on either the Messages API or Managed Agents. Repeat the URL in the user message text and the fetch goes through.
Is url_not_in_prior_context an HTTP error?
No. The API returns HTTP 200 and puts a web_fetch_tool_result_error block with error_code url_not_in_prior_context inside the response. Claude sees it and keeps going, so you have to inspect the content blocks to catch it.
Do MCP tool results count as prior context for web fetch?
No. On the Messages API, results from the MCP connector, code execution and tool search don't count. On Managed Agents, output from bash, read and MCP tools doesn't count either. Client-side tool results on the Messages API do count.
Can I build a tool that echoes URLs to get around the rule?
Technically yes on the Messages API, because client tool results count even when they echo Claude's own text. Don't. An echo tool hands back exactly the exfiltration path the rule closes. Return URLs from your own data or allowlist instead.
Need an Agent That Reads the Web Safely?
I build Claude agents and n8n automations that research, fetch and act on live data, with the URL sources, domain fences and logging set up so they don't leak or loop. Tell me what you want it to read.
Related Posts
AI Agents
Claude Agent Memory Stores vs the Memory Tool
A memory store is a versioned folder Anthropic mounts into your agent's sandbox at /mnt/memory. How it differs from the client-side memory tool, the read_write default that enables persistent prompt injection, the 10,000-memory cap that fails silently, and the 15-second sync on self-hosted sandboxes.
AI Agents
Claude Mid-Conversation Tool Changes: Keep the Cache
Editing the tools array invalidates the prompt cache for the whole conversation. tool_addition and tool_removal change what Claude can call without ever touching it — plus the placement rules that 400.
AI Agents
Claude Tool Use Best Practices for Reliable Agents
The biggest lever on whether a Claude agent calls the right tool isn't the model — it's how you write the tools. Four habits fix most "wrong tool" bugs: write each description like it's the only docs the model gets (say what it does AND when not to use it — overlap is the #1 cause of misfires), make the input schema strict with enums, required fields, and per-parameter descriptions (then validate in your handler anyway), return errors as tool results with is_error and a plain next-step message instead of a raw stack trace or empty string, and keep the tool count small — three or four to start, consolidate or hand the model code before listing fifty. Force tool_choice only when the action is singular; parallelize only genuinely independent calls. The model reads your name, description, and schema — not your code — so those definitions are a prompt you forgot you were writing.