●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October●FORK — Claude Code 2.1.212 changes what /fork does: it copies your conversation into a new background session with its own row in claude agents, so you can keep working. The old in-session subagent is now /subtask●LIMITS — WebSearch calls are now capped at 200 per session by default, and subagent spawns get the same 200 ceiling, so a runaway search or delegation loop stops on its own●MCPBG — MCP tool calls running past two minutes now move to the background automatically, keeping the session usable. Tune the threshold with CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS●PLANFIX — Fixed plan mode auto-running file-modifying Bash commands such as touch and rm without a permission prompt or an SDK canUseTool callback●SONNET5 — Claude Sonnet 5 is running on introductory pricing of $2 per million input tokens and $10 per million output. After August 31 it moves to $3 and $15●IPO — Bankers are reportedly lining up investor meetings for Anthropic ahead of a possible public listing as soon as October
Production-Grade Hallucination Defense for Claude API: A Multi-Layer Architecture
Prompt engineering alone is not enough to suppress hallucinations in production. After a real customer incident, I rebuilt the system around four defensive layers — input grounding, tool-use escape hatches, citations, and post-hoc verification. This is the implementation playbook.
"A user complained that we gave them a fake phone number." That was a real incident in a customer-support agent I was running last year. The system prompt explicitly said "do not guess information you don't know." It was Claude Sonnet 4.5 at temperature 0.3, with several integrated tools. By the standards of the time, it was a carefully built agent.
Hallucinations still happen. They happen in production.
This article starts from the moment I gave up on the assumption that better prompts alone would solve the problem. With four defensive layers — prompt-side grounding, inference-time tooling, output-time schema enforcement, and post-hoc verification — how far can you actually push fact-error rates down? I have collected the seven implementation patterns that made the largest difference in the systems I currently operate, with TypeScript code that runs as-is.
Why a single prompt loses
If you describe hallucination as "Claude lies sometimes," you will pick the wrong countermeasures. In production, I see at least three distinct failure modes.
Fabrication errors: Claude invents proper nouns, numbers, or URLs that look plausible but were never in the training data. Phone numbers, person names, and price tables are the most painful, because the business impact is direct.
Reasoning errors: Claude misreads the supplied context. The document says "cancellation is free up to 30 days in advance," and the answer says "cancellation is always free."
Instruction-skipping errors: You ask for JSON and get prose. You ask for a polite tone and the response slips into casual phrasing. These are hallucinations too, in the broad sense.
The three modes have different root causes. Fabrication is "refusal to admit not knowing." Reasoning errors are "failure to read context carefully." Instruction-skipping is "loss of formatting attention over long generations."
If you try to fix all three by stuffing more rules into a single prompt, the prompt swells and accuracy actually drops. My conclusion was simple: separate the layers, and use a different mechanism for each.
Layer 1: Input grounding as a hard contract
The most effective single intervention is to make it impossible for Claude to answer from its own knowledge. This is the basic idea of RAG (retrieval-augmented generation), but the production-quality move is to design grounding as a strong contract, not a polite request.
import Anthropic from "@anthropic-ai/sdk";const client = new Anthropic();interface RetrievedDoc { id: string; title: string; content: string; source_url: string;}async function answerWithGrounding( question: string, docs: RetrievedDoc[]): Promise<string> { // Bad pattern: "please refer to these documents" // Production pattern: a hard contract — anything outside the documents is a violation const groundedSystem = `You are an internal knowledge bot.Absolute rules:1. You may only ground answers on information inside the <documents> section below.2. Do not include any fact that is not present in <documents>, even if it is common knowledge.3. If the documents are insufficient, answer exactly: "I cannot determine this from the provided information."4. No guessing or inference. Quote proper nouns, numbers, dates, and URLs verbatim from <documents>.Output format: end every response with "Sources: [doc_id1, doc_id2]" listing the documents you used.`; const docsBlock = docs .map((d) => `<document id="${d.id}" title="${d.title}">\n${d.content}\n</document>`) .join("\n\n"); const response = await client.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1024, system: groundedSystem, messages: [ { role: "user", content: `<documents>\n${docsBlock}\n</documents>\n\nQuestion: ${question}`, }, ], }); const text = response.content .filter((b): b is Anthropic.TextBlock => b.type === "text") .map((b) => b.text) .join("\n"); return text;}// Usage:// const answer = await answerWithGrounding("What is the refund policy?", retrievedDocs);// Expected output:// "Refunds are available in full for cancellations within 30 days of purchase.// Sources: [doc_42]"
The non-obvious detail here is that "absolute rules" needs three to five entries, not one. A single rule gets bent in ambiguous situations; multiple rules reinforce one another, and in my A/B tests violation rates roughly halved when I went from one rule to four. This is a quirk of how Claude weights instructions that I have confirmed several times in production traffic.
Wrapping the supplied content in <documents> tags is also intentional. Anthropic's prompt guidance recommends XML over Markdown delimiters, and in my experience Claude's attention locks on tagged blocks far more reliably than on --- separators.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦You will get a complete implementation path that takes hallucination from a customer-complaint problem to an acceptable residual risk — even after every prompt rewrite has stopped helping
✦You will learn to wire up self-consistency checks, LLM-as-judge verification, the Citations API, and JSON Schema guards together — with copy-paste TypeScript that runs as-is
✦You will know in advance the production-only failure modes such as judges going soft, citations returning empty strings, and tool schemas inviting fabrication, so you design around them from day one
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Grounding alone does not stop reasoning errors — Claude can still misread the documents. The fix is to give Claude a structured way to declare "I cannot answer," using Tool Use.
const escapeHatchTools: Anthropic.Tool[] = [ { name: "answer_with_citation", description: "Answer the user's question using the supplied documents as the only source. " + "You must include the IDs of the documents you cited.", input_schema: { type: "object" as const, properties: { answer: { type: "string", description: "Answer text shown to the user" }, citations: { type: "array", items: { type: "string" }, description: "Document IDs used as evidence (at least one required)", }, confidence: { type: "string", enum: ["high", "medium", "low"], description: "Your confidence in this answer", }, }, required: ["answer", "citations", "confidence"], }, }, { name: "decline_to_answer", description: "Use this when the documents do not contain enough information, when the question is " + "out of scope, or when it is ambiguous. Do not guess instead of calling this tool.", input_schema: { type: "object" as const, properties: { reason: { type: "string", enum: ["insufficient_information", "out_of_scope", "ambiguous_question"], }, suggested_action: { type: "string", description: "An alternative to suggest to the user (e.g., contact a human, provide more detail)", }, }, required: ["reason", "suggested_action"], }, },];async function answerWithEscapeHatch(question: string, docs: RetrievedDoc[]) { const docsBlock = docs .map((d) => `<document id="${d.id}">${d.content}</document>`) .join("\n"); const res = await client.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1024, tools: escapeHatchTools, tool_choice: { type: "any" }, // Force tool use system: "If you can answer, call answer_with_citation. If you cannot, call decline_to_answer. " + "Do not return plain text under any circumstances.", messages: [ { role: "user", content: `<documents>\n${docsBlock}\n</documents>\n\n${question}` }, ], }); const toolUse = res.content.find( (b): b is Anthropic.ToolUseBlock => b.type === "tool_use" ); if (!toolUse) { throw new Error("Claude returned no tool call - check tool_choice config"); } return { tool: toolUse.name, input: toolUse.input };}
Why does this help so much more than the same instruction in plain prose? When you ask Claude to "say I don't know," compliance is a soft preference. When you make it call a decline_to_answer tool with a typed schema, refusal becomes an action with structured arguments. Claude resists the pull toward giving a vague answer because the alternative path is a concrete first-class action it has been told to use.
The implementation detail that matters most is tool_choice: { type: "any" }. Without it, Claude decides on its own whether to use a tool or return plain text, and you will see plain-text leakage. In my production traffic this single line moved violation rates by an order of magnitude.
Making the confidence field required is also deliberate. Once Claude has to declare its own confidence, you can branch on low to add a "please confirm with us if this is critical" footer, or to escalate to a human reviewer.
Layer 3: Citations API to mechanically force evidence
Anthropic's Citations feature makes Claude attach the exact source span for each sentence it generates. Because Claude must point at real text in the input, fabrication of strings that are not in the source becomes structurally harder.
async function answerWithCitations(question: string, docs: RetrievedDoc[]) { const documentBlocks = docs.map((d) => ({ type: "document" as const, source: { type: "text" as const, media_type: "text/plain" as const, data: d.content, }, title: d.title, citations: { enabled: true }, })); const res = await client.messages.create({ model: "claude-sonnet-4-5", max_tokens: 1024, messages: [ { role: "user", content: [...documentBlocks, { type: "text", text: question }], }, ], }); // Each text block has a citations array attached const annotated = res.content.flatMap((block) => { if (block.type !== "text") return []; return [ { text: block.text, citations: block.citations ?? [], }, ]; }); // Segments with no citation are high hallucination risk — flag them for (const seg of annotated) { if (seg.text.trim().length > 30 && seg.citations.length === 0) { console.warn(`Uncited segment: "${seg.text.slice(0, 50)}..."`); } } return annotated;}
When citations are enabled, Claude experiences a cognitive constraint: every sentence needs a pointer to source text. After generation, cited_text (the exact original string) is attached to each segment, which means you can apply mechanical post-filters such as "drop any segment whose citations array is empty" or "reject any number that doesn't appear in the cited text."
Layer 4: Self-consistency to weed out unstable answers
This is where the production-grade work begins. Send the same question 3–5 times and check whether the answer is stable. Hallucinations are stochastic, so they tend to vary between samples; if Claude returns the same fact on every run, your trust in it is much higher.
import crypto from "node:crypto";interface SelfConsistencyResult { consensus_answer: string | null; agreement_rate: number; variations: string[];}async function selfConsistencyAnswer( question: string, docs: RetrievedDoc[], samples = 5): Promise<SelfConsistencyResult> { const docsBlock = docs.map((d) => `<doc id="${d.id}">${d.content}</doc>`).join("\n"); // Raise temperature to inject diversity, then check whether answers still converge const responses = await Promise.all( Array.from({ length: samples }, () => client.messages.create({ model: "claude-sonnet-4-5", max_tokens: 256, temperature: 0.7, system: "Extract one factual sentence from the supplied documents. " + "No preamble, no explanation, just the fact.", messages: [ { role: "user", content: `<docs>${docsBlock}</docs>\n\nQuestion: ${question}` }, ], }) ) ); const variations = responses.map((r) => r.content .filter((b): b is Anthropic.TextBlock => b.type === "text") .map((b) => b.text.trim()) .join("") ); // Group by exact normalized match (use embedding clustering for production) const counts = new Map<string, number>(); for (const v of variations) { const key = normalize(v); counts.set(key, (counts.get(key) ?? 0) + 1); } const sorted = Array.from(counts.entries()).sort((a, b) => b[1] - a[1]); const top = sorted[0]; return { consensus_answer: top && top[1] >= Math.ceil(samples / 2) ? top[0] : null, agreement_rate: top ? top[1] / samples : 0, variations, };}function normalize(s: string): string { return s .replace(/\s+/g, " ") .replace(/[.,;:!?'"()\[\]]/g, "") .toLowerCase();}
In practice I treat agreement_rate >= 0.6 as the trust threshold. Below that, the system either replies "this answer was unstable, please confirm with a human" or escalates to Opus.
The trade-off is obvious: 5x the API calls and significantly higher latency. In production I only apply self-consistency to the small slice of traffic where errors are expensive — high-stakes pricing displays, medical or legal information, and similar. That slice is around 5% of total requests in my system.
Layer 5: LLM-as-judge for independent verification
This is the strongest single move in the playbook. After generating an answer, pass it to a separate Claude session — a fresh system prompt, a clean conversation — and ask whether the answer is supported by the source documents.
interface JudgeVerdict { verdict: "supported" | "unsupported" | "partially_supported"; reasoning: string; problematic_claims: string[];}async function judgeAnswer( question: string, answer: string, source_docs: RetrievedDoc[]): Promise<JudgeVerdict> { const docsBlock = source_docs.map((d) => `[${d.id}] ${d.content}`).join("\n\n"); // Make the judge adversarial. A friendly judge defeats the entire layer. const judgeSystem = `You are a strict factual auditor. You evaluate whether an AI's answer is genuinely supported by the supplied source documents.Criteria:- Proper nouns, numbers, dates, and URLs in the answer must appear verbatim in the sources to count as supported- "Common-sense correct" or "reasonable inference" never counts as supported- If part of the answer is correct but other parts include unsupported information, mark partially_supported- Even if the answer looks fine, list any subtle inaccuracies you can findFor each problem, add an entry to problematic_claims as "Claim: Issue".`; const res = await client.messages.create({ model: "claude-opus-4-6", // Use a strong model for the judge max_tokens: 1024, system: judgeSystem, tools: [ { name: "submit_verdict", description: "Submit the factual verification verdict", input_schema: { type: "object" as const, properties: { verdict: { type: "string", enum: ["supported", "unsupported", "partially_supported"], }, reasoning: { type: "string" }, problematic_claims: { type: "array", items: { type: "string" } }, }, required: ["verdict", "reasoning", "problematic_claims"], }, }, ], tool_choice: { type: "tool", name: "submit_verdict" }, messages: [ { role: "user", content: `Sources:\n${docsBlock}\n\n` + `User question: ${question}\n\n` + `AI answer: ${answer}\n\n` + `Is this answer supported by the sources? Apply the criteria strictly.`, }, ], }); const tool = res.content.find( (b): b is Anthropic.ToolUseBlock => b.type === "tool_use" ); if (!tool) throw new Error("Judge did not return a verdict"); return tool.input as unknown as JudgeVerdict;}// Pipeline compositionasync function safeAnswer(question: string, docs: RetrievedDoc[]) { const draft = await answerWithGrounding(question, docs); const verdict = await judgeAnswer(question, draft, docs); if (verdict.verdict === "unsupported") { return { shown_to_user: "I am sorry — I do not have enough verified information to answer that confidently.", internal_log: { draft, verdict }, }; } if (verdict.verdict === "partially_supported") { const refined = await refineWithFeedback(draft, verdict.problematic_claims, docs); return { shown_to_user: refined, internal_log: { draft, verdict, refined } }; } return { shown_to_user: draft, internal_log: { draft, verdict } };}async function refineWithFeedback( draft: string, issues: string[], docs: RetrievedDoc[]): Promise<string> { const docsBlock = docs.map((d) => `[${d.id}] ${d.content}`).join("\n"); const res = await client.messages.create({ model: "claude-opus-4-6", max_tokens: 1024, system: "Rewrite the draft so that every claim is supported by the supplied sources. " + "Return only the rewritten body.", messages: [ { role: "user", content: `Sources: ${docsBlock}\n\nDraft: ${draft}\n\nIssues:\n${issues.join("\n")}`, }, ], }); return res.content .filter((b): b is Anthropic.TextBlock => b.type === "text") .map((b) => b.text) .join("");}
The reason this works is mostly social: when the same model both generates and evaluates, a confirmation bias creeps in and verdicts soften. Use a separate session with a separate system prompt, ideally a stronger model (Sonnet for generation, Opus for the judge). After I added this loop, hallucination-driven complaints in my system dropped to roughly zero. Costs went up by 2–3x, but compared to the cost of incident response and customer apology emails, it is dramatically cheaper.
Layer 6: Schemas and types to enforce shape
For instruction-skipping hallucinations — "I asked for JSON, I got prose" — combine Tool Use schemas with a runtime validator like Zod.
The non-obvious detail is feeding the previous error back into the next attempt. Once Claude knows what failed, it usually corrects on the next try. With three retries, this pattern reaches near-100% success in my measurements. JSON Schema's enum, required, and regex are good but not enough — Zod's runtime checks catch cases like 2026-13-45 where the format matches but the value is impossible.
Production pitfalls I have actually hit
Once you have the theory in place, you will encounter a specific set of issues during operation. Knowing them ahead of time saves days of debugging.
Judges go soft over time. Telling the judge "be strict" rarely lasts. Add specific phrases like "find faults from an adversarial perspective" and "use partially_supported aggressively." I sample 10 judge verdicts per week against human review, and rewrite the judge prompt the moment the gap widens.
Citations API can return empty cited_text. This happens with very long documents or when the cited fragment falls inside the middle of a sentence. Always log uncited segments and route them to human review.
All five self-consistency samples disagree. This is usually not a hallucination signal — it is a question that is too ambiguous to answer. In that case, ask the user a clarifying question before generating a final answer.
Complex Tool Use schemas invite fabrication. If your schema has more than three nested levels or more than ten required fields, Claude fabricates values to satisfy the structure. Keep schemas simple enough to scan in 30 seconds, and break complex extraction into two tool calls.
Judge cost balloons. Using Opus for every judge call is expensive. I ended up with a tiered filter: Sonnet generates, Sonnet judges, and Opus only re-judges the ones marked unsupported. Keeping Opus to under 10% of traffic kept quality high while staying inside the cost budget.
Prompt caching does not play nicely with the judge. Judge inputs change every request because they include the generated answer. You can still cache the source documents portion. See Cutting your monthly Claude API bill in half with prompt caching for the implementation details.
Thresholds are domain-specific. The right agreement_rate and the right tolerance for partially_supported vary by use case. Medical advice still struggles at 0.9; a casual chatbot is fine at 0.5. Run a 100-sample manual evaluation in your domain before going live and pick thresholds from data, not hope.
Observability without which you are flying blind
A defensive architecture is only as good as the signals you collect about it in production. Once the layers are wired up, the next step is to make their behavior visible. I instrument three categories of signals on every request, and I would not run any of these defenses without them.
The first category is per-layer rejection rates. For each layer, I log how often it triggered: how often grounding refused to answer, how often the escape hatch fired, what fraction of segments came back uncited, and how often the judge marked an answer unsupported. These numbers should not drift suddenly. A jump in escape-hatch usage often means an upstream retrieval pipeline degraded; a jump in judge rejections often means the model was updated and your prompts need to follow.
The second category is end-to-end latency by layer. Self-consistency adds 3–5x latency, and the judge typically adds another 1.5x because of the larger model. If you do not break this down per layer, you will spend hours debugging a slow request without realizing the judge is the bottleneck. I emit a span per layer using OpenTelemetry, then keep p50, p95, and p99 dashboards per route.
The third category is human-review sampling. Even with all the automated layers, I sample roughly 1 in 200 production answers for human evaluation. The cost is small, and the value is enormous: this is the only way to detect "the judge has gone soft" or "the grounded answer is technically correct but unhelpful in tone." The sampled human verdicts also feed directly into a regression set, which I rerun against new model versions and prompt edits before promoting them.
If you only adopt one observability practice from these three, choose human-review sampling. Automated metrics tell you that something changed; humans tell you what to do about it.
Cost realism: what these layers actually buy you
A common reaction to this architecture is "this looks expensive." It is. Across all seven layers, my high-stakes traffic costs roughly 4–6x what the same request would cost as a single Claude call. So the question is not "can I afford the layers?" but "what does each layer buy me, and is the price right?"
Layer 1 (grounding) is essentially free. It is just prompt design, and it pays for itself within a few requests by reducing fabrication.
Layer 2 (escape hatch) adds zero API cost and one tool-use turn of latency. The improvement in user trust — because Claude now declines instead of inventing — is the highest-ROI move on the entire list.
Layer 3 (Citations API) adds a small token overhead and is essentially free at scale. Use it on any request where the source documents are non-trivial.
Layer 6 (schemas) costs almost nothing in the steady state. The retry path doubles cost only when validation fails, which should be rare if your schema is well-designed.
Layers 4 (self-consistency) and 5 (judge) are where real money goes. Self-consistency multiplies generation cost by the number of samples, and the judge adds its own full inference. This is exactly why you must apply them only to the slice of traffic where the cost of a wrong answer exceeds the cost of the verification calls.
In practice I price requests against an "incident-equivalent" — the average cost of a customer apology, refund, or human-handled escalation. If a route's hallucination cost would be $50 per incident at a 1% error rate, $0.40 of extra inference per request is a bargain.
How to choose the combination
Applying all seven layers to every request explodes cost. In my production system I tier them by request criticality.
For low-stakes traffic (FAQs, casual chat), Layers 1 (grounding) and 2 (escape hatch) are enough. For mid-stakes (product specs, procedural guidance), I add Layer 3 (Citations) and Layer 6 (schemas). For high-stakes domains (medical, legal, financial), I stack Layer 4 (self-consistency) and Layer 5 (judge) on top of everything else.
For broader patterns on production design and the monitoring dashboards you will want to set up alongside these defenses, see Multi-agent production patterns for the Claude API — it pairs naturally with this defensive architecture.
Where this architecture still falls short
Honest reporting requires acknowledging that even the full stack does not solve every problem. There are categories of error where this architecture helps less than you might hope, and recognizing them up front prevents misplaced confidence.
The first weakness is over-conservatism. Stack enough layers and Claude starts declining questions it could have answered correctly. I have measured this drift directly: in one deployment, declination rates climbed from 4% to 11% after I added the judge, and several of those declinations were against questions a human would have answered without hesitation. The fix is to tune judge prompts to reward correctness, not just caution, and to monitor false-decline rates as carefully as false-fabrication rates.
The second weakness is shared error modes. If your retrieval pipeline returns the wrong documents, all the grounding in the world cannot help — Claude will faithfully ground on irrelevant text. Citations and judges become useless when the inputs themselves are off. This is why I treat retrieval quality as a first-class part of the defense rather than a separate concern. A weekly retrieval-quality eval, even on a small sample, catches drift before it shows up as user-visible errors.
The third weakness is novel adversarial inputs. Prompt injection in user-supplied text can route around grounding, especially when the documents themselves contain attacker-controlled strings. Layer 6 schemas help reduce the surface area, and tool-use schemas with strict enums keep responses on rails, but you should still treat any system that ingests user content as exposed to injection regardless of these defenses. A separate review of injection-resistant prompt patterns is overdue in any production system.
Finally, this architecture does not address bias or harm beyond factuality. A perfectly grounded answer can still be culturally inappropriate, legally risky, or strategically misaligned with your brand voice. Those concerns require their own review layers — moderation, persona enforcement, content policy checks — that operate orthogonally to the hallucination defenses described here.
One step you can take tomorrow
If you only implement one layer from this article, start with Layer 2 (the escape-hatch tool). Adding the tool definitions to an existing system takes an afternoon, and you will feel the change in answer quality the same day. Just don't forget tool_choice: { type: "any" }.
From there, build the habit of measuring hallucination rates weekly. Add the next layer the moment your number crosses a threshold you cannot tolerate. That cadence keeps you from over-engineering and from under-protecting.
There is no perfect defense. You absorb the residual risk through non-technical layers too — clear caveats to the user, escalation paths to humans, and well-defined service boundaries. Claude API reaches production quality only when the engineering and the operational layers are designed together.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.