ZeroLeaksDocs
Shield SDK

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;
}
CodeMeaning
SHIELD_UNAUTHORIZEDThe API key was rejected (401).
SHIELD_FORBIDDENCheck the account's model access and required research acknowledgement (403).
SHIELD_INPUT_TOO_LARGEThe request exceeds the input limit (413).
SHIELD_RATE_LIMITEDThe service cannot admit the request (429).
SHIELD_HTTP_ERRORAnother 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, LeakDetectedError is thrown after the last chunk, so redacted text has already been yielded. It carries the confidence and fragmentCount of the first leak. OutputBlockedError is thrown from your for await loop 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 your for await loop at the done event of the text that has the leak or finding, after the events before it. See Responses API streaming.
  • With the AI SDK's shieldLanguageModelMiddleware, generateText rejects with them. streamText never throws them: the stream ends with an error part that carries the error, so it reaches onError and fullStream, result.text resolves to the text sent before it, and finishReason is "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(), or chat.stream() rejects with them before any output reaches your code. A Gemini chat's sendMessage() or sendMessageStream() 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 your for await loop, or from read() on the Mistral stream. OutputBlockedError comes before any of the text with the finding, after the chunks or events before it. LeakDetectedError for a prompt leak or canary in the text comes after the last chunk or event, so redacted text has already been yielded, and carries the confidence and fragmentCount of 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 with InjectionDetectedError, and so does generateContentStream() in "buffer" mode. In "chunked" and "passthrough" mode it is thrown from your for await loop, after chunks from the turn that called the tool. See Automatic function calling.
  • With LangChain's shieldChatModel, all three errors reject invoke(), batch(), and generate(); with batch()'s returnExceptions option, the error takes the place of that input's result. In stream(), "buffer" and "chunked" mode both read the whole response first, so the error comes before any chunk and the stream() 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 through handleLLMError. See shieldChatModel.
  • With LangChain's ShieldCallbackHandler, LangChain logs each error with console.error and passes it on to the run. InjectionDetectedError from a chat model's messages comes before the model is called, so invoke() and stream() 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. LeakDetectedError and OutputBlockedError come from handleLLMEnd, after the model responded: invoke() rejects without returning the output, but with stream() they are thrown from your for await loop after every chunk has been yielded, unredacted. See ShieldCallbackHandler.
  • With shieldMcpClient, callTool(), readResource(), and getPrompt() reject with InjectionDetectedError after the server answered, in place of returning the result; a blocked tool call has already run on the server. listTools() rejects with it only with onFlaggedTools: "throw", after the server listed its tools. callTool() rejects with ToolPolicyError before the server is called, when a policy refuses 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 with InputGuardrailTripwireTriggered before the model is called, and with OutputGuardrailTripwireTriggered after the agent's final output; with stream: true, both are thrown from your for await loop and result.completed, and the output one only after the text was streamed. The tool guardrails throw nothing with the default behavior: "rejectContent", since the model gets a rejection message instead; with "throwException", run() rejects with the SDK's ToolCallError, whose error is ToolInputGuardrailTripwireTriggered or ToolOutputGuardrailTripwireTriggered. 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;
}

On this page