Skip to content
Back to skills

Integrate Arcjet Guard Claude Agent Sdk Py

ASecurity

Integrate Arcjet Guard into the Python Claude Agent SDK — wrap authored @tool with guard_tool, and use guard_hooks for UserPromptSubmit inbound plus PreToolUse on unwrapped built-ins / MCP. Use when asked to add Arcjet to claude-agent-sdk, rate limit those tools, screen inbound prompts, or block prompt injection / PII. This is Python claude-agent-sdk, not the JS adapter and not Claude Managed Agents hosted sessions.

  • 4 stars
  • 0 votes
  • 0 copies
  • 2 views
  • Added September 23, 2026
ai-agentspythonbashfastapigitapi

Works with

  • cli
  • api
  • mcp

Security analysis

A96/100
  • mediumInstalls packages at runtime which could introduce malicious dependencies

Pro shows the line behind each finding and how to fix it

Scanned September 23, 2026

npx -y skills add arcjet/skills --skill integrate-arcjet-guard-claude-agent-sdk-py --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Integrate Arcjet Guard Claude Agent Sdk Py?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Integrate Arcjet Guard Claude Agent Sdk Py
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/arcjet-integrate-arcjet-guard-claude-agent-sdk-py/badge)](https://www.skillsdirectory.com/skills/arcjet-integrate-arcjet-guard-claude-agent-sdk-py)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: integrate-arcjet-guard-claude-agent-sdk-py
description: Integrate Arcjet Guard into the Python Claude Agent SDK — wrap authored @tool with guard_tool, and use guard_hooks for UserPromptSubmit inbound plus PreToolUse on unwrapped built-ins / MCP. Use when asked to add Arcjet to claude-agent-sdk, rate limit those tools, screen inbound prompts, or block prompt injection / PII. This is Python claude-agent-sdk, not the JS adapter and not Claude Managed Agents hosted sessions.
license: Apache-2.0
compatibility: Requires Python >= 3.10 and official claude-agent-sdk>=0.2.127,<1 via arcjet[claude-agent-sdk] (safe extra, no chromadb). Requires PyPI arcjet 1.1.0.
metadata:
  author: arcjet
  type: core
  library: arcjet
---

# Integrate Arcjet Guard into the Python Claude Agent SDK

`arcjet.guard.claude_agent_sdk` wraps the agent's existing Arcjet client.
It never talks to the Arcjet API itself. Shared Guard fundamentals
(client, rules, labels, decisions, capture, registration) live in
[../arcjet/references/guards_python.md](../arcjet/references/guards_python.md).
Load that reference for anything that is not Claude Agent SDK-specific.

Official `claude-agent-sdk>=0.2.127,<1` only — not the JS
`@anthropic-ai/claude-agent-sdk` adapter
(`@arcjet/guard/claude-agent-sdk/v0`, docs
https://docs.arcjet.com/guards/claude-agent-sdk/), not Vercel AI, not
community forks, and not Claude Managed Agents hosted
`client.beta.sessions` (`arcjet.guard.claude_managed_agents`). Importing
`arcjet.guard.claude_agent_sdk` does not load LangChain, CrewAI, or the
JS adapter. The extra is safe (no chromadb).

Exports: `guard_tool`, `guard_hooks`, `claude_agent_context`. Authored
`@tool` + `create_sdk_mcp_server` for tools you own; `guard_hooks` for
inbound `UserPromptSubmit` and unwrapped built-ins / MCP `PreToolUse`.
There is no inbound helper and no `guard_can_use_tool`.

Three surfaces, one decision rule:

- **An authored `@tool`** → `guard_tool`. Denial is JSON-in-content +
  `is_error: True`. Python does **not** forward `structuredContent`
  (that is the JS adapter). Do **not** raise.
- **Inbound text** → `guard_hooks` `UserPromptSubmit` (`decision:
  "block"`). That is the only place a turn can be declined before the
  model reads the prompt.
- **Built-ins / unwrapped MCP** → `guard_hooks` `PreToolUse`
  (`permissionDecision: "deny"`). `PostToolUse` is capture only.
- **Correlation** → `claude_agent_context` reads a caller-owned UUID
  `session_id`. It never mints.

Docs: https://docs.arcjet.com/guards/claude-agent-sdk/.

## Unwrapped tools deny on `PreToolUse` via `guard_hooks`

Built-ins (Bash, Write, …) and MCP tools you did not pass through
`guard_tool` have no authored handler. `PreToolUse` is the only deny for
those. `PostToolUse` cannot undo a tool that already ran. List every
`guard_tool` wrapper in `exclude` or each authored tool is guarded twice
(two round trips, two quota units). Entries match the reported name:
pass `{"server": "support", "name": "lookup_order"}` for an authored MCP
tool (it resolves to `mcp__support__lookup_order`) and a bare string for
a built-in such as `"Bash"`. A bare authored name deliberately does not
match every server's tool of that name.

## Authored `@tool` denial is JSON-in-content + `is_error: True`

`guard_tool` wraps the `@tool` definition so the handler never runs on
`DENY` (or unevaluated Guard under the default `on_guard_error="deny"`).
The model receives the `ArcjetDenialResult` as JSON text on `content`
with `is_error: True`. Do **not** set `structuredContent` (JS only). Do
**not** raise: a throw is a raw exception; omitting `is_error` looks
like success. Same fail-closed default as
[#196](https://github.com/arcjet/arcjet-py/pull/196): only `"allow"`
fails open; a `DENY` always blocks. Core `guard()` still fails open.

## `can_use_tool` is not a policy gate

`can_use_tool` is human-in-the-loop. `allowed_tools`, allow rules, and
`bypassPermissions` / `acceptEdits` skip it. Same trap as CrewAI
`human_input`, JS `canUseTool`, LangGraph `interrupt()`, and OpenAI
Agents `needs_approval`. There is no inbound helper — screen prompt
injection on `guard_hooks` `UserPromptSubmit`.

## 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 `guard_tool`. Built-ins and
   unwrapped MCP get `guard_hooks` `PreToolUse`.
2. What **limits**? (e.g. "10 lookups/min per user" → `TokenBucket`.)
3. Who is the **user** for metadata — an opaque user/tenant ID (never PII)?
   Default: none. Session id is the correlation id, not the user. The
   Claude SDK requires `session_id` to be a UUID and allows a given id
   to be created only once — later turns use `resume`.
4. Is an Arcjet outage unacceptable? Every helper defaults to
   `on_guard_error="deny"`. Ask explicitly about inbound
   `UserPromptSubmit`: failing closed there means the agent stops
   answering, so `"allow"` is a routine and legitimate choice at that
   one call site.

## The things readers get wrong

1. **This is not the JS adapter and not Claude Managed Agents.** Do not
   import `@arcjet/guard/claude-agent-sdk/v0` or
   `arcjet.guard.claude_managed_agents`.
2. **There is no inbound helper.** Screen on `UserPromptSubmit` via
   `guard_hooks`.
3. **`can_use_tool` is HITL, not policy.**
4. **Python does not forward `structuredContent`.** Authored deny is
   JSON-in-content + `is_error: True`.
5. **`session_id` is a UUID, and only once.** Later turns: `resume`.
   `claude_agent_context` never mints.
6. **List every `guard_tool` wrapper in `exclude`.** Missing exclude
   double-calls Guard.
7. **Key rate limits on the authenticated caller**, not a model-supplied
   order id. Hand `query` / `create_sdk_mcp_server` the copy
   `guard_tool` returns.
8. **Do not hand-wrap every tool with raw `guard()`.**
9. **Isolation needs both `setting_sources=[]` and
   `strict_mcp_config=True`.** One without the other still exposes an
   unguarded path.
10. **Two distinct ids.** `ClaudeAgentOptions.session_id` is a unique
    UUID per run (`resume` later). The Guard `session_id` is a
    long-lived actor id. A missing decision is not a denial.

## Step 1: Install and find the guard client

Requires PyPI `arcjet` **1.1.0** (the `arcjet[claude-agent-sdk]` extra is not in 1.0.0):

```bash
pip install "arcjet[claude-agent-sdk]"
```

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

```python
import os
from arcjet.guard import launch_arcjet

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
```

## Step 2: Gate authored tools — `guard_tool`

```python
from claude_agent_sdk import ClaudeAgentOptions, create_sdk_mcp_server, query, tool
from arcjet.guard import DetectPromptInjection, TokenBucket, launch_arcjet
from arcjet.guard.claude_agent_sdk import guard_hooks, guard_tool

aj = launch_arcjet(key=os.environ["ARCJET_KEY"])
lookup_limit = TokenBucket(
    label="order.looked-up",
    bucket="lookups",
    refill_rate=10,
    interval_seconds=60,
    max_tokens=10,
)
mcp_limit = TokenBucket(
    label="mcp.invoked",
    bucket="mcp-access",
    refill_rate=20,
    interval_seconds=60,
    max_tokens=20,
)
inbound = DetectPromptInjection()
user_id = authenticated_user_id
# Caller-owned UUID for this conversation. Later turns: resume=session_id.
session_id = conversation_id

@tool("lookup_order", "Look up an order by number", {"order_id": str})
async def lookup_order(args: dict) -> dict:
    return {
        "content": [{"type": "text", "text": f"{args['order_id']}: shipped"}],
    }

lookup_order = guard_tool(
    guard=aj,
    tool=lookup_order,
    action="order.looked-up",
    rules=[lookup_limit(key=user_id, requested=1)],
    on_guard_error="deny",
)
# can_use_tool=... is HITL — not this policy gate
```

## Step 3: Screen inbound and gate unwrapped tools — `guard_hooks`

```python
async for message in query(
    prompt=user_text,
    options=ClaudeAgentOptions(
        session_id=session_id,  # later turns: resume=session_id instead
        mcp_servers={
            "support": create_sdk_mcp_server(name="support", tools=[lookup_order]),
        },
        hooks=guard_hooks(
            guard=aj,
            session_id=session_id,
            exclude=[{"server": "support", "name": "lookup_order"}],
            inbound={
                "action": "message.received",
                "rules": lambda ctx: [inbound(ctx["prompt"])],
            },
            action="mcp.invoked",
            rules=lambda ctx: [mcp_limit(key=user_id, requested=1)],
            on_guard_error="deny",
        ),
    ),
):
    pass
```

## Step 4: Correlation

`claude_agent_context(session_id=session_id)` reads that same
caller-owned UUID (hook `session_id` first, then `options.session_id`).
It never mints. Pass the id you already have into `guard_hooks` and
`ClaudeAgentOptions` — do not derive a new one per turn. The Claude SDK
also requires `session_id` to be a UUID and allows a given id to be
created only once — later turns use `resume`.

## Verify the integration

1. `python -m py_compile` (or the project's type-check) passes.
2. Exercise inbound PI (`UserPromptSubmit` block), an authored-tool deny
   (JSON-in-content + `is_error: True`, no `structuredContent`), a
   built-in / unwrapped MCP deny (`PreToolUse`), a rate limit, and
   fail-closed (an unreachable guard). Confirm a wrapped tool produces
   **one** guard decision per invocation — a second decision under the
   `PreToolUse` action means a missing `exclude` entry.
3. Confirm in the Arcjet Console / CLI that decisions share the
   caller-owned UUID `session_id`.
4. Manual E2E with a real `ARCJET_KEY` is still-to-verify until you run it.

The example `fastapi-claude-agent-sdk-guard` stays with Runtime — do not
invent a new example name. Do not add an example in this skills repo.

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

View all in ai-agents →

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.