Update — April 3, 2026: After publishing this analysis, I went back through the code and found something that contradicts one of the claims below. I wrote that Haiku "summarizes" your content. That’s wrong — and the distinction matters for how you optimize. Read the correction: Everyone (Including Me) Got the Haiku Pipeline Wrong.
Claude doesn’t see your raw web page. A smaller model (Haiku) summarizes it first, and only the summary reaches the main model. There’s a hardcoded 8-search cap per query. ~100 developer doc domains bypass the summarization layer entirely. And there’s a secret domain blocklist API that silently makes sites invisible to Claude. All of this is in the code.
01How I got here
If you work in GEO, you’ve probably spent months guessing at how Claude handles citations. Does it prefer certain domains? Is there a ranking algorithm? Does it weight freshness? We’ve all been running black-box tests and speculating.
Then Anthropic handed us the answer key.
Version 2.1.88 of @anthropic-ai/claude-code shipped with a 59.8MB source map file that pointed to a zip archive on Anthropic’s own Cloudflare R2 bucket. Inside: 1,903 TypeScript files. The full client-side architecture of Claude Code — the CLI tool, not the model itself — but including every system prompt, every tool definition, and every instruction that shapes how Claude interacts with the web.
I want to be clear about scope. This is the harness, not the model weights. I can’t tell you why Claude prefers one search result over another at the neural-network level. But I can tell you exactly what instructions it receives, what tools it has, what limits are hardcoded, and how your content gets processed between the moment Claude fetches it and the moment it writes a citation. That’s more than we’ve ever had.
Here’s what I found.
02Your content passes through two models, not one
This is the single most important finding in the entire codebase, and I haven’t seen anyone else talk about it yet.
When Claude fetches a URL — whether from search results or a user pasting a link — it doesn’t feed the raw HTML into the main conversation model. Instead, it runs a pipeline:
First, the HTML gets converted to Markdown using Turndown. Then it’s truncated to 100,000 characters (hardcoded as MAX_MARKDOWN_LENGTH). Then — and this is the part that matters — it gets sent to Haiku, Anthropic’s small/fast model, along with the user’s prompt. Haiku produces a summary. Only that summary reaches the main Claude model.
src/tools/WebFetchTool/utils.ts — applyPromptToMarkdown()
const truncatedContent = markdownContent.length > MAX_MARKDOWN_LENGTH
? markdownContent.slice(0, MAX_MARKDOWN_LENGTH) +
'\n\n[Content truncated due to length...]'
: markdownContent
const modelPrompt = makeSecondaryModelPrompt(truncatedContent, prompt, isPreapprovedDomain)
const assistantMessage = await queryHaiku({ ... })The actual code path. Your content hits Haiku before the main model ever sees it.
You’re not writing for Claude. You’re writing for Haiku’s ability to summarize your page into something Claude can use.
Think about what this means for optimization. Haiku is a smaller, less capable model. It’s fast, but it misses nuance. If your key insight is buried in paragraph 14 behind three marketing paragraphs, Haiku is going to summarize around it, and Claude will never see it.
What this means for your content: Front-load your facts. Put the answer, the number, the key claim in the first two paragraphs. Use clear headings that Haiku can use as structural cues. Dense, structured content (tables, numbered steps, definitions) survives lossy summarization better than narrative prose.
Oh, and there’s a 125-character quote limit baked into the Haiku prompt for non-preapproved domains:
Enforce a strict 125-character maximum for quotes from any source document.
Use quotation marks for exact language from articles;
any language outside of the quotation should never be word-for-word the same.Claude enforces a hard 125-character quote ceiling for non-preapproved domains.
Claude is structurally prevented from quoting you verbatim. Your content has to be distillable into Claude’s own paraphrasing. Hard stats, specific numbers, and structured data survive this filter. Fluffy marketing copy doesn’t.
03But ~100 domains skip the Haiku layer entirely
Here’s where it gets interesting. There’s a file called preapproved.ts that contains a hardcoded whitelist of about 100 domains. These domains get their content passed directly to the main model as raw Markdown, bypassing the Haiku summarization step completely.
src/tools/WebFetchTool/preapproved.ts (excerpt)
export const PREAPPROVED_HOSTS = new Set([
// Anthropic
'platform.claude.com', 'code.claude.com', 'modelcontextprotocol.io',
// Languages
'docs.python.org', 'developer.mozilla.org', 'doc.rust-lang.org',
// Frameworks
'react.dev', 'nextjs.org', 'fastapi.tiangolo.com', 'laravel.com',
// Cloud
'docs.aws.amazon.com', 'kubernetes.io', 'cloud.google.com',
// ... ~100 domains total
])Every single one is a developer documentation site. No news sites. No commercial sites. No marketing blogs. No Wikipedia, even.
This creates a two-tier citation system. If you’re react.dev, Claude sees your full documentation in pristine Markdown. If you’re everyone else, Claude sees a Haiku-compressed summary of your page. That’s a massive fidelity advantage for the preapproved tier.
The condition for raw passthrough is strict: the domain must be preapproved, the content-type must be text/markdown, and the content must be under 100K characters. If any of those fail, you fall back to the Haiku pipeline like everyone else.
Since I know the first thing you’re going to do is check whether your backlink targets are on this list, here’s every single domain — straight from the source code, organized exactly as Anthropic categorized them:
From preapproved.ts — these bypass Haiku summarization entirely.
platform.claude.comClaude API docscode.claude.comClaude Code docsmodelcontextprotocol.ioMCP specgithub.com/anthropicsAnthropic GitHubagentskills.ioAgent skills registrydocs.python.orgPythonen.cppreference.comC/C++docs.oracle.comJavalearn.microsoft.comC#/.NET/Azuredeveloper.mozilla.orgMDN Web APIsgo.devGopkg.go.devGo packageswww.php.netPHPdocs.swift.orgSwiftkotlinlang.orgKotlinruby-doc.orgRubydoc.rust-lang.orgRustwww.typescriptlang.orgTypeScriptreact.devReactangular.ioAngularvuejs.orgVue.jsnextjs.orgNext.jsexpressjs.comExpress.jsnodejs.orgNode.jsbun.shBunjquery.comjQuerygetbootstrap.comBootstraptailwindcss.comTailwind CSSd3js.orgD3.jsthreejs.orgThree.jsredux.js.orgReduxwebpack.js.orgWebpackjestjs.ioJestreactrouter.comReact Routerdocs.djangoproject.comDjangoflask.palletsprojects.comFlaskfastapi.tiangolo.comFastAPIpandas.pydata.orgPandasnumpy.orgNumPywww.tensorflow.orgTensorFlowpytorch.orgPyTorchscikit-learn.orgScikit-learnmatplotlib.orgMatplotlibrequests.readthedocs.ioRequestsjupyter.orgJupyterlaravel.comLaravelsymfony.comSymfonywordpress.orgWordPressdocs.spring.ioSpringhibernate.orgHibernatetomcat.apache.orgTomcatgradle.orgGradlemaven.apache.orgMavenasp.netASP.NETdotnet.microsoft.com.NETnuget.orgNuGetblazor.netBlazorreactnative.devReact Nativedocs.flutter.devFlutterdeveloper.apple.comiOS/macOSdeveloper.android.comAndroidkeras.ioKerasspark.apache.orgApache Sparkhuggingface.coHugging Facewww.kaggle.comKagglewww.mongodb.comMongoDBredis.ioRediswww.postgresql.orgPostgreSQLdev.mysql.comMySQLwww.sqlite.orgSQLitegraphql.orgGraphQLprisma.ioPrismadocs.aws.amazon.comAWScloud.google.comGoogle Cloudlearn.microsoft.comAzurekubernetes.ioKuberneteswww.docker.comDockerwww.terraform.ioTerraformwww.ansible.comAnsiblevercel.com/docsVerceldocs.netlify.comNetlifydevcenter.heroku.comHerokucypress.ioCypressselenium.devSeleniumdocs.unity.comUnitydocs.unrealengine.comUnreal Enginegit-scm.comGitnginx.orgNginxhttpd.apache.orgApache HTTPA few things jump out from the full list. Two domains are path-scoped: github.com/anthropics only matches Anthropic’s own repos (not all of GitHub), and vercel.com/docs only matches the docs subdirectory. The code enforces this at segment boundaries — /anthropics won’t match /anthropics-evil/malware. Also worth noting: learn.microsoft.com appears in both the language docs and cloud categories, covering .NET, C#, and Azure from a single domain. If you’re building backlinks in the dev docs space, this is your target list.
04The 8-search ceiling
When Claude decides to search the web, the WebSearchTool creates a server-side tool schema with a hardcoded limit:
function makeToolSchema(input: Input): BetaWebSearchTool20250305 {
return {
type: 'web_search_20250305',
name: 'web_search',
max_uses: 8, // Hardcoded to 8 searches maximum
}
}The hardcoded 8-search ceiling in WebSearchTool.
Each WebSearch invocation can run up to 8 sub-queries internally. A simple factual question might trigger 1-2. A complex, multi-faceted question could use all 8. More sub-queries = more SERP results = more chances for your domain to appear in the citation set.
This is consistent with something I’ve seen in other AI search systems — Perplexity does the same thing with their reranker stages. The more complex the user’s question, the wider the net gets cast. But there’s a hard ceiling, and 8 is it.
The search results come back as simple {title, url} objects. No snippets. No meta descriptions. Just the page title and the URL. That’s what Claude uses to decide what to cite.
Your title tag is your citation label. When Claude writes Source Title in its response, "Source Title" comes directly from your <title> element as it appears in search results. A descriptive, self-contained title that includes the key entity or topic is what gets a user to trust and click that citation. This isn’t speculation — it’s in the code.
05The mandatory citation instruction
Every single WebSearch result gets a postscript appended to it before reaching the model:
formattedOutput += '\nREMINDER: You MUST include the sources above in your '
+ 'response to the user using markdown hyperlinks.'This reminder is injected into every single search result Claude processes.
And the tool prompt itself contains a "CRITICAL REQUIREMENT" section mandating a Sources: section with linked URLs. This isn’t soft guidance. It’s injected into every search result. Claude has to cite — the question is whether it cites you.
I also checked whether this instruction persists across sessions. It does — the system prompt is divided into static (cached globally) and dynamic (per-session) sections separated by a SYSTEM_PROMPT_DYNAMIC_BOUNDARY marker. The citation requirement lives in the tool prompt, which is part of the static section. Anthropic won’t be changing this casually because it would break their prompt cache across all users.
06The silent domain blocklist
This one caught me off guard. Before Claude fetches any URL, it makes a preflight API call to Anthropic’s servers:
const response = await axios.get(
\`https://api.anthropic.com/api/web/domain_info?domain=${encodeURIComponent(domain)}\`,
{ timeout: DOMAIN_CHECK_TIMEOUT_MS },
)
if (response.data.can_fetch === true) {
DOMAIN_CHECK_CACHE.set(domain, true)
return { status: 'allowed' }
}
return { status: 'blocked' }Your domain can be silently blocked. No error message. No "I couldn’t access that site." Just... nothing.
If can_fetch comes back false, your domain is silently blocked. No error message to the user. The fetch doesn’t happen.
Allowed domains get cached for 5 minutes (DOMAIN_CHECK_CACHE). Blocked domains are not cached — they’re re-checked every time, which means Anthropic can unblock a domain and it takes effect immediately.
Enterprise customers can skip this with a skipWebFetchPreflight setting. Regular users can’t. There’s no public documentation of what’s blocked or why.
If your site suddenly vanishes from Claude’s citations, this API is the first thing to investigate. You could be ranking #1 in search results and Claude would still never see your page if your domain is on this blocklist. It’s a single point of failure that operates completely outside of traditional SEO or GEO visibility.
07Anti-distillation: why black-box testing has a noise floor
I want to flag something for anyone who’s been running experiments on Claude’s citation behavior by hitting the API. The source reveals an anti-distillation system specifically designed to pollute the signal:
// Anti-distillation: send fake_tools opt-in for 1P CLI only
if (feature('ANTI_DISTILLATION_CC') ? ... : false) {
result.anti_distillation = ['fake_tools']
}When enabled, this tells the API server to inject decoy tool definitions into the system prompt. Any competitor (or researcher) scraping Claude’s responses to reverse-engineer its behavior would inherit fake tool schemas. There’s also a "streamlined mode" that strips tool-call details and replaces them with vague summaries like "searched 3 patterns, read 5 files."
The practical implication: if you’ve been testing citation patterns by observing Claude’s API outputs, some of what you’re seeing may be deliberately injected noise. This doesn’t mean black-box testing is useless — but it means your confidence intervals should be wider than you think.
08The memory system creates citation flywheels
Claude Code has a persistent memory system at ~/.claude/projects/<slug>/memory/ that stores user preferences, feedback on past behavior, and project context. The system explicitly tells Claude:
"You should build up this memory system over time so that future conversations can have a complete picture of who the user is, how they’d like to collaborate with you, what behaviors to avoid or repeat."
This is file-based, survives across sessions, and includes a taxonomy of memory types: user, feedback, project, and reference. If a user finds your content useful and Claude notes that preference, it persists. Future sessions with that user are influenced by stored memories.
In the unreleased KAIROS mode (an autonomous daemon that runs continuously), this gets even more interesting. Memories shift to append-only daily logs, and a background "DreamTask" consolidates them nightly — literally a sleep-cycle metaphor for AI memory consolidation. The model reviews its recent sessions while idle and distills learnings into topic files.
First impressions compound. If you’re the authoritative source a user encounters early on a topic, Claude may store that as a preference that persists. Being cited once isn’t just a one-time win — it can become a durable advantage through the memory system. This is especially true for B2B and developer audiences who use Claude Code repeatedly for the same project contexts.
09What’s coming: KAIROS and the always-on citation surface
The codebase is full of references to KAIROS — an unreleased autonomous agent mode where Claude runs continuously in the background, proactively searching for information and briefing the user through a SendUserMessage tool (internally called "Brief").
The Brief tool prompt says it all:
SendUserMessage is where your replies go. Text outside it is visible if the user
expands the detail view, but most won’t — assume unread. Anything you want them
to actually see goes through SendUserMessage.KAIROS means Claude will initiate searches without the user asking. Scheduled tasks finish and Claude proactively checks for updates. Background work surfaces blockers. This isn’t speculative — the code is fully built behind feature flags, with compiled paths in main.tsx.
For GEO, this expands the citation surface from "user asks a question" to "Claude decides to look something up on its own." Content that covers time-sensitive, frequently-changing topics — the exact category that triggers web search over training data — gets more opportunities to surface as always-on modes ship.
10Some things I was hoping to find but didn’t
Transparency is important, so here’s what isn’t in the leaked code:
No client-side ranking algorithm. The search happens server-side via a web_search_20250305 tool type. The client sends the query and gets back results. Whatever ranking, reranking, or quality filtering Anthropic does happens inside their API infrastructure, not in the Claude Code client. The leaked code is the harness, not the brain.
No freshness scoring. Unlike what I’ve found in ChatGPT’s configuration (where use_freshness_scoring_profile: true is explicitly set), there’s no client-side freshness signal in Claude’s code. This doesn’t mean Claude doesn’t prefer recent content — it almost certainly does — but that logic lives in the model weights or the server-side search, not in the client prompt.
No domain authority scoring. There’s no E-E-A-T equivalent or domain reputation system visible in the code. The preapproved list is purely for the Haiku-bypass, not for ranking.
11The full picture: what to actually do with this
Putting it all together, here’s the optimization stack that falls directly out of the code:
Prioritized by direct evidence from the codebase.
Front-load facts in first 2 paragraphs
Haiku summarization loses nuance — lead with the answer
Write descriptive, self-contained title tags
Search results are {title, url} only — title IS the citation text
Target post-cutoff and dynamic topics
Claude only searches when training data is insufficient
Keep pages under 100K characters
MAX_MARKDOWN_LENGTH truncates everything beyond
Use structured content (tables, lists, stats)
Survives 125-char quote limit and Haiku compression
Monitor for domain blocklist inclusion
domain_info API silently blocks domains
Win in traditional search first
Claude's server-side search feeds from standard search results
Produce content for complex queries
Complex questions use more of the 8-search cap = wider citation net
12What I’m going to look at next
The leaked code raises some questions I want to dig into:
Can we hit the domain_info API endpoint directly to check if a domain is blocked? The code shows it’s a simple GET request with no authentication in the client — but it might require an API key or session token server-side.
How much information loss actually happens in the Haiku summarization step? I’m planning to run a comparison: take 50 pages, fetch them through Claude’s WebFetch pipeline, and compare the Haiku summary to the original content to measure what gets dropped.
And the GrowthBook feature flag tengu_plum_vx3 that controls whether searches go to Haiku or the main model — what percentage of users are in each cohort? This could mean citation quality varies significantly between users and nobody realizes it.
I’ll update this post as I find answers.
Methodology: This analysis is based on direct examination of the Claude Code v2.1.88 source code leaked via npm source map on March 31, 2026. All code excerpts are from the leaked TypeScript files. File paths and line references are traceable to the original codebase. No black-box testing or speculation was used for the findings above — every claim is backed by a specific file and function.
13Sources and related reading
Fortune: Anthropic leaks its own AI coding tool’s source code
The Register: Anthropic accidentally exposes Claude Code source code
Metehan.ai: I Found It in the Code — The Recency Bias Reshaping AI Search
Metehan.ai: Perplexity AI Ranking 59+ Factors Revealed
Dani Leitner: When Do LLMs Search the Internet?
Chatoptic: Claude’s Leaked System Prompt — 12 Key Takeaways for GEO