Skip to main content
skill Claude Code CX Codex Cursor GitHub Copilot

langchain-middleware — Claude Code skill: what it does, how to install

Adds middleware hooks for LangChain agents: pauses execution before dangerous tool calls, lets a human approve, edit or reject the action, intercepts calls for logging and retry logic.

Install into Claude Code · Skills CLI (Vercel) All methods and platforms ↓
npx skills add langchain-ai/langchain-skills --skill langchain-middleware -a claude-code
Open on GitHub

What langchain-middleware does

The skill provides three patterns. HumanInTheLoopMiddleware pauses the agent before calling a tool (send_email, delete_email) and waits for a decision: approve, edit or reject. When editing, the human changes arguments (to, subject, body); when rejecting, passes feedback and the agent continues accordingly. Custom hooks (wrap_tool_call, before_model, after_model, before_agent, after_agent) intercept calls for error handling, logging or early return.

Requires a checkpointer (MemorySaver) and thread_id in the config — without them Command(resume={...}) cannot resume execution. You can set a policy per tool: send_email allows three decision types, delete_email only approve or reject, read_email runs without interrupt. Wrap hooks call handler(request) inside or return early; before/after hooks return None or a dict of state updates.

For TypeScript use humanInTheLoopMiddleware with interruptOn, allowedDecisions and Zod tool schemas. The skill puts an __interrupt__ field in result1 with the expected action; the second invoke with Command continues the thread in the same session.

What it helps with

  • pause the agent before sending an email or deleting data for manual confirmation
  • edit tool call arguments (recipient address, email subject) before execution
  • reject an action and pass feedback so the agent tries a different approach
  • intercept a tool call for logging, retry logic or early result return
  • set different approval policies for different tools in one agent

Example prompts for the agent

  • pause the agent before calling `send_email` and let me approve the recipient address
  • set up a `wrap_tool_call` hook that logs all tool calls to `tool_calls.log`
  • resume agent execution after rejection — pass feedback 'cannot delete data without manager approval'

How to install langchain-middleware

1Skills CLI (Vercel)

npx skills add langchain-ai/langchain-skills --skill langchain-middleware -a claude-code

2Manually: put the files in place

~/.claude/skills/langchain-middleware/SKILL.md

Copy the whole skill folder; for one project use .claude/skills/langchain-middleware/

From the same set14

The whole set →
deep-agents-memory langchain-ai A skill that maps where a Deep Agent keeps its files: thread state, a persistent store, or the real disk. Useful once the agent starts dropping context. Claude Code CX Codex Cursor +1 19,000installs langgraph-persistence langchain-ai A Claude Code skill for LangGraph persistence: wiring a checkpointer, keeping conversations apart by thread_id, and replaying older graph state. Claude Code CX Codex Cursor +1 18,000installs langgraph-fundamentals langchain-ai Skill for writing LangGraph code across platforms. Covers StateGraph, state schemas, nodes, edges, Command, Send, invoke, streaming, and error handling. Claude Code CX Codex Cursor +1 18,000installs langchain-rag langchain-ai Equips coding agents with reference RAG implementations in Python and TypeScript using LangChain loaders, splitters, and vector stores. Claude Code CX Codex Cursor +1 17,500installs langchain-fundamentals langchain-ai Guides LangChain agent work with create_agent(), tools, and middleware. Use it when you need saved conversation state, approval flows, or error handling. Claude Code CX Codex Cursor +1 17,400installs langgraph-human-in-the-loop langchain-ai Skill for building LangGraph workflows that pause and wait for human input. Use it to show agent output, request approval, or validate user responses before continuing. Claude Code CX Codex Cursor +1 17,200installs deep-agents-orchestration langchain-ai Guides setup of orchestration in Deep Agents: subagent delegation, todo planning, and human approval interrupts. Claude Code CX Codex Cursor +1 16,100installs langchain-dependencies langchain-ai Provides verified package versions and dependency rules for LangChain, LangGraph, and LangSmith. Helps set up clean Python or TypeScript projects without version conflicts. Claude Code CX Codex Cursor +1 16,000installs

Similar

Pair Programming ruvnet This skill runs AI-assisted pair programming with driver and navigator roles, code checks, testing, and dedicated TDD, debugging, and mentor modes. Claude Code CX Codex Cursor +1 1,200installs github-multi-repo ruvnet A skill for coordinating several repositories at once: it aligns package versions, checks cross-package integration and suggests structure changes. Claude Code CX Codex Cursor +1 1,200installs application-form-filler Paramchoudhary A skill that fills job application form fields with answers built from the candidate's CV and the job description. Handy when every ATS question needs a tailored response. Claude Code CX Codex Cursor +1 6,600installs seo iannuttall Lets your coding agent read your own Search Console, Analytics, Bing and Clicky numbers, plus keyword and backlink research from DataForSEO or Ahrefs. Claude Code CX Codex Cursor +1 11,500installs setup-matt-pocock-skills Matt Pocock This skill configures a repository for Matt Pocock's engineering skills by setting its issue tracker, triage labels, and domain docs layout. Run it before using the other skills. Claude Code 1,052,851installs to-tickets Matt Pocock Turns a plan, spec, or conversation into small end-to-end tickets with explicit blockers. Use it to agree on the work breakdown before tickets go to a tracker. Claude Code 711,230installs writing-plans Jesse Vincent This skill turns requirements for a multi-step task into an implementation plan before code changes begin. It helps when another engineer needs exact files, interfaces, and checks. Claude Code CX Codex 282,859installs stitch-loop official google-labs-code Guides agents through iterative website creation with Stitch MCP. The agent reads the baton file, generates UI, integrates the page, and prepares tasks for the next run. Claude Code CX Codex Cursor +1 56,300installs

Questions about langchain-middleware

How to install langchain-middleware in Claude Code?

Run npx skills add langchain-ai/langchain-skills --skill langchain-middleware -a claude-code. The skill will appear in ~/.claude/skills/langchain-middleware/SKILL.md.

What does langchain-middleware do?

Pauses a LangChain agent before dangerous tool calls for manual approval, editing or rejection. Provides custom hooks to intercept model and tool calls.

What is needed for Human-in-the-Loop to work?

A checkpointer (e.g. MemorySaver) and thread_id in the config. Without them Command(resume={...}) cannot find the saved state and resume execution.

Source text

SKILL.md · MIT
---
name: langchain-middleware
description: "INVOKE THIS SKILL when you need human-in-the-loop approval, custom middleware, or structured output. Covers HumanInTheLoopMiddleware for human approval of dangerous tool calls, creating custom middleware with hooks, Command resume patterns, and structured output with Pydantic/Zod."
---

<overview>
Middleware patterns for production LangChain agents:

- **HumanInTheLoopMiddleware** / **humanInTheLoopMiddleware**: Pause before dangerous tool calls for human approval
- **Custom middleware**: Intercept tool calls for error handling, logging, retry logic
- **Command resume**: Continue execution after human decisions (approve, edit, reject)

**Requirements:** Checkpointer + thread_id config for all HITL workflows.
</overview>

---

## Human-in-the-Loop

<ex-basic-hitl-setup>
<python>
Set up an agent with HITL middleware that pauses before sending emails for approval.

```python
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import MemorySaver
from langchain.tools import tool

@tool
def send_email(to: str, subject: str, body: str) -> str:
    """Send an email."""
    return f"Email sent to {to}"

agent = create_agent(
    model="gpt-4.1",
    tools=[send_email],
    checkpointer=MemorySaver(),  # Required for HITL
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
            }
        )
    ],
)
```
</python>
<typescript>
Set up an agent with HITL that pauses before sending emails for human approval.

```typescript
import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
import { tool } from "@langchain/core/tools";
import { z } from "zod";

const sendEmail = tool(
  async ({ to, subject, body }) => `Email sent to ${to}`,
  {
    name: "send_email",
    description: "Send an email",
    schema: z.object({ to: z.string(), subject: z.string(), body: z.string() }),
  }
);

const agent = createAgent({
  model: "anthropic:claude-sonnet-4-5",
  tools: [sendEmail],
  checkpointer: new MemorySaver(),
  middleware: [
    humanInTheLoopMiddleware({
      interruptOn: { send_email: { allowedDecisions: ["approve", "edit", "reject"] } },
    }),
  ],
});
```
</typescript>
</ex-basic-hitl-setup>

<ex-running-with-interrupts>
<python>
Run the agent, detect an interrupt, then resume execution after human approval.

```python
from langgraph.types import Command

config = {"configurable": {"thread_id": "session-1"}}

# Step 1: Agent runs until it needs to call tool
result1 = agent.invoke({
    "messages": [{"role": "user", "content": "Send email to [email protected]"}]
}, config=config)

# Check for interrupt
if "__interrupt__" in result1:
    print(f"Waiting for approval: {result1['__interrupt__']}")

# Step 2: Human approves
result2 = agent.invoke(
    Command(resume={"decisions": [{"type": "approve"}]}),
    config=config
)
```
</python>
<typescript>
Run the agent, detect an interrupt, then resume execution after human approval.

```typescript
import { Command } from "@langchain/langgraph";

const config = { configurable: { thread_id: "session-1" } };

// Step 1: Agent runs until it needs to call tool
const result1 = await agent.invoke({
  messages: [{ role: "user", content: "Send email to [email protected]" }]
}, config);

// Check for interrupt
if (result1.__interrupt__) {
  console.log(`Waiting for approval: ${result1.__interrupt__}`);
}

// Step 2: Human approves
const result2 = await agent.invoke(
  new Command({ resume: { decisions: [{ type: "approve" }] } }),
  config
);
```
</typescript>
</ex-running-with-interrupts>

<ex-editing-tool-arguments>
<python>
Edit the tool arguments before approving when the original values need correction.

```python
# Human edits the arguments — edited_action must include name + args
result2 = agent.invoke(
    Command(resume={
        "decisions": [{
            "type": "edit",
            "edited_action": {
                "name": "send_email",
                "args": {
                    "to": "[email protected]",  # Fixed email
                    "subject": "Project Meeting - Updated",
                    "body": "...",
                },
            },
        }]
    }),
    config=config
)
```
</python>
<typescript>
Edit the tool arguments before approving when the original values need correction.

```typescript
// Human edits the arguments — editedAction must include name + args
const result2 = await agent.invoke(
  new Command({
    resume: {
      decisions: [{
        type: "edit",
        editedAction: {
          name: "send_email",
          args: {
            to: "[email protected]",  // Fixed email
            subject: "Project Meeting - Updated",
            body: "...",
          },
        },
      }]
    }
  }),
  config
);
```
</typescript>
</ex-editing-tool-arguments>

<ex-rejecting-with-feedback>
<python>
Reject a tool call and provide feedback explaining why it was rejected.

```python
# Human rejects
result2 = agent.invoke(
    Command(resume={
        "decisions": [{
            "type": "reject",
            "feedback": "Cannot delete customer data without manager approval",
        }]
    }),
    config=config
)
```
</python>
</ex-rejecting-with-feedback>

<ex-multiple-tools-different-policies>
<python>
Configure different HITL policies for each tool based on risk level.

```python
agent = create_agent(
    model="gpt-4.1",
    tools=[send_email, read_email, delete_email],
    checkpointer=MemorySaver(),
    middleware=[
        HumanInTheLoopMiddleware(
            interrupt_on={
                "send_email": {"allowed_decisions": ["approve", "edit", "reject"]},
                "delete_email": {"allowed_decisions": ["approve", "reject"]},  # No edit
                "read_email": False,  # No HITL for reading
            }
        )
    ],
)
```
</python>
</ex-multiple-tools-different-policies>

<boundaries>
### What You CAN Configure

- Which tools require approval (per-tool policies)
- Allowed decisions per tool (approve, edit, reject)
- Custom middleware hooks: `before_model`, `after_model`, `wrap_tool_call`, `before_agent`, `after_agent`
- Tool-specific middleware (apply only to certain tools)
</boundaries>

---

## Custom Middleware Hooks

Six decorator hooks are available. Two patterns:

- **Wrap hooks** (`wrap_tool_call`, `wrap_model_call`): `(request, handler)` — call `handler(request)` to proceed, or return early to short-circuit.
- **Before/after hooks** (`before_model`, `after_model`, `before_agent`, `after_agent`): `(state, runtime)` — inspect or modify state. Return `None` or a dict of state updates.

<ex-wrap-tool-call>
<python>
`@wrap_tool_call` intercepts tool execution. **Do NOT use `yield`** — it creates a generator and causes `NotImplementedError`.

```python
from langchain.agents.middleware import wrap_tool_call

@wrap_tool_call
def retry_middleware(request, handler):
    for attempt in range(3):
        try:
            return handler(request)
        except Exception:
            if attempt == 2:
                raise

@wrap_tool_call
def guard_middleware(request, handler):
    if request.tool_call["name"] == "dangerous_tool":
        return "This tool is disabled"  # short-circuit
    return handler(request)
```
</python>
<typescript>
`createMiddleware({ wrapToolCall })` intercepts tool execution.

```typescript
import { createMiddleware } from "langchain";

const retryMiddleware = createMiddleware({
  wrapToolCall: async (request, handler) => {
    for (let attempt = 0; attempt < 3; attempt++) {
      try { return await handler(request); }
      catch (e) { if (attempt === 2) throw e; }
    }
  },
});
```
</typescript>
</ex-wrap-tool-call>

<ex-before-after-hooks>
<python>
`before_model` / `after_model` / `before_agent` / `after_agent` all share `(state, runtime)` signature.

```python
from langchain.agents.middleware import before_model, after_model

@before_model
def log_calls(state, runtime):
    print(f"Calling model with {len(state['messages'])} messages")

@after_model
def check_output(state, runtime):
    print(f"Model responded")
```
</python>
<typescript>
All before/after hooks share the same `(state, runtime)` signature via `createMiddleware`.

```typescript
import { createMiddleware } from "langchain";

const loggingMiddleware = createMiddleware({
  beforeModel: (state, runtime) => {
    console.log(`Calling model with ${state.messages.length} messages`);
  },
  afterModel: (state, runtime) => {
    console.log("Model responded");
  },
});
```
</typescript>
</ex-before-after-hooks>

<boundaries>
### What You CANNOT Configure

- Interrupt after tool execution (must be before)
- Skip checkpointer requirement for HITL
</boundaries>

<fix-missing-checkpointer>
<python>
HITL middleware requires a checkpointer to persist state.

```python
# WRONG
agent = create_agent(model="gpt-4.1", tools=[send_email], middleware=[HumanInTheLoopMiddleware({...})])

# CORRECT
agent = create_agent(
    model="gpt-4.1", tools=[send_email],
    checkpointer=MemorySaver(),  # Required
    middleware=[HumanInTheLoopMiddleware({...})]
)
```
</python>
<typescript>
HITL requires a checkpointer to persist state.

```typescript
// WRONG: No checkpointer
const agent = createAgent({
  model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
  middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});

// CORRECT: Add checkpointer
const agent = createAgent({
  model: "anthropic:claude-sonnet-4-5", tools: [sendEmail],
  checkpointer: new MemorySaver(),
  middleware: [humanInTheLoopMiddleware({ interruptOn: { send_email: true } })],
});
```
</typescript>
</fix-missing-checkpointer>

<fix-no-thread-id>
<python>
Always provide thread_id when using HITL to track conversation state.

```python
# WRONG
agent.invoke(input)  # No config!

# CORRECT
agent.invoke(input, config={"configurable": {"thread_id": "user-123"}})
```
</python>
</fix-no-thread-id>

<fix-wrong-resume-syntax>
<python>
Use Command class to resume execution after an interrupt.

```python
# WRONG
agent.invoke({"resume": {"decisions": [...]}})

# CORRECT
from langgraph.types import Command
agent.invoke(Command(resume={"decisions": [{"type": "approve"}]}), config=config)
```
</python>
<typescript>
Use Command class to resume execution after an interrupt.

```typescript
// WRONG
await agent.invoke({ resume: { decisions: [...] } });

// CORRECT
import { Command } from "@langchain/langgraph";
await agent.invoke(new Command({ resume: { decisions: [{ type: "approve" }] } }), config);
```
</typescript>
</fix-wrong-resume-syntax>

© langchain-ai · MIT · langchain-ai/langchain-skills

See also

Project sponsors

Companies that keep this analytics open
Gold sponsor

Impressions, clicks and code copies — open statistics. We keep the numbers public for the same reason NeBlask itself is public. Become a sponsor