invoke, don't treat HITL or callbacks as a gate, wrap authored tools or put policy on middleware, forbid extra schema fields, correlate with a caller-owned thread ID, and run the same scanners in CI that you run in the editor.What is the LangChain security guide?
LangChain is one of the most widely used agent frameworks. You define tools, wire a model, and call invoke() or ainvoke() to run a turn. The model reads user messages and may call those tools to query databases, send email, or call APIs. Any untrusted text in the conversation—support tickets, uploaded files, retrieved documents—can try to steer those calls. That is prompt injection.
You need two separate controls. Screen inbound text before the agent starts, then gate tool calls with rules you define before the side effect runs. LangChain also offers human-in-the-loop middleware and interrupts for confirmation. Those pause for a person; they don't replace an action gate on every tool invocation.
Arcjet Guard is a runtime policy layer for AI agents. You define rules—prompt-injection detectors, rate limits, PII checks, allowlists—and Guard allows or denies inbound text and tool calls before side effects run. The examples below use Guard for both controls.
This guide covers LangChain 1.2+ JavaScript (langchain, @langchain/core) and LangChain 1.3+ Python (langchain, langchain-core, langgraph). The two runtimes share a job and don't share an import. These examples wrap LangChain agent tools—not LangGraph StateGraph plus ToolNode (see the LangGraph security guide) and not Vercel AI SDK tool() handlers.
| Runtime | Packages | Guard package (examples) | Agent constructor |
|---|---|---|---|
| Python |
| arcjet.guard.langchain | create_agent |
| JavaScript |
| @arcjet/guard/langchain/v1 | createAgent |
Work through these eight topics in order. Each one is a control you can verify, not a slogan.
- Patch and lock dependencies. Stay on LangChain 1.x.
- Keep API keys off the client and out of LangSmith traces.
- Screen user text before
invoke/ainvoke. - Don't treat
humanInTheLoopMiddlewareor callbacks as a policy gate. - Wrap authored tools that you call yourself, or put agent-wide policy on middleware.
- Narrow
args_schemabefore you wrap so the model can't invent extra fields. - Correlate the run with a caller-owned thread or session ID.
- Catch issues in the editor and in CI before they ship.
For shorter wiring recipes, see How do I secure a LangChain Python agent? and the LangChain JS agent guard. For the Guard helpers used in the Python examples, see the LangChain agent guard.
How do you keep LangChain dependencies safe?
Apply patches on a schedule you actually keep, and out of band for a critical advisory. Commit a lockfile so every environment installs the same graph. LangChain 1.0 is an LTS line—stay on 1.x until 2.0 ships, then plan the upgrade. Legacy 0.3 is in maintenance until December 2026; don't start an app there.
JavaScript wrapToolCall only sees runtime.configurable.thread_id as of LangChain 1.2.34. That is why the JS peer floor is higher than @langchain/core. Install the provider package your model string needs (@langchain/openai for "openai:gpt-4o").
Python extras are split on purpose. The langchain extra is guard_tool on a BaseTool. The langchain-agents extra adds middleware for create_agent and pulls LangGraph. Don't install the agents extra if you only wrap tools that you call yourself. See trivial packages for the cost of a convenience extra that you don't use.
How should LangChain secrets be handled?
Model keys such as OPENAI_API_KEY and ANTHROPIC_API_KEY belong on the server, loaded at process start. Don't put them in a client bundle, a LangSmith example, or a prompt template that later lands in a trace.
LangSmith and callback handlers can serialize tool arguments. Redact before export. See redacting sensitive data from logs and storing secrets in environment variables.
How do you screen inbound LangChain text?
LangChain doesn't expose a dedicated inbound hook, so screen in your route handler, job, or webhook before you call invoke, ainvoke, or stream. That is where a pasted ticket or fetched webpage still lives as plain text—before the model treats it as instructions. wrapModelCall, beforeModel, and afterModel intercept the model call, not user text.
With Guard, call arcjet.guard() / arcjet.guard_sync() with prompt-injection rules on that string. Pair the async Guard client with ainvoke() and the sync client with invoke(). launch_arcjet_sync is for Flask, Django, and other sync Python. Direct guard() / guard_sync() fails open: an ALLOW isn't proof the rules ran, so gate on hasFailedOpen() / has_failed_open() when this call site must fail closed. On deny, don't start the agent.
JavaScript:
import { detectPromptInjection } from "@arcjet/guard";import { langchainContext } from "@arcjet/guard/langchain/v1";import { arcjet } from "./arcjet.js";
const inbound = detectPromptInjection();const config = { configurable: { thread_id: conversationId } };
const decision = await arcjet.guard({ label: "message.received", rules: [inbound(userText)], ...langchainContext(config),});
if (decision.conclusion === "DENY" || decision.hasFailedOpen()) { throw new Error("message blocked");}Python: call guard() / guard_sync() with label="message.received" the same way, then create_agent(...).ainvoke(...). Pass config={"configurable": {"arcjet_correlation_id": session_id}} so middleware and guarded tools share one Sequence. The helpers don't use the LangChain run_id.
Why isn't human-in-the-loop a security policy?
LangChain can pause an agent and wait for a person to approve the next tool call. That is human-in-the-loop (HITL): useful for workflows where a reviewer needs full context. It isn't a substitute for evaluating every tool call against policy before it runs.
JavaScript humanInTheLoopMiddleware and interrupt() pause for a person. Python callbacks and LangGraph interrupts do the same. They are confirmation, not a remote allow or deny.
A handler that logs on_tool_start can't refuse the send. LangChain ignores callback return values. ArcjetCaptureHandler is observe-only. See human approval is not a security policy.
How do you gate LangChain tools?
Once the agent is running, tools are where side effects happen. LangChain offers several surfaces depending on whether you call a tool yourself or let the agent pick it. Pick the helper that matches what you hold when the effect would run.
Python:
- Any callable:
guard_action/guard_action_syncin corearcjet.guard. - A
BaseToolthat you call yourself:guard_tool(arcjet[langchain]). - An agent from
create_agent:ArcjetMiddlewareplusToolPolicy(arcjet[langchain-agents]). - Observe only:
ArcjetCaptureHandler. It can't deny.
Configure args_schema, handle_tool_error, and response_format before you wrap. Changes after wrapping don't reach the tool. Narrow the schema with extra="forbid" so the model can't invent a recipient.
from langchain.agents import create_agentfrom arcjet.guard import TokenBucket, launch_arcjetfrom arcjet.guard.langchain import ArcjetMiddleware, ToolPolicy
arcjet = launch_arcjet(key=ARCJET_KEY)send_limit = TokenBucket( refill_rate=5, interval_seconds=60, max_tokens=5, bucket="email",)
agent = create_agent( model="openai:gpt-4o", tools=[send_email, search_orders], middleware=[ ArcjetMiddleware( guard=arcjet, policies={ "send_email": ToolPolicy( action="email.sent", rules=[send_limit(key="email", requested=1)], ) }, tools=[send_email, search_orders], ) ],)A typo or a renamed @tool function leaves that tool unguarded. Pass tools= the same sequence that you gave create_agent. On deny, ArcjetMiddleware raises ArcjetDeniedError. guard_tool raises ArcjetToolDeniedError, or follows handle_tool_error when you configured that before wrapping. Alert on ArcjetUnavailableError / ArcjetToolUnavailableError; a denial is the system working.
Python guard_tool and ArcjetMiddleware compose: a guarded tool called from a guarded agent evaluates once per policy. Pick the surface that matches what you hold. You don't need both for the same tool.
JavaScript:
- Authored
tool():guardTool. Returns a plainArcjetDenialResult. Don't throw. Don't fabricate aToolMessage. - Agent-wide:
guardMiddlewareascreateMiddleware({ wrapToolCall }). Returns a realToolMessagewith JSONcontentand default status. A bare object crashes the reducer. Don't setstatus: "error". - Policy sits on
wrapToolCallonly. The middleware skips branded (guardTool) tools when it can look them up.
import { createAgent } from "langchain";import { tool } from "@langchain/core/tools";import { z } from "zod";import { localDetectSensitiveInfo, tokenBucket } from "@arcjet/guard";import { guardMiddleware, guardTool } from "@arcjet/guard/langchain/v1";import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({ bucket: "lookups", refillRate: 10, intervalSeconds: 60, maxTokens: 10,});const detectPii = localDetectSensitiveInfo();
const lookupOrder = guardTool( arcjet, tool( async ({ orderNumber, note }) => ({ orderNumber, note, status: "shipped", }), { name: "lookup_order", description: "Look up an order by number", schema: z.object({ orderNumber: z.string(), note: z.string(), }), }, ), { action: "order.looked-up", rules: (input) => [ lookupLimit({ key: input.orderNumber, requested: 1 }), detectPii(input.note), ], },);
const agent = createAgent({ model: "openai:gpt-4o", tools: [lookupOrder], middleware: [guardMiddleware(arcjet, { sessionId: conversationId })],});Scan the free-text note. An opaque orderNumber doesn't trip email, phone, card, or IP detection.
How does the editor and CI catch LangChain security bugs?
Turn on TypeScript strict or Pyright, a linter, and secret scanning in the editor. Run the same scanners in CI that you run locally—an editor warning that isn't a CI failure will be ignored.
Fail the build on secrets in the diff and on known vulnerable dependencies. None of these replace a review of the tools that you passed to create_agent / createAgent and the tools that you actually wrapped. They catch the mistakes that are cheap to find automatically.
What do you do after this guide?
Confirm each item against one sensitive turn: a prompt that asks for a refund, an authored tool that you call yourself, and a tool that the model picks by name. Then apply the LangGraph security guide if you own a StateGraph, and AI agent runtime security for sequence, budget, and identity controls.
Frequently asked questions
What is the LangChain security guide?
Use this eight-item guide on LangChain Python 1.3 and JS 1.2.34+. Stay on 1.x, keep keys out of LangSmith traces, screen text before invoke, don't treat HITL or callbacks as a gate, wrap authored tools or put policy on middleware, forbid extra schema fields, correlate with a caller-owned thread ID, and run the same scanners in CI that you run in the editor.
Is this the same as LangGraph StateGraph?
No. Python create_agent and JS createAgent are this guide. StateGraph plus ToolNode is the LangGraph security guide and @arcjet/guard/langgraph/v1.
Can ArcjetCaptureHandler claim a tool?
No. LangChain ignores callback return values. Use guard_tool or ArcjetMiddleware in Python, and guardTool or guardMiddleware in JavaScript.
Why is the JS langchain peer 1.2.34?
wrapToolCall only sees runtime.configurable.thread_id as of LangChain 1.2.34. Earlier 1.2 releases can't correlate the middleware gate.
AI runtime security in your code
Protect your AI agent workflows with Arcjet
Get allow, deny, and redact on agent actions before the side effect.