arcjet/arcjet-js/integrate-arcjet-guard-langchain

Claude Code and agent skill by arcjet/arcjet-js

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Integrate Arcjet security into a LangChain JS createAgent using @arcjet/guard — wrap tool() / StructuredTool, put guardMiddleware on createAgent({ middleware }) for MCP / unwrapped tools, and read configurable.thread_id for correlation. Use when asked to add Arcjet to langchain createAgent, rate limit its tools, screen inbound messages, or block prompt injection / PII. This is LangChain JS, not the Python page.

Install

npx vskill@latest install arcjet/arcjet-js/integrate-arcjet-guard-langchain

SKILL.md

Integrate Arcjet Guard into a LangChain JS createAgent

@arcjet/guard's LangChain v1 namespace wraps the agent's existing Arcjet client. It never talks to the Arcjet API itself. Three surfaces, one decision rule:

  • An authored tool (tool() / StructuredTool) → guardTool().

DENY returns a plain ArcjetDenialResult. Do not throw. Do not fabricate a ToolMessage. createAgent's baseHandler wraps a non-ToolMessage in a success ToolMessage.

  • MCP / unwrapped / runtime-discovered tools → guardMiddleware().

A createAgent({ middleware }) middleware whose wrapToolCall is the invoke()-wide gate. It denies by returning a real ToolMessage (content = JSON of the payload, tool_call_id = request.toolCall.id, name = request.toolCall.name) without calling handler. Already-branded tools are skipped when request.tool can be looked up. Do not set status: "error". Do not throw (throws bubble and drop arcjetDenied).

  • Correlation → langchainContext() reads

configurable.thread_id (what wrapToolCall sees on runtime.configurable as of langchain 1.2.34), then caller-owned sessionId / conversationId. It never mints a new id. It never reads traceId. A run that pauses on interrupt() resumes through the same config, so it keeps its thread_id and its later decisions stay on the Sequence that started it — the interrupt and its resume value are simply not correlation sources of their own.

This namespace is LangChain JS `createAgent` + `wrapToolCall`. Not LangGraph Graph API (StateGraph + ToolNode) — that is @arcjet/guard/langgraph/v1. Not vercel-ai/v7. Server-side provider tools and headless .implement() tools are out of scope. Do not also wrap the same tool with @arcjet/guard/langgraph/v1 or @arcjet/guard/vercel-ai/v7.

Docs live at docs.arcjet.com/guards/langchain/, which covers JavaScript and Python in tabs. /guards/langchain-js/ redirects there.

Screen inbound before agent.invoke — there is no inbound hook. SDK middleware that is not wrapToolCall is not Guard.

There is no first-class inbound channel, so there is no guardInbound. Put prompt-injection (and other inbound rules) in the application before agent.invoke. wrapModelCall / beforeModel / afterModel intercept the model call, not user text. They are not this policy gate.

humanInTheLoopMiddleware / interrupt is HITL, not a policy gate.

humanInTheLoopMiddleware / interrupt() / approve-edit-reject-respond is human-in-the-loop. Same trap as Mastra requireApproval, Claude canUseTool, LangGraph interrupt(), Genkit toolApproval, OpenAI Agents needsApproval, Google ADK requireConfirmation, and Cloudflare Think needsApproval. There is no guardApproval. Policy sits on wrapToolCall only — do not deny in afterModel. HITL already lives there.

Deny inside tool() (and guardMiddleware's wrapToolCall). MCP and unwrapped tools skip an unwrapped handler.

The authored tool() handler is the deny point for tools you own. MCP tools, runtime-discovered tools, and anything not wrapped with guardTool skip that handler. guardMiddleware is the invoke()-wide gate for those.

guardMiddleware can deny. LangChain's official auth example returns a ToolMessage without calling handler. wrapToolCall's return is not passed through baseHandler. A duck-typed object without the real class fails ToolMessage.isInstance and crashes the messages reducer. Do not throw. Do not set status: "error".

Questions to ask the human first

Ask only what you cannot infer from the code; suggest defaults.

  1. Which tools are risky (external side effects, irreversible, spends

money, sends messages)? Those get guardTool. MCP / runtime-discovered / tools you did not author get guardMiddleware.

  1. What limits? (e.g. "10 lookups/min per order" → tokenBucket.)
  2. Who is the user for metadata — an opaque user/tenant ID (never PII)?

Default: none. Pass it via metadata on the policy. Put the conversation / session id you already have on agent.invoke(..., { configurable: { thread_id } }). That id is the correlation id, not the user. wrapToolCall only sees runtime.configurable.thread_id as of langchain 1.2.34.

  1. Is an Arcjet outage unacceptable? Every helper defaults to

onGuardError: "deny". Ask explicitly about inbound screening before agent.invoke: failing closed there means the agent does not run for the duration of the outage, so "allow" is a routine and legitimate choice at that one call site.

The six things readers get wrong

  1. There is no `guardInbound`. Screen prompt injection before

agent.invoke. wrapModelCall / beforeModel / afterModel are not Guard.

  1. `humanInTheLoopMiddleware` / `interrupt()` is not a policy gate.

It is HITL. Policy sits on wrapToolCall only. Do not deny in afterModel.

  1. The import path is versioned and there is no alias.

@arcjet/guard/langchain/v1. @arcjet/guard/langchain does not resolve. Docs are /guards/langchain/; read the JavaScript tab.

  1. Correlation is read, never minted. Do not call createAgentContext

inside a middleware / tool callback — that generates a second id and splits the Sequence. Put the id you already chose on configurable.thread_id. Do not read traceId. Resuming after an interrupt() reuses the same config and therefore the same thread_id, so decisions after the pause already correlate to the originating Sequence — do not derive an id from the interrupt or its resume value.

  1. **Do not double-wrap with @arcjet/guard/langgraph/v1,

@arcjet/guard/vercel-ai/v7, or @arcjet/guard/claude-managed-agents/v0.** guardTool throws if the tool already carries the Arcjet protection brand. guardMiddleware skips branded tools so Guard is not double-called.

  1. Two denial envelopes. Do not collapse them. guardTool returns

a plain ArcjetDenialResult. guardMiddleware wrapToolCall MUST return a real ToolMessage. A bare object from wrapToolCall is the reducer-crash case.

Step 1: Install and find the guard client

Install @arcjet/guard (required), plus langchain and @langchain/core (optional peers, needed for @arcjet/guard/langchain/v1). Always use the versioned path: @arcjet/guard/langchain/v1 resolves; @arcjet/guard/langchain throws ERR_PACKAGE_PATH_NOT_EXPORTED. The peer range is >=1.2.0 <2 for langchain and >=1 <2 for @langchain/core (the range langgraph/v1 already shipped — langchain's own ^1.2.9 peer on core is what actually binds here). wrapToolCall only sees runtime.configurable.thread_id as of langchain 1.2.34. Node 22+.

sh
npm install @arcjet/guard @arcjet/sensitive-info-rampart langchain @langchain/core

If the agent has no guard client yet, launch one once at module scope:

ts
import { launchArcjet } from "@arcjet/guard";
export const arcjet = launchArcjet({ key: process.env.ARCJET_KEY! });

Step 2: Gate authored tools

ts
import { tokenBucket, localDetectSensitiveInfo, policyInput } from "@arcjet/guard";
import { guardTool } from "@arcjet/guard/langchain/v1";
import { rampart } from "@arcjet/sensitive-info-rampart";
import { tool } from "@langchain/core/tools";
import { z } from "zod";
import { arcjet } from "./arcjet.js";
const lookupLimit = tokenBucket({
refillRate: 10,
intervalSeconds: 60,
maxTokens: 10,
});
// Factory then text — same shape as `detectPromptInjection()(text)`.
// Scan free-text args (a note, reason, body). An opaque `orderNumber`
// is not free text, so do not pass it here.
const detectPii = localDetectSensitiveInfo({ backend: rampart() });
export 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",
// Invoke config is the trusted half of LangChain `func`/`invoke`.
actor: (_input, runtime) => {
const config = runtime as { configurable?: { thread_id?: string } } | undefined;
return String(config?.configurable?.thread_id ?? userId);
},
inputs: (input) => ({
orderNumber: policyInput.server.string(input.orderNumber),
}),
rules: (input) => [
lookupLimit({ key: input.orderNumber, requested: 1 }),
detectPii(input.note),
],
},
);
  • Omit rules to submit none. The guard call still happens.
  • Optional actor and inputs (static, or a resolver over this adapter's native call — parsed input plus trusted runtime/context) are forwarded on the

guard call so a remote policy that declares those names can evaluate. Build each input with policyInput. A resolver that throws or returns a value Guard cannot use is left out of the guard call, and onGuardError decides.

  • On DENY the original func / invoke never runs. The caller

receives { arcjetDenied: true, reason, message, retryable }. Through createAgent, baseHandler wraps that object in a success ToolMessage.

  • Default onGuardError: "deny" blocks the tool if Arcjet is unreachable,

or if an action, rules, metadata or sessionId callback throws or returns a value Guard cannot use. A failed callback never skips the guard call: Guard is called without its value (a failed action falls back to "tool.invoked"), so remote policy still evaluates the call and a DENY stands.

Step 3: Gate unwrapped / MCP / runtime-discovered tools

ts
import { tokenBucket } from "@arcjet/guard";
import { guardMiddleware } from "@arcjet/guard/langchain/v1";
import { createAgent } from "langchain";
import { arcjet } from "./arcjet.js";
const mcpLimit = tokenBucket({
refillRate: 20,
intervalSeconds: 60,
maxTokens: 20,
});
const agent = createAgent({
model,
tools: [lookupOrder, ...mcpTools],
middleware: [
guardMiddleware(arcjet, {
action: ({ toolName }) => `${toolName}.invoked`,
rules: ({ toolName }) => [mcpLimit({ key: toolName, requested: 1 })],
sessionId: conversationId,
}),
],
});

Already-branded (guardTool) tools skip the middleware guard when they are present on request.tool. Tools that cannot be looked up (request.tool undefined — MCP / unwrapped / runtime-discovered) are still gated.

Step 4: Screen inbound before invoke

ts
import { detectPromptInjection } from "@arcjet/guard";
import { langchainContext } from "@arcjet/guard/langchain/v1";
import { arcjet } from "./arcjet.js";
const inbound = detectPromptInjection();
const decision = await arcjet.guard({
label: "message.received",
rules: [inbound(userText)],
...langchainContext({ configurable: { thread_id: conversationId } }),
});
if (decision.conclusion === "DENY") {
throw new Error("message blocked");
}
if (decision.hasFailedOpen()) {
throw new Error("inbound screening failed open");
}
await agent.invoke(
{ messages: [{ role: "user", content: userText }] },
{ configurable: { thread_id: conversationId } },
);

There is no guardInbound.

Step 5: Correlation

Put the id you already have on configurable.thread_id:

ts
await agent.invoke(
{ messages: [{ role: "user", content: userText }] },
{ configurable: { thread_id: conversationId } },
);

Preference order: configurable.thread_id, then caller-owned sessionId, then conversationId, then init.sessionId / init.correlationId. If none is a valid 1–256 printable-ASCII string, the call is uncorrelated rather than joined to a generated id nobody has.

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

7-day trend score
5

Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/arcjet/arcjet-js/integrate-arcjet-guard-langchain/badge)](https://verified-skill.com/skills/arcjet/arcjet-js/integrate-arcjet-guard-langchain)

Something wrong with this skill or its page? Tell us.