Errors
Hosted API failures, blocked injections, output findings, and tool policy errors.
Shield errors extend ShieldError, which provides a code property for programmatic handling.
ShieldError
Base class for all Shield errors. Extends Error and adds a code string.
class ShieldError extends Error {
readonly code: string;
constructor(message: string, code: string);
}ShieldAPIError
Hosted detect() and createHostedDetector() reject with ShieldAPIError when a check cannot complete. It extends ShieldError and includes an optional HTTP status. A failed check is not a clean verdict.
import { detect, ShieldAPIError } from "@zeroleaks/shield";
try {
const result = await detect(untrustedText, { requireFullCoverage: true });
if (result.flagged) {
throw new Error("Input requires review");
}
} catch (error) {
if (error instanceof ShieldAPIError) {
// Keep the input blocked while the check is unavailable.
console.error(error.code, error.status);
}
throw error;
}| Code | Meaning |
|---|---|
SHIELD_UNAUTHORIZED | The API key was rejected (401). |
SHIELD_FORBIDDEN | Check the account's model access and required research acknowledgement (403). |
SHIELD_INPUT_TOO_LARGE | The request exceeds the input limit (413). |
SHIELD_RATE_LIMITED | The service cannot admit the request (429). |
SHIELD_HTTP_ERROR | Another HTTP error prevented the check. |
Configuration errors, timeouts, cancellation, invalid responses, and incomplete coverage also reject. See the migration guide for coverage and timeout options.
InjectionDetectedError
Thrown by the provider wrappers when prompt injection is detected in a user message or a tool result and onDetection is "block". Includes risk and categories for logging or alerting, and source for where the injection was found.
type InjectionSource = "user" | "tool";
class InjectionDetectedError extends ShieldError {
readonly risk: string; // "low" | "medium" | "high" | "critical"
readonly categories: string[]; // e.g. ["instruction_override", "role_hijack"]
readonly source?: InjectionSource;
constructor(risk: string, categories: string[], source?: InjectionSource);
// code: "INJECTION_DETECTED"
}The wrappers always set source: "user" for a user message, "tool" for a tool result or document (see Tool results). An error you construct without it has source undefined. With source: "tool", the message reads "Prompt injection detected in a tool result (…)"; otherwise it reads "Prompt injection detected (…)".
LeakDetectedError
Thrown by the provider wrappers when throwOnLeak is true and the output contains leaked system prompt fragments or the canary. In AI SDK streams it arrives in an error part instead, as described below. sanitize() itself never throws. Includes confidence and fragmentCount.
class LeakDetectedError extends ShieldError {
readonly confidence: number; // 0-1
readonly fragmentCount: number;
constructor(confidence: number, fragmentCount: number);
// code: "LEAK_DETECTED"
}For a prompt leak, confidence and fragmentCount come from sanitize(). For an Anthropic tool_use input or an AI SDK argument object, confidence is the highest of its strings and fragmentCount counts the fragments across them. For a canary in text with no prompt leak, confidence is the canary finding's and fragmentCount is the number of places it was found.
OutputBlockedError
Thrown by the provider wrappers when blockOnOutputFindings is true and output scanning reports a high or critical finding, such as a credential or an exfiltration link. The wrapper blocks the output instead of redacting it. In AI SDK streams the error arrives in an error part.
type BlockedFinding = Pick<OutputFinding, "type" | "kind" | "severity">;
class OutputBlockedError extends ShieldError {
readonly findings: BlockedFinding[]; // e.g. [{ type: "secret", kind: "github_pat", severity: "critical" }]
constructor(findings: readonly BlockedFinding[]);
// code: "OUTPUT_BLOCKED"
}findings lists every finding in the blocked scan, including lower-severity findings, with only their type, kind, and severity. It contains neither the matched text nor its preview, so the error is safe to log. The message lists up to five of them, such as "Model output blocked: secret:github_pat (critical)". If the same scan found a prompt leak or a canary and throwOnLeak is true, LeakDetectedError is thrown instead.
ToolPolicyError
Thrown by the MCP client wrapper when a tool policy refuses a tool call, before the server is called. It carries the policy's decision.
class ToolPolicyError extends ShieldError {
readonly tool: string; // the tool's name, as the call gave it
readonly reason: ToolPolicyReason; // e.g. "undeclared_tool", "invalid_arguments", "untrusted_to_sink"
readonly violations: SchemaViolation[]; // with "invalid_arguments": [{ path, keyword, message }]; else []
constructor(decision: Pick<ToolPolicyDecision, "tool" | "reason" | "message" | "violations">);
// code: "TOOL_POLICY_VIOLATION"
}The message is the decision's message, such as Tool "send_email" can send data out, and this session has seen untrusted content from read_inbox. Neither the message nor violations holds argument values, so the error is safe to log. To throw it from your own tool loop, pass it a refused decision: if (!decision.allowed) throw new ToolPolicyError(decision).
When the errors are thrown
Provider wrappers throw InjectionDetectedError before sending the request, so a blocked request costs no tokens. Tool results produced during a call are an exception: they are checked after the tool runs. These include results from Google Gen AI's automatic function calling, results seen by ShieldCallbackHandler, and responses from an MCP client's tools, resources, and prompts, as described below. LeakDetectedError and OutputBlockedError occur after the response arrives. The wrapper and streaming mode determine how much output has already reached your code. In "passthrough" streaming mode the stream is not scanned, so neither error is thrown for it.
- In the OpenAI, Anthropic, and Groq wrappers, for non-streaming responses and the default
"buffer"streaming mode, they are thrown before any output reaches your code. - In the OpenAI, Anthropic, and Groq wrappers'
"chunked"streaming mode,LeakDetectedErroris thrown after the last chunk, so redacted text has already been yielded. It carries theconfidenceandfragmentCountof the first leak.OutputBlockedErroris thrown from yourfor awaitloop as soon as the chunk or event with the finding arrives, before its text is yielded. The OpenAI and Groq wrappers yield chunks one behind the stream, so the chunk before it is not yielded either. - With the OpenAI Responses API and
stream: true, both are thrown from yourfor awaitloop at thedoneevent of the text that has the leak or finding, after the events before it. See Responses API streaming. - With the AI SDK's
shieldLanguageModelMiddleware,generateTextrejects with them.streamTextnever throws them: the stream ends with anerrorpart that carries the error, so it reachesonErrorandfullStream,result.textresolves to the text sent before it, andfinishReasonis"error". Nothing from the text block or chunk that leaked is emitted. See Vercel AI SDK. shieldMiddleware().sanitizeOutput()throws them directly.- In the Google Gen AI and Mistral wrappers, for non-streaming responses and the default
"buffer"streaming mode,generateContent(),generateContentStream(),chat.complete(), orchat.stream()rejects with them before any output reaches your code. A Gemini chat'ssendMessage()orsendMessageStream()rejects the same way, and the reply is not added to its history. - In the Google Gen AI and Mistral wrappers'
"chunked"streaming mode, both are thrown from yourfor awaitloop, or fromread()on the Mistral stream.OutputBlockedErrorcomes before any of the text with the finding, after the chunks or events before it.LeakDetectedErrorfor a prompt leak or canary in the text comes after the last chunk or event, so redacted text has already been yielded, and carries theconfidenceandfragmentCountof the first leak. A finding in function call or tool call arguments throws either error as soon as its chunk or event arrives. Chunks and events are yielded one behind the stream, so the one before the error is not yielded either. - With Google Gen AI's automatic function calling, an injection in what a callable tool returns is found after the model asked for the tool and the tool ran, but before the result is sent back to the model.
generateContent()rejects withInjectionDetectedError, and so doesgenerateContentStream()in"buffer"mode. In"chunked"and"passthrough"mode it is thrown from yourfor awaitloop, after chunks from the turn that called the tool. See Automatic function calling. - With LangChain's
shieldChatModel, all three errors rejectinvoke(),batch(), andgenerate(); withbatch()'sreturnExceptionsoption, the error takes the place of that input's result. Instream(),"buffer"and"chunked"mode both read the whole response first, so the error comes before any chunk and thestream()call itself rejects.streamEvents()throws it from your loop before any of the response's text or tool calls. Tokens held back from a blocked response are never sent to callback handlers; LangChain reports the error to them throughhandleLLMError. See shieldChatModel. - With LangChain's
ShieldCallbackHandler, LangChain logs each error withconsole.errorand passes it on to the run.InjectionDetectedErrorfrom a chat model's messages comes before the model is called, soinvoke()andstream()reject. From a tool's output or retrieved documents, it comes after the tool or retriever ran, and that tool's or retriever's call rejects in place of returning the result; a framework that catches tool errors may pass the error message to the model instead.LeakDetectedErrorandOutputBlockedErrorcome fromhandleLLMEnd, after the model responded:invoke()rejects without returning the output, but withstream()they are thrown from yourfor awaitloop after every chunk has been yielded, unredacted. See ShieldCallbackHandler. - With
shieldMcpClient,callTool(),readResource(), andgetPrompt()reject withInjectionDetectedErrorafter the server answered, in place of returning the result; a blocked tool call has already run on the server.listTools()rejects with it only withonFlaggedTools: "throw", after the server listed its tools.callTool()rejects withToolPolicyErrorbefore the server is called, when apolicyrefuses the call. See MCP client. - The OpenAI Agents SDK guardrails throw none of these errors: a guardrail trips, and the SDK throws its own.
shieldToolPolicyGuardrail()behaves like the other tool guardrails.run()rejects withInputGuardrailTripwireTriggeredbefore the model is called, and withOutputGuardrailTripwireTriggeredafter the agent's final output; withstream: true, both are thrown from yourfor awaitloop andresult.completed, and the output one only after the text was streamed. The tool guardrails throw nothing with the defaultbehavior: "rejectContent", since the model gets a rejection message instead; with"throwException",run()rejects with the SDK'sToolCallError, whoseerrorisToolInputGuardrailTripwireTriggeredorToolOutputGuardrailTripwireTriggered. Catch these from@openai/agents. See OpenAI Agents SDK.
Creating a wrapper throws a plain TypeError or RangeError, not a ShieldError, when a canary, harden.canary, or output.canary option can't be matched in output. See Canary.
Example try/catch
import { shieldOpenAI } from "@zeroleaks/shield/openai";
import {
InjectionDetectedError,
LeakDetectedError,
OutputBlockedError,
} from "@zeroleaks/shield";
const client = shieldOpenAI(openai, {
systemPrompt: "...",
onDetection: "block",
throwOnLeak: true,
blockOnOutputFindings: true,
});
try {
const response = await client.chat.completions.create({
model: "gpt-5.5",
messages: [{ role: "user", content: userInput }],
});
return response;
} catch (err) {
if (err instanceof InjectionDetectedError) {
console.log(err.source, err.risk, err.categories);
return { error: "Invalid request", status: 400 };
}
if (err instanceof LeakDetectedError) {
console.log(err.confidence, err.fragmentCount);
return { error: "Output sanitization failed", status: 500 };
}
if (err instanceof OutputBlockedError) {
console.log(err.findings);
return { error: "Output blocked", status: 500 };
}
throw err;
}