Skip to main content
The Claude Agent SDK (claude-agent-sdk) runs the agentic loop in a Node.js subprocess managed by the upstream package. The egisai patch hooks into the two SDK-exposed extension points — PreToolUse and PostToolUse — so policy enforcement happens at the same boundary the subprocess does, before any tool runs and before Claude is shown any tool result. This is the only subprocess-loop framework the SDK governs with full pre-execution and pre-feedback enforcement; everything else either runs in-process (Tier 1 / Tier 2) or runs on managed infrastructure with no hook point (AWS Bedrock Agents — see AWS Bedrock).

Supported surface

Both sync and async usage are covered.

Install

Use

No code changes are required to activate governance — egisai injects its hooks alongside any hooks you supply, and user-provided hooks continue to run.

What gets enforced

The patch inserts a small dispatcher under both PreToolUse and PostToolUse matcher slots. On each turn:
  1. Input phase — the prompt text is evaluated against your request-phase policies (PII detection, regex denylist, model allow-list, size cap, intent judge). A block refuses the turn before the subprocess starts.
  2. PreToolUse — for each tool the model wants to call, the SDK runs output-side policies that target tool dispatches (deny_tool_call, deny_mcp_call, deny_bash_command, deny_db_query, deny_financial_action, semantic_guard with targets: ["tool_calls"]). A block returns permissionDecision: deny to the CLI; the tool never runs.
  3. PostToolUse — for each tool that ran, the SDK evaluates output-side policies against the tool’s response text (pii_scan, deny_output_regex, semantic_guard). A sanitize verdict masks the result in place via the SDK’s updatedToolOutput / updatedMCPToolOutput substitution contract; a block replaces the result with a refusal payload — in both cases the model never sees the raw bytes.
  4. End-of-turn — the aggregated assistant response (text + structured tool calls) is evaluated one more time.
Tool result shapes handled by PostToolUse:
  • MCP-shaped responses{"content": [{"type": "text", "text": "..."}]}. PII in text parts is masked or replaced; non-text parts (images, audio) pass through untouched.
  • Raw-string responses — typical of Bash, Read, Write, … Replaced via updatedToolOutput.
  • Opaque-dict responses — JSON-serialized for scanning, then replaced via updatedToolOutput.

Audit fidelity

A tool_call step row is emitted only when a tool’s result was non-allow (sanitized, blocked, or substituted). Allow turns stay clean — no step inflation. The audit row’s request_text preview is sampled from the post-sanitization payload, never the raw value. The privacy contract — raw PII never leaves the SDK boundary — applies the same way it does to every other integration.

Access inventory

The patch also feeds the dashboard’s per-agent Access tab from three declaration surfaces, automatically:
  • mcp_servers — one MCP-server entry per configured server (name + transport label only — never the command, URL, or credentials). In-process SDK servers built with create_sdk_mcp_server additionally contribute full tool metadata (name, PII-sanitized description, parameter names, schema hash).
  • allowed_tools — built-in tool names and fully-qualified mcp__… names granted to the session. Permission specifiers such as Bash(git:*) and server-level grants (mcp__<server>) are understood.
  • The CLI’s init system message — the toolset the subprocess actually loaded, filtered through allowed_tools.
allowed_tools is treated as the grant boundary: when it is set, CLI built-ins that load into the session but sit outside the grant (TaskStop, CronDelete, …) are permission-gated, not reachable, and are not declared — so the Access tab shows the agent’s effective access instead of the CLI’s full built-in catalog. If the model invokes a gated tool anyway, the platform records it from the audit stream as observed access. With no allowed_tools configured, the full loaded toolset is declared.

Backwards compatibility

On older claude-agent-sdk versions that don’t expose the hooks field, the patch falls back to advisory mode and the audit rows are honestly labeled. The PermissionError / refusal stub your code sees on on_block="raise" / on_block="stub" is unchanged.

When a call is blocked

By default a blocked call raises PermissionError from inside the SDK’s receive_response() iterator. With on_block="stub", the iterator yields a refusal-shaped message and continues so the agent loop can keep running.

What’s next

Agent frameworks

Other agent frameworks supported by the SDK.

Two-phase governance

The phase model the policy engine applies on every turn.