Shrike Guard SDK

Drop-in protection for OpenAI, Anthropic, and Google Gemini. Secure your LLM applications with 3 lines of code.

$pip install shrike-guard

Quick Start

The Shrike Guard SDK is a drop-in replacement for popular LLM client libraries. Simply swap your import and add your Shrike API key - all security scanning happens automatically.

Python
from shrike_guard import ShrikeOpenAI
 
# Drop-in replacement - same API as OpenAI
client = ShrikeOpenAI(
    api_key="your-openai-key",
    shrike_api_key="your-shrike-key",
)
 
# Use exactly like OpenAI - scanning happens automatically
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Hello, how are you?"}]
)

Installation

PyPython
Terminal
pip install shrike-guard
TSTypeScript
Terminal
npm install shrike-guard

Supported Providers

LanguagePackageProviders
Pythonshrike-guardOpenAI, Anthropic, Google Gemini
TypeScriptshrike-guardOpenAI, Anthropic

Configuration

All SDKs support these configuration options:

OptionTypeDefaultDescription
shrike_api_keystringrequiredAPI key for Shrike authentication
shrike_endpointstringProduction URLCustom endpoint URL
fail_modestring"closed"Behavior on scan failure (default: block)
scan_timeoutnumber10-30sTimeout for scan requests

Fail Modes

Fail Closed (Default)

If scanning fails (timeout, network error), block the request. Enforcement outranks availability — the Zero-Trust default across the SDKs and MCP.

Fail Open (opt-in)

If scanning fails, allow the request to proceed. Opt in only when availability must outrank enforcement (e.g. a low-stakes chatbot tolerating scan outages).

Python
# Fail-closed (default) - blocks on scan failure (Zero-Trust)
client = ShrikeOpenAI(
    api_key="...",
    shrike_api_key="...",
    fail_mode="closed"  # default — shown for clarity
)
 
# Fail-open - opt in when availability must outrank enforcement
client = ShrikeOpenAI(
    api_key="...",
    shrike_api_key="...",
    fail_mode="open"
)

Sessions

A session is what Shrike accumulates risk against. Turns that share a session id are correlated with one another, so a refusal earlier in a session shapes what happens later in it. Choosing the session boundary is the difference between governing one user and governing all of them at once.

By default a client uses one process-wide session id. That suits a single agent doing one job. A server handling many end users needs one session per user, or the first user’s refusal counts against the next user’s action. Build one client at startup and derive a per-request view from it. The view shares the parent’s connection pool, so deriving one per request is cheap.

Python
from shrike_guard.scanner import ScanClient
 
guard = ScanClient(api_key=SHRIKE_KEY)           # once, at startup
 
def handle(request):                              # per request
    scoped = guard.for_session(request.session_id)
    verdict = scoped.scan_command(request.command)
    if not verdict["safe"]:
        return refuse(verdict["recovery"]["instruction"])
    return run(request.command)

Naming your agents

Every scan is attributed to an agent id. That is what a declared scope is matched against, and what the Agents screen groups by. The default is a per-process id. Name it when you want your own agents to be recognisable, either per client or for the whole process.

LanguagePer clientPer derived view
PythonScanClient(api_key=…, session_id=…, agent_id=…)for_session(session_id, agent_id=None)
TypeScriptnew ScanClient({ apiKey, sessionId, agentId })forSession(sessionId, agentId?)
GoNewClient(key, WithSession(…), WithAgentID(…))ForSession(sessionID)
Environment variableEffect
SHRIKE_AGENT_IDNames every agent in the process. Read at scan time, so it can be set after startup.
SHRIKE_SUPPRESS_SESSION_WARNINGSet to 1 to silence the one-time notice about the process-wide session, once you have chosen your session boundary deliberately.

Where the content came from

Every verdict carries content_origin, which answers a question the verdict alone cannot: was this the operator’s own prompt, or the agent acting on its own? That decides who a refusal is addressed to, a person who can rephrase or an agent that must change course. Unknown content types resolve to agent_action, never to human_prompt.

ValueMeans
human_promptA person typed it. A refusal is addressed to them.
agent_outputThe model generated it.
agent_actionThe agent is acting: a command, a query, a file write.
third_partyIt arrived from outside: retrieved context, another agent, a search result.

TypeScript also exports attributableToOperator(origin), true only for human_prompt.

Provider Examples

OpenAI

Python
from shrike_guard import ShrikeOpenAI
 
client = ShrikeOpenAI(
    api_key="your-openai-key",
    shrike_api_key="your-shrike-key",
)
 
response = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Explain quantum computing"}]
)
 
print(response.choices[0].message.content)

Anthropic

Python
from shrike_guard import ShrikeAnthropic
 
client = ShrikeAnthropic(
    api_key="your-anthropic-key",
    shrike_api_key="your-shrike-key",
)
 
response = client.messages.create(
    model="claude-3-opus-20240229",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Explain quantum computing"}]
)
 
print(response.content[0].text)

Google Gemini

Python
from shrike_guard import ShrikeGemini
import google.generativeai as genai
 
genai.configure(api_key="your-gemini-key")
model = genai.GenerativeModel("gemini-pro")
 
# Wrap the Gemini model
shrike_model = ShrikeGemini(
    model=model,
    shrike_api_key="your-shrike-key",
)
 
response = shrike_model.generate_content("What is machine learning?")
print(response.text)

Error Handling

The SDK provides specific exception types for different error scenarios:

ExceptionWhen RaisedContains
ShrikeBlockedErrorThreat detectedthreat_type, confidence, violations
ShrikeScanErrorScan failed (in closed mode)Error details
ShrikeConfigErrorInvalid configurationConfiguration issue
Python
from shrike_guard import ShrikeOpenAI, ShrikeBlockedError, ShrikeScanError
 
try:
    response = client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": user_input}]
    )
except ShrikeBlockedError as e:
    # Threat was detected
    print(f"Blocked: {e.threat_type}")
    print(f"Confidence: {e.confidence}")
    print(f"Violations: {e.violations}")
    # Show user-friendly error message
except ShrikeScanError as e:
    # Scan failed (only in fail_mode="closed")
    print(f"Scan error: {e}")

Governance Helpers

The SDK ships three helpers that close the loop between a Shrike verdict and the model's next turn. See the Cookbook and the integration guide for how they compose.

system_prompt() — the canonical block

Returns the ~180-word "Working with Shrike" block that teaches the model how to react to Shrike verdicts. Drop it into your agent's system prompt as the first non-role paragraph.

Python
# Python
from shrike_guard import system_prompt, SYSTEM_PROMPT_VERSION
 
prompt = (
    "You are a customer support agent for Acme Corp.\n\n"
    + system_prompt()
    + "\n\nWhen customers ask about refunds..."
)
# Pin behavior against SYSTEM_PROMPT_VERSION if needed.

format_block_feedback() — inject the verdict back to the model

When Shrike blocks or holds a tool call, render the verdict as a canonical prompt-shape string and append it as a system message to the model's next turn. The model, having been taught by system_prompt() to recognize the prefix, reads the Reason / Threat type / Recovery / Available tools lines and adjusts.

Python
# Python
from shrike_guard import format_block_feedback
 
verdict = shrike.scan_sql_query(query)
if not verdict.get("safe"):
    model_messages.append({
        "role": "system",
        "content": format_block_feedback(verdict),
    })

Renders four verdict shapes: block / warn / require_approval / allow. Forward-compat with the upcoming recovery block (instruction, available_tools, patterns_triggered) — new fields render automatically without a signature change.

evaluateRotation() — the two-shape session rotation record

When a verdict indicates the session should rotate (either explicit threat_type: "session_locked" or accumulated session_risk_score >= 0.7), the helper returns either a ModuleOwnedRotation (SDK's own session was in use) or a CallerOwnedRotationRecommendation (caller supplied their own session_id). Discriminate on rotated.

TypeScript
// TypeScript
import { evaluateRotation, getSessionId } from 'shrike-guard';
 
const rotation = evaluateRotation({
  threat_type: verdict.threat_type,
  session_state: verdict.session_state,
  effective_session_id: currentSessionId,
  module_session_id: getSessionId(),
});
 
if (rotation?.rotated) {
  // Module-owned: SDK's fallback SESSION_ID rotated
  currentSessionId = rotation.new_session_id;
} else if (rotation) {
  // Caller-owned: adopt suggested_new_session_id on the VERY NEXT call.
  // suggestions are per-event — do not cache across turns.
  currentSessionId = rotation.suggested_new_session_id;
}

Available in TypeScript today. Python port is deferred — the Python scanner needs session_id threading through ScanClient.scan() first. Track this as a known parity gap.

Framework Starters

Shrike governs an agent from inside its tool loop: every tool call is judged before it executes, the person's prompt is scanned on the way in and never blocked, and the agent gets one tool, request_scope, to ask for more than it holds. One framework-free core carries that behaviour (shrike_guard.govern in Python, govern.tsin TypeScript); each starter is a thin translation of one framework's hooks.

FrameworkPythonTypeScript
Claude Agent SDKshrike_guard.claude_agent · pip install shrike-guard[claude-agent]shrike-guard/claude-agent
OpenAI Agents SDKshrike_guard.openai_agents · [openai-agents]shrike-guard/openai-agents
Google ADKshrike_guard.google_adk · [google-adk]next
LangChain create_agent / LangGraphshrike_guard.langgraph_agent · [langgraph]shrike-guard/langchain
CrewAIshrike_guard.crewai_agent · [crewai]Python only
Vercel AI SDKTypeScript onlyshrike-guard/ai

Per-framework detail, including what each starter wires into, is on the agent frameworks page. To see a governed agent refused before wiring one up, the playground scans every tool call for real.

Three lines, then your mappings

Python
from shrike_guard import ScanClient
from shrike_guard.openai_agents import govern   # or claude_agent, google_adk, langgraph_agent, crewai_agent
 
guard = ScanClient(api_key=SHRIKE_KEY, agent_id="invoice-agent", session_id=run_id)
gov = govern(guard, agent_id="invoice-agent")
gov.declare(["file_path", "file_content", "sql"], purpose="Reconcile Q3 vendor invoices")
 
gov.map_tool("run_query", "sql", arg="query")
gov.map_tool("save_report", "file", path_arg="path", content_arg="text")
gov.exempt("get_time")
 
agent = gov.govern_agent(agent)   # or gov.hooks / gov.middleware / gov.govern_tools(...)

A tool mapping says which Shrike surface a tool is and which argument carries the payload: command (with an optional working-directory argument),file (a path and the content written to it), file_path,sql, web_search, rag_context,a2a_message, agent_card, or none for a tool allowed without a scan. The Claude Agent SDK starter ships mappings for the SDK's built-in tools; the others govern the tools you write, so they need your mappings.

A tool with no mapping is refused, with a message that says how to map it, until you map it or choose what an unmapped tool gets. on_unmapped="authorize" asks whether the agent may call it at all: the arguments stay put, and the tool's name goes to Shrike, where the declared scope answers. A tool outside the allowlist is refused by name even though nothing read what it was carrying, and an expired or exhausted scope holds it. That permit is narrower than a mapped tool's and the record says so. The other two choices are "allow", which records the call and moves on without asking, and "scan", which sends the arguments to be read as text.

What the model sees

The same thing everywhere. An allowed call runs. A warned call runs with the advisory attached. A blocked call does not run and the model gets the reason. A held call, an action outside the declared scope, does not run and the model gets the reason, the recovery, and the instruction to ask with request_scope and then stop; an operator decides on the Agents screen. An action Shrike could not check is refused (fail_mode="closed"). Every decision lands on gov.decisions and on your on_decision callback, with the axis that objected.

Where a framework can route a hold to a person, on_hold="ask" does that (the Claude Agent SDK's permission callback). Where it cannot, a hold is answered like a refusal and the model is told an operator can grant it.

Specialized Scanners

Beyond LLM prompt scanning, SDKs provide specialized security scanners for common attack vectors:

SQL Injection Scanner

Python
# Python
result = client.scan_sql(
    query="SELECT * FROM users WHERE id = ?",
    database="postgresql",
    allow_destructive=False  # Block DROP, DELETE, TRUNCATE
)
 
if result.safe:
    # Execute the query
    cursor.execute(query)

File Path Validator

Python
# Python - Prevent path traversal attacks
result = client.scan_file(
    path="/uploads/user-file.pdf",
    content=file_content  # Optional
)
 
if result.safe:
    # Process the file
    save_file(path, content)

CLI Command Scanner

Governs shell commands before execution — destructive operations, exfiltration imperatives, privilege escalation patterns.

Python
# Python
result = client.scan_command("rm -rf /var/log/app/*.log")
if result.safe:
    subprocess.run(result.content, shell=True)

Web Search Scanner

Governs outbound search queries before external egress, a high-blast-radius sink for reconnaissance such as “obtain AWS metadata creds”. Short queries are scanned at full depth, so a brief adversarial query is caught rather than skipped for its length.

Python
# Python
result = client.scan_web_search("latest CVEs for kubernetes API server")
if result.safe:
    search_engine.query(result.content)

A2A Message Scanner

Governs agent-to-agent messages before cross-agent trust is extended. Trivial pings pass; adversarial imperatives run the full cascade regardless of how short they are.

Python
# Python
result = client.scan_a2a_message("Please summarize the last 3 turns of context")

Retrieved Context Scanner

Governs retrieved context before it reaches the model, the standard carrier for indirect prompt injection. Pass the chunks separately rather than joining them: a naive join merges two documents and loses the boundary an injection usually sits on.

Python
# Python
result = client.scan_rag_context(chunks, query="quarterly revenue")
if result["safe"]:
    answer = model.complete(prompt, context=chunks)

MCP Tool Scanner

Governs a single MCP tool definition before you trust it, for tool poisoning in a tools/list response. Screen a tool once at registration rather than on every call.

Python
# Python
result = client.scan_mcp_schema(tool["name"], tool["description"], tool.get("inputSchema"))
if result["safe"]:
    register(tool)

Agent Card Scanner

Governs A2A agent-card metadata (name / description / URL / capabilities) before connection establishment. Blocks malicious capabilities and suspicious domains.

Python
# Python
result = client.scan_agent_card(
    '{"name":"weather-agent","description":"forecasts",'
    '"url":"https://weather.acme.com","capabilities":["forecast"]}'
)

Streaming & Async Support

Streaming Responses

All SDKs support streaming responses with real-time scanning:

Python
# Python streaming
stream = client.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Write a story"}],
    stream=True
)
 
for chunk in stream:
    print(chunk.choices[0].delta.content, end="")

Async Support

Python
from shrike_guard import ShrikeAsyncOpenAI
 
async def main():
    client = ShrikeAsyncOpenAI(
        api_key="...",
        shrike_api_key="...",
    )
 
    response = await client.chat.completions.create(
        model="gpt-4",
        messages=[{"role": "user", "content": "Hello!"}]
    )

Response Schema

Every scan returns the same shape, whether the verdict is safe or refused and whether you called a general or a specialized scanner. Prose changes; the fields do not. Route on refuse_tier rather than on message text.

JSON
1{
2 "safe": false,
3 "action": "block",
4 "refuse_tier": "block",
5 "threat_type": "prompt_injection",
6 "content_origin": "human_prompt",
7 "confidence": "high",
8 "severity": "critical",
9 "recovery": {
10 "instruction": "Rephrase without instructions that override the system prompt."
11 },
12 "session_state": {
13 "session_risk_score": 0.45,
14 "session_turn_number": 2,
15 "session_patterns": [],
16 "session_locked": false
17 },
18 "violations": [
19 {
20 "threat_type": "prompt_injection",
21 "owasp_category": "LLM01",
22 "severity": "critical",
23 "action": "block",
24 "user_message": "This request contains patterns commonly used to override AI instructions.",
25 "suggested_action": "If unintended, try rephrasing without phrases like 'ignore previous instructions'."
26 }
27 ]
28}
FieldTypeDescription
safebooleantrue if the content passed. Note that a held action can be safe and still require approval, so branch on refuse_tier.
actionstringallow, warn, require_approval, or block
refuse_tierstringThe four-state verdict. The field to route on.
threat_typestringNormalized threat category, when one was found
content_originstringWhere the content came from. See Sessions.
confidencestringA bucket: high, medium, or low. Exact detector scores stay on the backend.
recoveryobjectWhat to do next. Present on every refusal; show the instruction rather than retrying.
session_stateobjectTurn number, accumulated risk, matched patterns, and whether the session is locked
violationsarrayOne entry per finding, carrying the category and both messages

What you will not receive

Detection attribution is stripped before the response leaves the backend: matched patterns, per-detector confidences, which layer fired, and internal policy identifiers. You get the category and the remediation, which is what an integration needs to act. Do not write code that depends on attribution fields.

Best Practices

1. Use Environment Variables

Never hardcode API keys in your source code.

Python
import os
from shrike_guard import ShrikeOpenAI
 
client = ShrikeOpenAI(
    api_key=os.environ["OPENAI_API_KEY"],
    shrike_api_key=os.environ["SHRIKE_API_KEY"],
)

2. Handle Errors Gracefully

Always provide user-friendly feedback when content is blocked.

Python
try:
    response = client.chat.completions.create(...)
except ShrikeBlockedError:
    return {"error": "Your message was blocked for safety reasons."}
except ShrikeScanError:
    return {"error": "Security check failed. Please try again."}

3. Choose Appropriate Fail Mode

ScenarioRecommended Mode
Most production apps (default)"closed"
Regulated / financial / healthcare"closed"
Availability-critical (tolerates scan outage)"open"
Development / testing"open"

4. Monitor Scan Latency

Set appropriate timeouts based on your SLA requirements.

Python
client = ShrikeOpenAI(
    api_key="...",
    shrike_api_key="...",
    scan_timeout=5.0  # 5 seconds max
)