LangChain security guide

Apply runtime security to LangChain JavaScript and Python agents.

9 min read
In short: Apply runtime security to LangChain JavaScript and Python agents. Screen inbound text, guard tools, authorize resources, and test middleware and approval-resume paths in each runtime.

How do you secure LangChain?

To secure a LangChain agent, screen inbound text, wrap authored tools, and apply tool-call middleware to tools supplied by other integrations. Derive identity from server-controlled context and authorize resources inside each handler. LangChain JavaScript and Python use separate adapters; test their denial behavior and human-approval resume paths independently.

LangChain is a framework for building agents that call tools. 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.

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 following examples 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.

RuntimePackagesGuard package (examples)Agent constructor
Python

langchain>=1.3,<2, langchain-core>=1.2.5,<2, langgraph>=1.2,<2

arcjet.guard.langchaincreate_agent
JavaScript

langchain>=1.2.34,<2, @langchain/core>=1,<2

@arcjet/guard/langchain/v1createAgent

Use this checklist to review your application:

  • 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 humanInTheLoopMiddleware or callbacks as a policy gate.
  • Wrap authored tools that you call yourself, or put agent-wide policy on middleware.
  • Narrow args_schema before 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?. For the Guard helpers used in the 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 do you manage LangChain secrets?

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 observe lifecycle events; they do not themselves pause execution for approval. LangGraph interrupts can pause a Python run. Keep those roles separate from policy enforcement on every tool call.

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_sync in core arcjet.guard.
  • A BaseTool that you call yourself: guard_tool (arcjet[langchain]).
  • An agent from create_agent: ArcjetMiddleware plus ToolPolicy (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_agent
from arcjet.guard import TokenBucket, launch_arcjet
from 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 plain ArcjetDenialResult. Don't throw. Don't fabricate a ToolMessage.
  • Agent-wide: guardMiddleware as createMiddleware({ wrapToolCall }). Returns a real ToolMessage with JSON content and default status. A bare object crashes the reducer. Don't set status: "error".
  • Policy sits on wrapToolCall only. 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";
// Resolve authenticatedUser from the server session before constructing tools.
const lookupLimit = tokenBucket({
bucket: "lookups",
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});
const detectPii = localDetectSensitiveInfo({
deny: ["EMAIL", "PHONE_NUMBER", "IP_ADDRESS", "CREDIT_CARD_NUMBER"],
});
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: authenticatedUser.id, 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 do 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.

How do you verify enforcement before deployment?

Use deterministic Guard responses and a handler call counter. A DENY or unavailable evaluation must leave the counter at zero; an authorized ALLOW must call the handler once. Also test another tenant's resource identifier: application authorization must still refuse access even when Guard allows.

Test a wrapped authored tool and an unwrapped MCP tool in both JavaScript and Python. Exercise middleware ordering, a rejected input, and approval resume. Callback traces are evidence that a run was observed, not proof that the tool was prevented from executing.

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?

To secure a LangChain agent, screen inbound text, wrap authored tools, and apply tool-call middleware to tools supplied by other integrations. Derive identity from server-controlled context and authorize resources inside each handler. LangChain JavaScript and Python use separate adapters; test their denial behavior and human-approval resume paths independently.

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

Wrap the callable or the BaseTool so the decision sits on the tool itself rather than on a callback running alongside it.