Sandbox your LangGraph agent with IronClaw¶
You built an agent with LangGraph: a stateful graph of nodes, where a ToolNode
(or the prebuilt create_react_agent) executes the tool calls the model emits. That
is a great way to design control flow. The problem starts when it runs somewhere
real: that ToolNode runs your tools in your own process, with your API key in
memory and unrestricted outbound network. One prompt-injected instruction and every
edge in your graph is an edge on your host.
IronClaw runs the same job behind a sealed sandbox instead: no network card, the model key held host-side and never in the agent, and every privileged tool call routed through a human-approval gateway and an audit log.
Runnable example
A one-command LangGraph-to-IronClaw example lives at
examples/integrations/langgraph:
it compiles a real LangGraph StateGraph with a ToolNode whose
sandboxed_shell tool executes inside an IronClaw sandbox, then prints a
PASS/FAIL containment table with every escape attempt blocked. Zero credentials,
just Docker.
Why sandbox this¶
A typical LangGraph agent wires a ToolNode straight to host execution:
from langgraph.prebuilt import ToolNode, create_react_agent
from langchain_community.tools import ShellTool
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(model="gpt-4o", api_key="sk-...") # key lives in this process
agent = create_react_agent(llm, [ShellTool()]) # ShellTool runs on YOUR box
agent.invoke({"messages": [("user", "summarize today's incidents")]})
Three things are true of that graph, and all three are risks:
- The key is in the process. Anything that can read
os.environcan readsk-.... - Every tool node runs with your privileges.
ShellToolexecutes on your host, with your filesystem and network, on any path the graph can reach. - Egress is wide open. The process can reach any host on the internet.
IronClaw closes all three by construction. The
example
keeps your graph exactly as designed — the same ToolNode — but points its
execution at a sealed per-session sandbox.
See it work first (no credentials)¶
git clone https://github.com/IronSecCo/ironclaw.git && cd ironclaw
examples/integrations/langgraph/run.sh
It brings up the offline mock provider (no model key), compiles a LangGraph
StateGraph with a ToolNode, and drives it over one benign task plus a battery of
escape attempts — network exfil, host-filesystem read, Docker-socket takeover — each
reported BLOCKED. The run exits non-zero if any containment expectation fails, so
it doubles as a CI check.
The fix, in your graph¶
Keep the graph; swap the tool. The example's make_sandbox_tool(sandbox) returns an
ordinary LangChain StructuredTool, so it drops into a ToolNode or
create_react_agent with no other change:
from ironclaw_sandbox import IronClawSandbox
from ironclaw_tool import make_sandbox_tool
from langgraph.prebuilt import ToolNode
with IronClawSandbox() as sandbox:
tool = make_sandbox_tool(sandbox) # runs inside the sealed sandbox, not your host
tool_node = ToolNode([tool]) # ... the exact node your graph already uses
Every command the model routes through that node now executes inside the box as an
unprivileged uid, with network=none and no host mounts.
What you gained¶
- The key left the agent. It lives host-side and is injected per request; a compromised graph has nothing to steal.
network=noneby default. The sandbox has no NIC. The only egress is the audited model-proxy socket, plus whatever hosts you explicitly allowlist.- Every tool node is contained. No matter which path your graph takes, the tool call lands in the sandbox, and privileged actions flow through a human-approval gateway into the audit log.
Same stateful graph you designed in LangGraph. A perimeter it never had.