diff --git a/99 Daily/2026-04-17.md b/99 Daily/2026-04-17.md index 7ee032a..a36277c 100644 --- a/99 Daily/2026-04-17.md +++ b/99 Daily/2026-04-17.md @@ -170,3 +170,6 @@ tags: [daily] - 12:51 | `memory-compiler` - **Asked:** Compile a new article about Agent SDK hosting into the wiki knowledge base. - **Done:** Filed structured article as `agent-sdk/hosting-production.md` with deployment patterns, system requirements, and provider options. +- 12:52 (1min) | `memory-compiler` + - **Asked:** Asked to compile a new wiki article about agent loop mechanics into the knowledge base structure. + - **Done:** Created structured wiki article documenting agent loop lifecycle, turn mechanics, message types, tools, and execution rules. diff --git a/raw/How the agent loop works.md b/raw/_processed/How the agent loop works.md similarity index 100% rename from raw/How the agent loop works.md rename to raw/_processed/How the agent loop works.md diff --git a/wiki/_master-index.md b/wiki/_master-index.md index 4bd90e9..7195843 100644 --- a/wiki/_master-index.md +++ b/wiki/_master-index.md @@ -30,7 +30,7 @@ This 3-hop pattern works for hundreds of articles without vector search. | [[wiki/web-agency/_index\|web-agency/]] | AI-assisted website building & selling: Claude Code, Nanobanana 2, Kling, LaunchPath MCP | 1 | | [[wiki/dotfiles/_index\|dotfiles/]] | Linux terminal ricing: Kitty, Fish, WezTerm CLI, modern Rust CLI tools, LazyVim, unified themes, Tabby | 9 | -| [[wiki/agent-sdk/_index\|agent-sdk/]] | Claude Agent SDK (formerly Claude Code SDK) — build autonomous AI agents in Python and TypeScript | 12 | +| [[wiki/agent-sdk/_index\|agent-sdk/]] | Claude Agent SDK (formerly Claude Code SDK) — build autonomous AI agents in Python and TypeScript | 13 | | [[wiki/llm-models/_index\|llm-models/]] | OpenAI model catalog — GPT-5.x, o-series reasoning, audio/realtime, embeddings, moderation | 1 | | [[wiki/claude-code/_index\|claude-code/]] | Claude Code product docs — install, capabilities, surfaces, MCP, hooks, scheduling, multi-agent, plugins, skills, error recovery | 7 | diff --git a/wiki/agent-sdk/_index.md b/wiki/agent-sdk/_index.md index dce71cd..63aaeda 100644 --- a/wiki/agent-sdk/_index.md +++ b/wiki/agent-sdk/_index.md @@ -26,3 +26,4 @@ Build production AI agents using the same tools, agent loop, and context managem | [[wiki/agent-sdk/custom-tools\|custom-tools]] | Define custom tools with @tool/@tool(), in-process MCP server, error handling, images, resources, annotations | raw/Give Claude custom tools.md | 2026-04-17 | | [[wiki/agent-sdk/user-input-approvals\|user-input-approvals]] | canUseTool callback: tool approvals, AskUserQuestion clarifying questions, allow/deny/modify, Python streaming workaround | raw/Handle approvals and user input.md | 2026-04-17 | | [[wiki/agent-sdk/hosting-production\|hosting-production]] | Production hosting: container requirements, 4 deployment patterns (ephemeral/long-running/hybrid/multi-agent), sandbox providers, ops notes | raw/Hosting the Agent SDK.md | 2026-04-17 | +| [[wiki/agent-sdk/agent-loop\|agent-loop]] | Message lifecycle, turns, tool execution, context window, compaction, sessions, result subtypes, hooks | raw/How the agent loop works.md | 2026-04-17 | diff --git a/wiki/agent-sdk/sdk-hooks.md b/wiki/agent-sdk/sdk-hooks.md new file mode 100644 index 0000000..37d5903 --- /dev/null +++ b/wiki/agent-sdk/sdk-hooks.md @@ -0,0 +1,218 @@ +--- +title: "Intercept and Control Agent Behavior with SDK Hooks" +aliases: [sdk-hooks, agent-hooks, hook-callbacks] +tags: [agent-sdk, hooks, python, typescript, security, permissions] +sources: [raw/Intercept and control agent behavior with hooks.md] +created: 2026-04-17 +updated: 2026-04-17 +--- + +# Intercept and Control Agent Behavior with SDK Hooks + +SDK hooks are async callback functions registered in `ClaudeAgentOptions` that fire at key execution points. Unlike [[wiki/agent-sdk/hooks-guide|shell command hooks]] (defined in settings files), these run in-process alongside your agent code. + +## What Hooks Can Do + +- **Block** dangerous operations before they execute (`permissionDecision: "deny"`) +- **Modify** tool inputs before execution (`updatedInput`) +- **Auto-approve** specific tools without user prompts (`permissionDecision: "allow"`) +- **Log / audit** every tool call for compliance or debugging +- **Inject context** into the conversation the model sees (`systemMessage`) +- **Track lifecycle** — subagent start/stop, session boundaries, compaction + +## Available Hook Events + +| Hook Event | Python | TypeScript | Trigger | +|---|---|---|---| +| `PreToolUse` | Yes | Yes | Before tool executes — can block or modify | +| `PostToolUse` | Yes | Yes | After tool succeeds | +| `PostToolUseFailure` | Yes | Yes | After tool fails | +| `UserPromptSubmit` | Yes | Yes | When user prompt is submitted | +| `Stop` | Yes | Yes | Agent execution stops | +| `SubagentStart` | Yes | Yes | Subagent initializes | +| `SubagentStop` | Yes | Yes | Subagent completes | +| `PreCompact` | Yes | Yes | Before conversation compaction | +| `PermissionRequest` | Yes | Yes | Before permission dialog | +| `Notification` | Yes | Yes | Agent status messages | +| `SessionStart` | **No** | Yes | Session init (Python: use shell hooks instead) | +| `SessionEnd` | **No** | Yes | Session ends | +| `TaskCompleted` | **No** | Yes | Background task finishes | +| `ConfigChange` | **No** | Yes | Config file changes | +| `WorktreeCreate` | **No** | Yes | Git worktree created | +| `WorktreeRemove` | **No** | Yes | Git worktree removed | + +## Configuration + +```python +from claude_agent_sdk import ClaudeAgentOptions, HookMatcher + +options = ClaudeAgentOptions( + hooks={ + "PreToolUse": [HookMatcher(matcher="Write|Edit", hooks=[my_callback])] + } +) +``` + +- **Keys** = hook event names (case-sensitive: `PreToolUse` not `preToolUse`) +- **Values** = list of `HookMatcher` objects + +### Matchers + +`HookMatcher(matcher="...", hooks=[...], timeout=60)` + +- `matcher` — regex against tool name (e.g. `"Write|Edit"`, `"^mcp__"`, `"Bash"`) +- Omit `matcher` to match all occurrences of the event +- Matchers filter by **tool name only** — filter by file path inside the callback + +**Built-in tool names:** `Bash`, `Read`, `Write`, `Edit`, `Glob`, `Grep`, `WebFetch`, `Agent` +**MCP tools:** `mcp____` (e.g. `mcp__playwright__browser_click`) + +## Callback Inputs & Outputs + +### Inputs (3 args) +1. `input_data` — typed dict with event details; all share `session_id`, `cwd`, `hook_event_name` +2. `tool_use_id` — correlates `PreToolUse` ↔ `PostToolUse` for the same call +3. `context` — `AbortSignal` in TypeScript; reserved in Python + +### Output Fields + +| Field | Level | Effect | +|---|---|---| +| `systemMessage` | top-level | Injects text into conversation (model sees it) | +| `continue` (`continue_` in Python) | top-level | Stop agent after this hook if `False` | +| `hookSpecificOutput.permissionDecision` | nested | `"allow"` / `"deny"` / `"ask"` | +| `hookSpecificOutput.permissionDecisionReason` | nested | Reason string shown to model | +| `hookSpecificOutput.updatedInput` | nested | Replacement tool input (must also set `allow`) | +| `hookSpecificOutput.additionalContext` | nested | Appended to tool result (PostToolUse only) | + +Return `{}` to allow the operation unchanged. + +**Priority:** `deny` > `ask` > `allow` — if any hook denies, the operation is blocked. + +### Async (fire-and-forget) output + +For side effects (logging, webhooks) that shouldn't block the agent: + +```python +async def async_hook(input_data, tool_use_id, context): + asyncio.create_task(send_to_logging_service(input_data)) + return {"async_": True, "asyncTimeout": 30000} +``` + +Cannot block, modify, or inject context — agent has already moved on. + +## Common Patterns + +### Block dangerous paths + +```python +async def protect_env_files(input_data, tool_use_id, context): + file_path = input_data["tool_input"].get("file_path", "") + if file_path.split("/")[-1] == ".env": + return { + "hookSpecificOutput": { + "hookEventName": input_data["hook_event_name"], + "permissionDecision": "deny", + "permissionDecisionReason": "Cannot modify .env files", + } + } + return {} +``` + +### Redirect writes to sandbox + +```python +async def redirect_to_sandbox(input_data, tool_use_id, context): + if input_data["tool_name"] == "Write": + original = input_data["tool_input"].get("file_path", "") + return { + "hookSpecificOutput": { + "hookEventName": input_data["hook_event_name"], + "permissionDecision": "allow", # required with updatedInput + "updatedInput": {**input_data["tool_input"], "file_path": f"/sandbox{original}"}, + } + } + return {} +``` + +### Auto-approve read-only tools + +```python +async def auto_approve_read_only(input_data, tool_use_id, context): + if input_data["tool_name"] in ["Read", "Glob", "Grep"]: + return { + "hookSpecificOutput": { + "hookEventName": input_data["hook_event_name"], + "permissionDecision": "allow", + } + } + return {} +``` + +### Chain hooks (single responsibility per hook) + +```python +options = ClaudeAgentOptions( + hooks={ + "PreToolUse": [ + HookMatcher(hooks=[rate_limiter]), + HookMatcher(hooks=[authorization_check]), + HookMatcher(hooks=[input_sanitizer]), + HookMatcher(hooks=[audit_logger]), + ] + } +) +``` + +Hooks execute in array order. Chain for complex logic, keep each focused. + +### Forward notifications to Slack + +```python +async def notification_handler(input_data, tool_use_id, context): + try: + await asyncio.to_thread(_send_slack, input_data.get("message", "")) + except Exception as e: + print(f"Slack failed: {e}") # swallow — don't interrupt agent + return {} + +options = ClaudeAgentOptions( + hooks={"Notification": [HookMatcher(hooks=[notification_handler])]} +) +``` + +Notification types: `permission_prompt`, `idle_prompt`, `auth_success`, `elicitation_dialog`. + +## Troubleshooting + +| Problem | Fix | +|---|---| +| Hook not firing | Check event name case; verify matcher regex; check `max_turns` not hit | +| Matcher too broad | Empty matcher fires on ALL tools — always use `matcher=` when possible | +| `updatedInput` not applied | Must be inside `hookSpecificOutput`, must also include `permissionDecision: "allow"`, must include `hookEventName` | +| `SessionStart`/`SessionEnd` missing in Python | Not in Python SDK — use shell hooks via `setting_sources=["project"]` | +| Subagent permission storms | Use `PreToolUse` to auto-approve tools for subagents; they don't inherit parent permissions | +| Recursive loops | `UserPromptSubmit` hooks spawning subagents can loop — check `agent_id` to guard | + +## Key Takeaways + +- SDK hooks (`HookMatcher` + async callbacks) run in-process; shell command hooks run in `.claude/settings.json` — they complement each other +- `PreToolUse` is the workhorse: block, modify inputs, or auto-approve +- Always return `{}` to pass through; never mutate `tool_input` in place — return a new object +- `deny` beats `ask` beats `allow` — multiple hooks compose safely +- `SessionStart`/`SessionEnd` are TypeScript-only in the SDK; Python gets them via shell hooks + `setting_sources` +- Use async output (`async_: True`) for logging/webhook side effects to avoid blocking the agent +- Matchers are tool-name regex only — filter by file path inside the callback body + +## Related Articles + +- [[wiki/agent-sdk/hooks-guide|hooks-guide]] — shell command hooks: all 25 events, exit codes, JSON output format +- [[wiki/agent-sdk/configure-permissions|configure-permissions]] — permission modes, allow/deny rules, subagent inheritance +- [[wiki/agent-sdk/python-api-reference|python-api-reference]] — full Python hook input/output type definitions +- [[wiki/agent-sdk/typescript-api-reference|typescript-api-reference]] — full TypeScript hook input/output type definitions +- [[wiki/agent-sdk/custom-tools|custom-tools]] — build tools to extend what the agent can call +- [[wiki/agent-sdk/user-input-approvals|user-input-approvals]] — `canUseTool` callback for interactive approval flows + +--- + +*Source: raw/Intercept and control agent behavior with hooks.md*