Claude Seekho

Claude Agent SDK Deep Dive

Claude Agent SDK gives you Claude Code's agent loop, built-in tools, permissions, sessions and hooks as a library — in Python (claude-agent-sdk) and TypeScript (@anthropic-ai/claude-agent-sdk). If you want the same loop from another language, run the claude -p ... --output-format json subprocess.

19.1 Difference between Claude's 4 tools

You needUse
A Claude Code-like agent in your own Python/TS app, in your own processAgent SDK
Interactive development in the terminalClaude Code CLI
Direct Claude API calls, with your own loop or Tool RunnerClient SDK (anthropic)
Anthropic hosts the agent (cloud sandbox)Managed Agents

19.2 ClaudeAgentOptions — main fields

FieldWhat it does
allowed_tools / disallowed_toolsWhich tools run without asking / never run
permission_modedefault, acceptEdits, plan, dontAsk, bypassPermissions, auto
can_use_toolPermission callback — when it is time to prompt
system_promptCustom or the Claude Code preset
mcp_serversExternal or in-process MCP servers
hooksCallbacks on lifecycle events
agentsProgrammatic subagents (AgentDefinition)
model, max_turns, max_budget_usdModel and limits (stop at the budget)
cwd, setting_sourcesWorking dir; whether to load .claude/ settings, CLAUDE.md, skills
resume / continue_conversationContinue or fork an old session

19.3 Custom tools (in-process MCP server)

from claude_agent_sdk import tool, create_sdk_mcp_server, ClaudeAgentOptions

@tool("get_ticket", "Fetches ticket details by ticket ID", {"ticket_id": str})
async def get_ticket(args):
    data = await crm.fetch(args["ticket_id"])
    return {"content": [{"type": "text", "text": str(data)}]}

crm_server = create_sdk_mcp_server(name="crm", version="1.0.0", tools=[get_ticket])

options = ClaudeAgentOptions(
    mcp_servers={"crm": crm_server},
    allowed_tools=["mcp__crm__get_ticket"],     # name pattern: mcp__<server>__<tool>
)

19.4 Hooks (Python callback)

from claude_agent_sdk import ClaudeSDKClient, ClaudeAgentOptions, HookMatcher

async def protect_env_files(input_data, tool_use_id, context):
    path = input_data["tool_input"].get("file_path", "")
    if path.split("/")[-1].lower() == ".env":
        return {"hookSpecificOutput": {
            "hookEventName": input_data["hook_event_name"],
            "permissionDecision": "deny",                 # allow | deny | ask | defer
            "permissionDecisionReason": ".env files cannot be edited",
        }}
    return {}                                             # {} = allow

options = ClaudeAgentOptions(hooks={
    "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[protect_env_files])]
})
Hook eventWhen
PreToolUse / PostToolUse / PostToolUseFailureBefore a tool (block/modify) / after / on failure
UserPromptSubmitWhen a prompt arrives (add context)
Stop, SubagentStart, SubagentStopAgent / subagent lifecycle
PreCompactBefore compaction (archive the transcript)
PermissionRequest, NotificationCustom approvals, Slack alerts
SessionStart / SessionEnd and moreOnly in the TypeScript SDK (in Python via settings-file hooks)

19.5 Multi-turn client and subagents

import asyncio
from claude_agent_sdk import (ClaudeSDKClient, ClaudeAgentOptions, AgentDefinition,
                              AssistantMessage, TextBlock)

options = ClaudeAgentOptions(
    allowed_tools=["Read", "Grep", "Glob"],
    agents={
        "code-reviewer": AgentDefinition(
            description="Reviews code changes",
            prompt="You are a code reviewer. Report issues in the diff.",
        ),
    },
)

async def main():
    async with ClaudeSDKClient(options=options) as client:
        await client.query("Explain the auth flow in the src/ folder")
        async for msg in client.receive_response():
            if isinstance(msg, AssistantMessage):
                for b in msg.content:
                    if isinstance(b, TextBlock): print(b.text)
        await client.query("Now have the code-reviewer review it")   # the context is remembered
        async for msg in client.receive_response():
            print(msg)

asyncio.run(main())