agentleFS
arcjetarcjet/skills/integrate-arcjet-guard-claude-agent-sdk-py/SKILL.md

integrate-arcjet-guard-claude-agent-sdk-py

Skill4 starsChanged 6 months agoApache-2.0View on GitHub

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.

  • Installs packages

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 2 are distinctive, and 2 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈2

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 2

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Ask only what you cannot infer from the code.

    line 82
  2. Very specific:

    Default: none. Session id is the correlation id, not the user.

    line 89
  3. Very specific:

    This is not the JS adapter and not Claude Managed Agents: Do not import @arcjet/guard/claude-agent-sdk/v0 or …

    line 100
  4. Distinctive:

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

    line 131

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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. 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: 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):

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

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

import os
from arcjet.guard import launch_arcjet

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

Step 2: Gate authored tools — guard_tool

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

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.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.