ZeroLeaksDocs
Shield SDKProvider Wrappers

OpenAI Provider

Wrap your OpenAI client with Shield for prompt hardening, injection detection in user messages and tool results, and output guarding, on Chat Completions and the Responses API.

The shieldOpenAI wrapper adds Shield to your existing OpenAI client. It intercepts every chat.completions.create and responses.create call to harden system prompts, detect injections in user messages and tool results, and redact prompt leaks, credentials, and exfiltration links from responses.

Usage

import OpenAI from "openai";
import { shieldOpenAI } from "@zeroleaks/shield/openai";

const client = shieldOpenAI(new OpenAI(), {
  systemPrompt: "You are a financial advisor...",
  onDetection: "block",
});

const response = await client.chat.completions.create({
  model: "gpt-5.5",
  messages: [
    { role: "system", content: "You are a financial advisor..." },
    { role: "user", content: userInput },
  ],
});

How it works

On every call to chat.completions.create, Shield:

  1. Copies the messages array so your objects are not modified.
  2. Hardens every message with role: "system" or role: "developer" (unless harden: false), adding the canary if you set one. Array content is replaced with a single text part.
  3. Runs detection on every user message (unless detect: false) and on every tool and function message (unless scanToolResults: false), including earlier turns. Text parts are joined; image parts are ignored.
  4. Calls the original OpenAI method.
  5. For every choice, checks message.content for prompt leaks (unless sanitize: false or there is no system prompt) and runs the output guard on it (unless output: false). It does the same for each tool call's function.arguments or custom tool input, and the legacy function_call.arguments. When text is redacted, the choice's logprobs is set to null. Streaming responses are handled as described in Streaming.

For prompt leak checks, Shield uses systemPrompt if provided. Otherwise, it joins the text of every system and developer message before hardening.

Responses API

The wrapper also guards client.responses.create when your client provides it (openai 4.87 and later):

const response = await client.responses.create({
  model: "gpt-5.5",
  instructions: "You are a financial advisor...",
  input: userInput,
});

console.log(response.output_text); // guarded

On every call to responses.create, Shield:

  1. Copies the input items so your objects are not modified.
  2. Hardens instructions when it is a string, and every input message with role: "system" or role: "developer" (unless harden: false). String content stays a string; an array of parts becomes a single input_text part.
  3. Runs detection on input when it is a string, and on every input message with role: "user", joining its input_text parts and ignoring images and files. Tool outputs in input are scanned as listed under Tool results.
  4. Calls the original OpenAI method.
  5. Guards the text of every output_text part in the response's message items, the arguments of function_call items, and the input of custom_tool_call items, and rebuilds response.output_text from the guarded parts. A redacted part gets an empty logprobs list.

For prompt leak checks, Shield uses systemPrompt if provided. Otherwise, it joins instructions and the text of every system and developer message in input before hardening. Detection sees only what you send: with previous_response_id or conversation, the earlier turns OpenAI stores are not rescanned.

Streaming

With stream: true, create() resolves immediately to a plain async iterable of the API's events. "buffer" and "chunked" behave the same way here:

  • Each text's response.output_text.delta events are held until its response.output_text.done event, each function call's response.function_call_arguments.delta events until its response.function_call_arguments.done, and each custom tool call's response.custom_tool_call_input.delta events until its response.custom_tool_call_input.done. The full text is guarded then.
  • If nothing was redacted, the buffered deltas are emitted unchanged. Otherwise, the guarded text is emitted as new delta events of 64 characters each, shaped like the originals: the same item_id, output_index, and content_index, the buffered deltas' sequence_numbers in order (extra pieces repeat the last one), and empty logprobs.
  • The done event carries the guarded text, and so do response.content_part.done, response.output_item.done, and the final response.completed, response.incomplete, or response.failed event.
  • Every other event passes through as it arrives, in order.
  • A leak with throwOnLeak, or a finding with blockOnOutputFindings, throws from your for await loop at the done event, after all preceding events.

"passthrough" returns the SDK's stream unchanged. Annotation offsets, such as a url_citation's start_index and end_index, are not adjusted after a redaction. response.refusal.delta and response.mcp_call_arguments.delta events are not held or guarded.

Options

OptionTypeDefaultDescription
systemPromptstringsystem and developer messagesThe system prompt to check output against. Defaults to the text of every system and developer message (Responses API: instructions too), before hardening.
hardenHardenOptions | false{}Hardening options, or false to disable
detectLocalDetectOptions | false{}Detection options for user messages, or false to disable
scanToolResultsboolean | LocalDetectOptionstrueDetection on tool results. true uses the detect options, an object replaces them, and false disables it. detect: false does not disable it.
sanitizeSanitizeOptions | false{}Prompt leak sanitization options, or false to disable
outputScanOutputOptions | false{}Output scanning options, or false to disable. By default credentials and exfiltration links are redacted.
onOutputFindings(findings: OutputFinding[]) => voidnoneCalled with the findings each time output scanning finds something
blockOnOutputFindingsbooleanfalseWhen true, throw OutputBlockedError instead of redacting if a finding is high or critical severity
canarystring | booleannonePlant a canary in the hardened system prompt and treat its appearance in output as a leak. true creates one. See Canary.
streamingSanitize"buffer" | "chunked" | "passthrough""buffer"How streamed output is guarded. See Streaming.
streamingChunkSizenumber8192Characters per chunk in "chunked" mode (Chat Completions only)
throwOnLeakbooleanfalseWhen true, throw LeakDetectedError instead of redacting a prompt leak or a canary
onDetection"block" | "warn""block""block" throws InjectionDetectedError; "warn" calls onInjectionDetected and sends the request anyway
onInjectionDetected(result: LocalDetectResult, source: "user" | "tool") => voidnoneCalled when injection is detected, in both modes, with where it was found
onLeakDetected(result: SanitizeResult) => voidnoneCalled when a prompt leak or a canary is found in the output

Blocking vs warning

By default, onDetection is "block": if an injection is detected in a user message or a tool result, Shield throws InjectionDetectedError with the risk level, the matched categories, and source, and the request is never sent. To log instead of blocking:

const client = shieldOpenAI(new OpenAI(), {
  onDetection: "warn",
  onInjectionDetected: (result, source) => {
    console.warn(`Injection in ${source} input: ${result.risk}`, result.matches);
  },
});

What is not covered

Only chat.completions.create and responses.create are guarded. Every other method on the wrapped client still works, such as chat.completions.retrieve(), responses.stream(), responses.parse(), responses.retrieve(), and client.beta, but its requests and responses are not checked or guarded. To stream a guarded response, call create() with stream: true. create() returns a plain Promise, and a guarded stream is a plain async iterable, so the SDK's withResponse() and toReadableStream() are unavailable at runtime, although TypeScript accepts them. See Shared behavior and What is not scanned.

On this page