LangGraph + LangChain + Chox: Shadow Verdicts in 10 Minutes
Add action-level governance to your LangGraph or LangChain agent. See what your agent would have been blocked from doing — without changing a single behavior.
Run one command in your project directory. Claude reads your existing agent code, finds the tools, and adds Chox integration automatically.
Run this in your project's root directory (requires Claude Code). Still need an account? Complete steps 1–3 below first.
claude "Integrate the Chox AI governance SDK (chox-ai-sdk) into this project's LangGraph / LangChain agent tools.
SDK reference:
pip install chox-ai-sdk python-dotenv
from dotenv import load_dotenv; load_dotenv()
from chox import ChoxGuard
guard = ChoxGuard(
base_url=os.getenv('CHOX_BASE_URL', 'https://chox.ai'),
token=os.environ['CHOX_CALLER_TOKEN'],
fail_open=True,
on_evaluate=on_verdict,
)
# Wrap a single function (preserves signature; async functions detected automatically):
wrapped = guard.wrap('integration.action', raw_fn)
# Wrap multiple tools at once:
wrapped_fns = guard.wrap_tools([
('stripe.charge', charge_fn),
('postgres.execute', query_fn),
])
# Decorator shorthand — use BEFORE @tool (wrap the raw fn, not the StructuredTool):
raw_search = lambda query: ... # or a def
wrapped_search = guard.wrap('db.search', raw_search)
search_tool = tool(wrapped_search) # @tool goes OUTSIDE
# EvaluateResponse fields: verdict, shadow_verdict, shadow_rule, shadow_reason,
# action_type, risk_score, reason, request_id, evaluated_at
def on_verdict(name, v):
if v.shadow_verdict == 'block':
print(f'[chox] {name} would have been blocked: {v.shadow_reason}')
Steps:
1. Find all agent tool functions — @tool decorators, StructuredTool.from_function(), or plain callables in tools=[].
2. Add import os if missing. Add ChoxGuard init at the top of each agent file using the env vars above.
3. For plain callables: replace fn with guard.wrap('dot.name', fn) everywhere passed to tools=[].
4. For @tool / StructuredTool: wrap the raw Python function FIRST, then pass it to @tool or StructuredTool.from_function(). Never wrap a StructuredTool object.
5. Use descriptive dot-notation names: 'stripe.create_charge', 'postgres.delete_rows', 'slack.post_message'.
6. Add chox-ai-sdk to requirements.txt or pyproject.toml if present.
7. Check for a .env file. If it exists, add CHOX_CALLER_TOKEN=your_caller_token_here to it (with a comment: # from Chox dashboard → AI Systems → Add AI System). If no .env exists, create one and add .env to .gitignore if not already there.
8. Do not change agent logic, LLM config, prompt templates, or tool descriptions."
Copy → open claude.ai → paste prompt → paste your agent code below it
Add the Chox AI governance SDK to the LangGraph / LangChain agent code I'll paste below.
SDK reference:
pip install chox-ai-sdk python-dotenv
from dotenv import load_dotenv; load_dotenv()
from chox import ChoxGuard
guard = ChoxGuard(
base_url=os.getenv("CHOX_BASE_URL", "https://chox.ai"),
token=os.environ["CHOX_CALLER_TOKEN"],
fail_open=True,
on_evaluate=on_verdict,
)
# Wrap a single function (async detected automatically):
wrapped = guard.wrap("integration.action", raw_fn)
# Wrap multiple tools at once:
guard.wrap_tools([("stripe.charge", fn1), ("postgres.query", fn2)])
# ⚠ CRITICAL for @tool / StructuredTool — wrap the RAW function BEFORE decoration:
wrapped_fn = guard.wrap("db.search", raw_search_fn) # wrap raw fn first
search_tool = tool(wrapped_fn) # then decorate
# Do NOT wrap the StructuredTool object itself.
# EvaluateResponse fields: verdict, shadow_verdict, shadow_rule, shadow_reason,
# action_type, risk_score, reason, request_id, evaluated_at
def on_verdict(name, v):
if v.shadow_verdict == "block":
print(f"[chox] {name} would have been blocked: {v.shadow_reason}")
Steps:
1. Find all tool functions (@tool, StructuredTool, plain callables in tools=[]).
2. Add import os if missing. Add ChoxGuard init at top of file using env vars above.
3. Wrap plain callables before tools=[]: tools=[guard.wrap("name", fn), ...]
4. Wrap @tool / StructuredTool: wrap the raw fn first, THEN pass to decorator (see example).
5. Use dot-notation names: "stripe.charge", "postgres.delete", "slack.send".
6. Add chox-ai-sdk to requirements.txt or pyproject.toml if present.
7. Do not change agent logic, LLM config, or tool descriptions.
8. Output the complete modified file(s).
--- My agent code: ---
What you'll build
A ReAct agent (LangGraph or LangChain — your choice) with two tools: a Stripe charge tool and a Postgres query tool. You'll wrap both with ChoxGuard so every tool call is evaluated by your Chox project before execution.
When the agent tries to charge $50,000 or run a DELETE FROM users query, you'll see shadow verdicts fire in real time — telling you what would have been blocked, without interrupting the agent.
Both framework paths share identical wrapping code. The only difference is the last two lines.
Prerequisites
Python 3.10+
For LangGraph: pip install chox-ai-sdk python-dotenv langgraph langchain-openai
For LangChain: pip install chox-ai-sdk python-dotenv langchain langchain-openai
OpenAI API key (or any LLM supported by LangChain)
Go to chox.ai/dashboard → New Project → give it a name and slug (e.g. "my-agent", slug "my-agent"). Copy the admin key shown once on creation.
Or via curl:
curl -X POST https://chox.ai/api/v1/projects \
-H "Content-Type: application/json" \
-d '{"name": "My LangGraph Agent", "slug": "my-agent"}'# Save the admin_key from the response
2Install the SDK
pip install chox-ai-sdk python-dotenv
3Create a caller token
Dashboard → your project → AI Systems → Add AI System → name it "langgraph-agent". Copy the token shown once.
Or via curl:
curl -X POST https://chox.ai/api/v1/callers \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "langgraph-agent"}'# Save the token from the response
4Set up your environment
Create a .env file in your project root with the caller token from Step 3:
If you don't already have one, add .env to your .gitignore to avoid committing credentials.
echo ".env" >> .gitignore
5Wrap your tools with ChoxGuard
import os
fromdotenvimport load_dotenv; load_dotenv()
fromchoximport ChoxGuard, EvaluateResponse
defon_verdict(tool_name: str, v: EvaluateResponse) -> None:
print(f"[chox] {tool_name} | verdict={v.verdict} shadow={v.shadow_verdict} risk={v.risk_score:.2f}")
if v.shadow_verdict == "block":
print(f" ⚠ Would have blocked: {v.shadow_reason} (rule: {v.shadow_rule})")
guard = ChoxGuard(
base_url="https://chox.ai",
token=os.environ["CHOX_CALLER_TOKEN"],
fail_open=True,
on_evaluate=on_verdict,
)
# Your real tool implementationsdef_charge_card(amount_cents: int, currency: str, description: str) -> dict:
# In production: stripe.Charge.create(...)return {"status": "succeeded", "amount": amount_cents, "currency": currency}
def_run_query(sql: str) -> dict:
# In production: execute against your databasereturn {"rows": [], "affected": 0}
# Wrap them - Chox evaluates before each call
charge_card = guard.wrap("stripe.create_charge", _charge_card)
run_query = guard.wrap("postgres.execute", _run_query)
The on_evaluate callback fires after every Chox evaluation. It receives the tool name and an EvaluateResponse with fields: verdict, shadow_verdict, shadow_rule, shadow_reason, risk_score, reason, and signals.
With fail_open=True (the default), if the Chox gateway is unreachable, your tools still execute - you never block production because of a network blip.
6Register with LangGraph or LangChain
Build your tool list the same way for both frameworks. Only the last two lines differ.
fromlangchain_core.toolsimport StructuredTool
fromlangchain_openaiimport ChatOpenAI
tools = [
StructuredTool.from_function(
func=charge_card,
name="charge_card",
description="Charge a customer's card. amount_cents is in cents (5000 = $50 USD).",
),
StructuredTool.from_function(
func=run_query,
name="run_sql",
description="Execute a SQL query against the database.",
),
]
llm = ChatOpenAI(model="gpt-4o-mini", temperature=0)
LangGraph
fromlanggraph.prebuiltimport create_react_agent
agent = create_react_agent(llm, tools)
# Run it in the next step with: agent.invoke({...})
LangChain
fromlangchainimport hub
fromlangchain.agentsimport AgentExecutor, create_openai_tools_agent
prompt = hub.pull("hwchase17/openai-tools-agent")
agent = create_openai_tools_agent(llm, tools, prompt)
agent_executor = AgentExecutor(agent=agent, tools=tools)
# Run it in the next step with: agent_executor.invoke({...})
7Set a financial threshold to trigger shadow blocks
By default, all calls log with shadow_verdict=allow. To see the governance layer actually fire, configure a financial threshold:
Set the Financial threshold to a value below $50,000 — e.g. $1,000. Any financial operation above this amount will receive a shadow_verdict=block.
Save. Shadow mode is still on — nothing gets blocked in production, only logged.
Now when the agent calls charge_card with $50,000, Chox will evaluate it as shadow_verdict=block and your on_verdict callback will fire the warning — while the actual charge still goes through.
8Run the agent
LangGraph:
# Triggers shadow verdicts for both tool calls
response = agent.invoke({
"messages": [
{"role": "user", "content":
"Charge the enterprise account $50,000 for the annual subscription, ""then clean up old test users: DELETE FROM users WHERE test=true"}
]
})
print(response["messages"][-1].content)
LangChain:
response = agent_executor.invoke({
"input": "Charge the enterprise account $50,000 for the annual subscription, ""then clean up old test users: DELETE FROM users WHERE test=true"
})
print(response["output"])
Expected console output (from the on_verdict callback):
[chox] stripe.create_charge | verdict=allow shadow=block risk=0.92
⚠ Would have blocked: Financial operation exceeds threshold (rule: financial_above_10k)
[chox] postgres.execute | verdict=allow shadow=block risk=0.82
⚠ Would have blocked: Destructive SQL operation detected (rule: destructive_sql)
Both tool calls executed normally — your agent finished without interruption. But Chox recorded exactly what would have been blocked when you flip enforcement on.
9See shadow verdicts in the dashboard
Go to chox.ai/dashboard → Logs. You'll see both tool calls logged with:
Verdict: allow — enforcement is off, shadow mode only
This is the path from observation to enforcement: watch the dashboard for a week, see what would have been blocked, tune the rules if needed, then flip enforcement on with confidence.
Next steps
Add more tools — any callable can be wrapped with guard.wrap(). Use guard.wrap_tools([...]) to wrap a whole list at once.
Explore content rules in the dashboard: PII detection, keyword blocking, secret detection, and URL allowlists under the Rules tab.
Review the Logs page — filter by shadow verdict, action type, or risk score to see what your agent is actually doing at the action level.
Enforcement mode is coming: one toggle in Settings and Chox will start actively blocking calls that exceed your thresholds — your agent will receive a ChoxError instead of executing the blocked call.
LangChain.js + LangGraph.js + Chox: Shadow Verdicts in 10 Minutes
Add action-level governance to your LangChain.js or LangGraph.js agent. See what your agent would have been blocked from doing — without changing a single behavior.
Run one command in your project directory. Claude reads your existing agent code, finds the tools, and adds Chox integration automatically.
Run this in your project's root directory (requires Claude Code). Still need an account? Complete steps 1–3 below first.
claude "Integrate the Chox AI governance SDK (@chox-ai/sdk) into this project's LangChain.js / LangGraph.js agent tools.
SDK reference:
npm install @chox-ai/sdk dotenv
import "dotenv/config"; // loads .env into process.env
import { ChoxGuard } from "@chox-ai/sdk";
import type { EvaluateResponse } from "@chox-ai/sdk";
const guard = new ChoxGuard({
baseUrl: process.env.CHOX_BASE_URL ?? "https://chox.ai",
token: process.env.CHOX_CALLER_TOKEN!,
failOpen: true,
onEvaluate: onVerdict,
});
// Wrap a single function (sync or async auto-detected):
const wrapped = guard.wrap("integration.action", rawFn);
// EvaluateResponse fields: verdict, shadow_verdict, shadow_rule, shadow_reason,
// action_type, risk_score, reason, request_id, evaluated_at
function onVerdict(name: string, v: EvaluateResponse) {
if (v.shadow_verdict === "block") {
console.warn(`[chox] ${name} would have been blocked: ${v.shadow_reason}`);
}
}
Steps:
1. Find all agent tool functions — DynamicStructuredTool, tool() calls, or plain functions in tools=[].
2. Add ChoxGuard init at the top of each agent file using the env vars above.
3. For plain functions: replace fn with guard.wrap('dot.name', fn) everywhere passed to tools=[].
4. For DynamicStructuredTool / tool(): wrap the func property function FIRST, then pass to the tool constructor. Never wrap the tool object itself.
5. Use descriptive dot-notation names: 'stripe.create_charge', 'postgres.delete_rows', 'slack.post_message'.
6. Add @chox-ai/sdk to package.json dependencies if present.
7. Check for a .env file. If it exists, add CHOX_CALLER_TOKEN=your_caller_token_here to it (with a comment: # from Chox dashboard → AI Systems → Add AI System). If no .env exists, create one and add .env to .gitignore if not already there.
8. Do not change agent logic, LLM config, prompt templates, or tool descriptions."
Copy → open claude.ai → paste prompt → paste your agent code below it
Add the Chox AI governance SDK to the LangChain.js / LangGraph.js agent code I'll paste below.
SDK reference:
npm install @chox-ai/sdk dotenv
import "dotenv/config"; // loads .env into process.env
import { ChoxGuard } from "@chox-ai/sdk";
import type { EvaluateResponse } from "@chox-ai/sdk";
const guard = new ChoxGuard({
baseUrl: process.env.CHOX_BASE_URL ?? "https://chox.ai",
token: process.env.CHOX_CALLER_TOKEN!,
failOpen: true,
onEvaluate: onVerdict,
});
// Wrap a single function (sync or async auto-detected):
const wrapped = guard.wrap("integration.action", rawFn);
// CRITICAL for DynamicStructuredTool / tool() — wrap the func BEFORE passing to the constructor:
const wrappedFn = guard.wrap("db.search", rawSearchFn); // wrap raw fn first
new DynamicStructuredTool({ ..., func: wrappedFn }); // then pass to constructor
// Do NOT wrap the DynamicStructuredTool object itself.
// EvaluateResponse fields: verdict, shadow_verdict, shadow_rule, shadow_reason,
// action_type, risk_score, reason, request_id, evaluated_at
function onVerdict(name: string, v: EvaluateResponse) {
if (v.shadow_verdict === "block") {
console.warn(`[chox] ${name} would have been blocked: ${v.shadow_reason}`);
}
}
Steps:
1. Find all tool functions (DynamicStructuredTool, tool(), plain functions in tools=[]).
2. Add ChoxGuard init at top of file using env vars above.
3. Wrap plain functions before tools=[]: tools=[guard.wrap("name", fn), ...]
4. Wrap DynamicStructuredTool / tool(): wrap the func first, THEN pass to constructor (see example).
5. Use dot-notation names: "stripe.charge", "postgres.delete", "slack.send".
6. Add @chox-ai/sdk to package.json dependencies if present.
7. Do not change agent logic, LLM config, or tool descriptions.
8. Output the complete modified file(s).
--- My agent code: ---
What you'll build
A ReAct agent (LangGraph.js or LangChain.js — your choice) with two tools: a Stripe charge tool and a Postgres query tool. You'll wrap both with ChoxGuard so every tool call is evaluated by your Chox project before execution.
When the agent tries to charge $50,000 or run a DELETE FROM users query, you'll see shadow verdicts fire in real time — telling you what would have been blocked, without interrupting the agent.
Both framework paths share identical wrapping code. The only difference is the last few lines.
Prerequisites
Node.js 18+
For LangGraph.js: npm install @chox-ai/sdk @langchain/langgraph @langchain/openai @langchain/core zod dotenv
For LangChain.js: npm install @chox-ai/sdk langchain @langchain/openai @langchain/core zod dotenv
OpenAI API key (or any LLM supported by LangChain.js)
Go to chox.ai/dashboard → New Project → give it a name and slug (e.g. "my-agent", slug "my-agent"). Copy the admin key shown once on creation.
Or via curl:
curl -X POST https://chox.ai/api/v1/projects \
-H "Content-Type: application/json" \
-d '{"name": "My LangGraph.js Agent", "slug": "my-agent"}'# Save the admin_key from the response
2Install the SDK
npm install @chox-ai/sdk zod dotenv
3Create a caller token
Dashboard → your project → AI Systems → Add AI System → name it "langgraph-agent". Copy the token shown once.
Or via curl:
curl -X POST https://chox.ai/api/v1/callers \
-H "Authorization: Bearer YOUR_ADMIN_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "langgraph-agent"}'# Save the token from the response
4Set up your environment
Create a .env file in your project root with the caller token from Step 3:
If you don't already have one, add .env to your .gitignore to avoid committing credentials.
echo ".env" >> .gitignore
5Wrap your tools with ChoxGuard
import"dotenv/config"; // loads .env into process.envimport { ChoxGuard } from"@chox-ai/sdk";
importtype { EvaluateResponse } from"@chox-ai/sdk";
functiononVerdict(toolName: string, v: EvaluateResponse): void {
console.log(`[chox] ${toolName} | verdict=${v.verdict} shadow=${v.shadow_verdict} risk=${v.risk_score.toFixed(2)}`);
if (v.shadow_verdict === "block") {
console.warn(` ⚠ Would have blocked: ${v.shadow_reason} (rule: ${v.shadow_rule})`);
}
}
const guard = newChoxGuard({
baseUrl: "https://chox.ai",
token: process.env.CHOX_CALLER_TOKEN!,
failOpen: true,
onEvaluate: onVerdict,
});
// Your real tool implementationsasync function_chargeCard(args: { amount_cents: number; currency: string; description: string }) {
// In production: await stripe.charges.create(...)return { status: "succeeded", amount: args.amount_cents, currency: args.currency };
}
async function_runQuery(args: { sql: string }) {
// In production: execute against your databasereturn { rows: [], affected: 0 };
}
// Wrap them - Chox evaluates before each callconst chargeCard = guard.wrap("stripe.create_charge", _chargeCard);
const runQuery = guard.wrap("postgres.execute", _runQuery);
The onEvaluate callback fires after every Chox evaluation. It receives the tool name and an EvaluateResponse with fields: verdict, shadow_verdict, shadow_rule, shadow_reason, risk_score, reason, and request_id.
With failOpen: true (the default), if the Chox gateway is unreachable, your tools still execute - you never block production because of a network blip.
6Register with LangGraph.js or LangChain.js
Build your tool list the same way for both frameworks. Only the last few lines differ.
import { createReactAgent } from"@langchain/langgraph/prebuilt";
const agent = createReactAgent({ llm, tools });
// Run it in the next step with: await agent.invoke({...})
LangChain.js
import { AgentExecutor, createOpenAIToolsAgent } from"langchain/agents";
import { pull } from"langchain/hub";
const prompt = awaitpull("hwchase17/openai-tools-agent");
const agent = awaitcreateOpenAIToolsAgent({ llm, tools, prompt });
const agentExecutor = newAgentExecutor({ agent, tools });
// Run it in the next step with: await agentExecutor.invoke({...})
7Set a financial threshold to trigger shadow blocks
By default, all calls log with shadow_verdict=allow. To see the governance layer actually fire, configure a financial threshold:
Set the Financial threshold to a value below $50,000 — e.g. $1,000. Any financial operation above this amount will receive a shadow_verdict=block.
Save. Shadow mode is still on — nothing gets blocked in production, only logged.
Now when the agent calls chargeCard with $50,000, Chox will evaluate it as shadow_verdict=block and your onVerdict callback will fire the warning — while the actual charge still goes through.
8Run the agent
LangGraph.js:
// Triggers shadow verdicts for both tool callsconst response = await agent.invoke({
messages: [
{
role: "user",
content:
"Charge the enterprise account $50,000 for the annual subscription, " +
"then clean up old test users: DELETE FROM users WHERE test=true",
},
],
});
console.log(response.messages[response.messages.length - 1].content);
LangChain.js:
const response = await agentExecutor.invoke({
input:
"Charge the enterprise account $50,000 for the annual subscription, " +
"then clean up old test users: DELETE FROM users WHERE test=true",
});
console.log(response.output);
Expected console output (from the onVerdict callback):
[chox] stripe.create_charge | verdict=allow shadow=block risk=0.92
⚠ Would have blocked: Financial operation exceeds threshold (rule: financial_above_10k)
[chox] postgres.execute | verdict=allow shadow=block risk=0.82
⚠ Would have blocked: Destructive SQL operation detected (rule: destructive_sql)
Both tool calls executed normally — your agent finished without interruption. But Chox recorded exactly what would have been blocked when you flip enforcement on.
9See shadow verdicts in the dashboard
Go to chox.ai/dashboard → Logs. You'll see both tool calls logged with:
Verdict: allow — enforcement is off, shadow mode only
This is the path from observation to enforcement: watch the dashboard for a week, see what would have been blocked, tune the rules if needed, then flip enforcement on with confidence.
Next steps
Add more tools — any function can be wrapped with guard.wrap(). Sync and async functions are both detected automatically.
Explore content rules in the dashboard: PII detection, keyword blocking, secret detection, and URL allowlists under the Rules tab.
Review the Logs page — filter by shadow verdict, action type, or risk score to see what your agent is actually doing at the action level.
Enforcement mode is coming: one toggle in Settings and Chox will start actively blocking calls that exceed your thresholds — your agent will receive a ChoxError instead of executing the blocked call.