Agent Assurance for your AI Deployment

Test how your agents actually behave across workflows, tools, and actions. Catch failures and vulnerabilities before they ship.

npm install -g @testmuai/rook@latest

Start Free Trial
Agent TestingAI TestingAI

How to Test Claude Agent SDK Agents: Tools, Hooks, Subagents and Evidence

How to test Claude Agent SDK agents: hermetic runs, hooks that record every tool call, subagent checks, guardrail tests, and evidence of what the run changed.

Published on:

An agent built on the Claude Agent SDK can end a run with a ResultMessage whose subtype is success, a tidy summary saying the config was updated, and an untouched config file on disk. The result message tells you the agent loop finished, while the file system tells you whether the work happened, and most first test suites only read the first one.

The SDK already exposes what a better test needs: hooks that see every tool call, message fields that mark subagent activity, and result fields that record permission denials and cost. This guide turns them into a Claude Agent SDK testing plan built around evidence, with offline tests I ran against @anthropic-ai/claude-agent-sdk 0.3.309 and their real output.

The same idea, grading what the agent did instead of what it said, is what TestMu AI Agent Assurance automates for Claude Code-style agents, and the last section shows how it fits beside your own tests.

Key Takeaways

Claude Agent SDK testing works best at the level of evidence: run the agent hermetically, record every tool call with PreToolUse and PostToolUse hooks, assert on the files and records it changed, and treat each subagent as its own unit. A success subtype on the result message only tells you the agent loop finished.

  • Hermetic runs: Set settingSources explicitly, run each test in a temporary git checkout, and cap maxTurns and maxBudgetUsd, because default query() options load every filesystem settings source, including the user and project settings files.
  • Hooks as a recorder: A PreToolUse and PostToolUse pair keyed by tool_use_id logs every tool call, and agent_id marks calls made inside a subagent, which turns each run into evidence a test can assert on.
  • The Bash gap: A Write|Edit hook never sees a file written by a Bash command, so check that every write-capable tool the agent may use is guarded, sandboxed, or removed with disallowed_tools.
  • Subagents as units: Only a subagent's final message reaches the parent, and file checkpointing doesn't track subagent edits, so assert on each subagent's own effects through git and the hook log.
  • State over summary: Check git diff and the records the agent touched instead of the result text. TestMu AI Agent Assurance discovers .claude/agents definitions and MCP servers on its own and grades each criterion against what the run changed.

The Test Surface of a Claude Agent SDK Agent

Claude Agent SDK testing means checking an agent built on Anthropic's SDK at the level of what it did: the tool calls it made, the files and records it changed, which subagent did the work, and whether its guardrails held. Each part of the SDK gives you a different kind of evidence.

SurfaceWhat can go wrongEvidence to checkHow to test it
Built-in toolsRead, Write, Edit, and Bash touch the wrong path or run the wrong commandHook log, git diffLive run in a temporary checkout
Custom toolsHandler logic writes twice, accepts bad input, or reports success on failureHandler return value, the record it writesCall the handler offline
HooksA guard misses a path or a tool, or blocks a valid callReturned permission decisionCall the hook offline with synthetic input
PermissionsA mode approves tools you meant to blockpermission_denials on the resultLive run with the production options
SubagentsNo delegation, delegation to the wrong agent, or a subagent using tools it shouldn'tAgent tool_use blocks, parent_tool_use_id, hook agent_idLive run plus the hook log
Final resultThe summary claims work the run didn't doState after the run, compared with the claimAssertions on files and records

The last row is where tool-using agents fail most. In Reason Less, Verify More (arXiv, July 2026), the authors report that on a budget agent, 78% of observed failures were silent wrong-state failures with no tool error, such as a booking cancelled or a claim acted on without verification. No exception and no error flag fired, so only a check on the resulting state could catch them.

Make Every Test Run Hermetic

A test that passes on your laptop and fails in CI usually picked up something from the machine. The options below decide what the agent can see and do, so set each one explicitly in tests.

Option (TypeScript / Python)Default behaviorTest setting
settingSources / setting_sourcesWhen omitted, all filesystem sources load (user, project, and local settings), which matches the CLI[] for isolation, or ['project'] when the agent needs its own .claude files
cwdThe current process directory in TypeScriptA fresh temporary git checkout per test, so git diff shows every change
permissionMode / permission_modedefault: calls that need approval and match no allow rule go to your callbackThe production mode, or dontAsk with an explicit allowed tools list
maxTurns, maxBudgetUsdNo spend limit by defaultTight caps, with a test that fails on the error_max_turns or error_max_budget_usd subtypes
CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH3 layers of subagents below the main agentThe value production uses, so nesting in tests matches real runs

Don't reach for bypassPermissions to make a flaky test pass. The SDK's permissions documentation warns that "Setting allowed_tools=["Read"] alongside permission_mode="bypassPermissions" still approves every tool, including Bash, Write, and Edit," so a test run in that mode exercises a tool surface production never has. The guide to AI agent permission testing covers how to check that scope holds once it's configured.

Note

Note: TestMu AI Agent Assurance runs from your terminal as rook and needs a TestMu AI account for credits. Models run in the TestMu AI controller, so no provider keys sit on your machine. Create your TestMu AI account

Record Every Tool Call With Hooks

Hooks are the cheapest evidence source the SDK gives you. The hooks documentation says the tool use ID "correlates PreToolUse and PostToolUse events for the same tool call," and that agent_id and agent_type are populated when a hook fires inside a subagent. A hook without a matcher runs for every event of its type, which is what a recorder needs.

// Pre/PostToolUse hook: record every call, keyed by tool_use_id, with the subagent that made it.
export function recordEvidence(log) {
  return async (input) => {
    log.push({
      event: input.hook_event_name,
      tool: input.tool_name,
      id: input.tool_use_id,
      agent: input.agent_id ?? 'main',
      ...(input.tool_response !== undefined && { response: input.tool_response }),
    });
    return {};
  };
}

// Register it with no matcher, beside a guard scoped to file-writing tools.
const evidence = [];
const options = {
  settingSources: [],
  hooks: {
    PreToolUse: [
      { matcher: 'Write|Edit|NotebookEdit', hooks: [guardWrites(workspace)] },
      { hooks: [recordEvidence(evidence)] },
    ],
    PostToolUse: [{ hooks: [recordEvidence(evidence)] }],
  },
};

These hook events carry the most weight in a test suite:

Hook eventPythonTypeScriptUse in tests
PreToolUseYesYesRecord intent, deny out-of-scope calls
PostToolUseYesYesRecord the tool response for each call
PostToolUseFailureYesYesFail the test when a write tool errored and the result still claims success
SubagentStart, SubagentStopYesYesCount delegations; SubagentStop includes the subagent's transcript path
PermissionDeniedNoYesLog denials as they happen in auto mode

Two behaviors from the same docs shape how you read the log. Matching hooks run in parallel and the most restrictive permission decision wins, so a recorder never weakens a guard, and hooks may not fire when the agent hits its max_turns limit, so a run that ends there needs its own assertion. For hook setup inside Claude Code itself, see Claude Code hooks.

Unit Test Hooks and Custom Tools Offline

A hook is an async function that takes an input object, a tool use ID, and a context with an abort signal, so a test can call it directly. A custom tool built with tool() returns a definition whose handler you can call the same way. Neither needs a model, which keeps these tests free and fast.

import path from 'node:path';
import { z } from 'zod';
import { tool } from '@anthropic-ai/claude-agent-sdk';

// Tools this guard covers. Keep the list next to the agent's allowedTools.
export const GUARDED_WRITE_TOOLS = ['Write', 'Edit', 'NotebookEdit'];

// PreToolUse hook: deny any file write outside the workspace.
export function guardWrites(workspace) {
  const root = path.resolve(workspace);
  return async (input) => {
    const target = input.tool_input?.file_path ?? input.tool_input?.notebook_path;
    if (!target) return {};
    const resolved = path.resolve(input.cwd, target);
    if (resolved === root || resolved.startsWith(root + path.sep)) return {};
    return {
      hookSpecificOutput: {
        hookEventName: 'PreToolUse',
        permissionDecision: 'deny',
        permissionDecisionReason: 'write outside ' + root + ' blocked',
      },
    };
  };
}

// A custom tool exposed through an in-process SDK MCP server.
export function refundTool(ledger) {
  return tool(
    'issue_refund',
    'Refund an order in full.',
    { orderId: z.string(), amount: z.number().positive() },
    async ({ orderId, amount }) => {
      if (ledger.some((r) => r.orderId === orderId)) {
        return { content: [{ type: 'text', text: orderId + ' was already refunded' }], isError: true };
      }
      ledger.push({ orderId, amount });
      return { content: [{ type: 'text', text: 'refund ' + orderId + ' recorded' }] };
    },
  );
}

The tests below call the guard, the recorder, and the tool handler with synthetic inputs shaped like the SDK's PreToolUseHookInput. The last test checks the configuration itself: every write-capable tool the agent is allowed must be covered by the guard.

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { guardWrites, recordEvidence, refundTool, GUARDED_WRITE_TOOLS } from './agent-hooks.mjs';

const base = { session_id: 's1', transcript_path: '/tmp/t.jsonl', cwd: '/repo' };
const signal = new AbortController().signal;

test('write guard denies a file outside the workspace', async () => {
  const hook = guardWrites('/repo/workspace');
  const out = await hook({ ...base, hook_event_name: 'PreToolUse', tool_name: 'Write',
    tool_input: { file_path: '../.env', content: 'X=1' }, tool_use_id: 'tu_1' }, 'tu_1', { signal });
  assert.equal(out.hookSpecificOutput.permissionDecision, 'deny');
});

test('write guard allows a file inside the workspace', async () => {
  const hook = guardWrites('/repo/workspace');
  const out = await hook({ ...base, cwd: '/repo/workspace', hook_event_name: 'PreToolUse', tool_name: 'Edit',
    tool_input: { file_path: 'src/app.js' }, tool_use_id: 'tu_2' }, 'tu_2', { signal });
  assert.deepEqual(out, {});
});

test('evidence log pairs Pre and Post events and attributes the subagent', async () => {
  const log = [];
  const hook = recordEvidence(log);
  await hook({ ...base, hook_event_name: 'PreToolUse', tool_name: 'Bash', tool_input: { command: 'npm test' },
    tool_use_id: 'tu_3', agent_id: 'agent_7', agent_type: 'test-runner' }, 'tu_3', { signal });
  await hook({ ...base, hook_event_name: 'PostToolUse', tool_name: 'Bash', tool_input: { command: 'npm test' },
    tool_response: { stdout: '12 passing' }, tool_use_id: 'tu_3', agent_id: 'agent_7' }, 'tu_3', { signal });
  assert.deepEqual(log.map((e) => [e.event, e.id, e.agent]), [['PreToolUse', 'tu_3', 'agent_7'], ['PostToolUse', 'tu_3', 'agent_7']]);
});

test('refund tool refuses a second refund for the same order', async () => {
  const ledger = [];
  const { handler } = refundTool(ledger);
  await handler({ orderId: 'A-1184', amount: 42 }, {});
  const second = await handler({ orderId: 'A-1184', amount: 42 }, {});
  assert.equal(second.isError, true);
  assert.equal(ledger.length, 1);
});

test('every write-capable tool the agent is allowed is guarded', () => {
  const allowedTools = ['Read', 'Write', 'Edit', 'Bash', 'mcp__billing__issue_refund'];
  const writeCapable = allowedTools.filter((t) => ['Write', 'Edit', 'NotebookEdit', 'Bash'].includes(t));
  const unguarded = writeCapable.filter((t) => !GUARDED_WRITE_TOOLS.includes(t));
  assert.deepEqual(unguarded, [], 'unguarded write path: ' + unguarded.join(', '));
});

Running node --test agent-hooks.test.mjs produced this output, trimmed of stack-trace lines:

✔ write guard denies a file outside the workspace (2.0402ms)
✔ write guard allows a file inside the workspace (1.7866ms)
✔ evidence log pairs Pre and Post events and attributes the subagent (0.8216ms)
✔ refund tool refuses a second refund for the same order (4.0326ms)
✖ every write-capable tool the agent is allowed is guarded (2.451ms)
ℹ tests 5
ℹ pass 4
ℹ fail 1

✖ every write-capable tool the agent is allowed is guarded (2.451ms)
  AssertionError [ERR_ASSERTION]: unguarded write path: Bash
  + actual - expected

  + [
  +   'Bash'
  + ]
  - []

The failure is the point. The hooks docs state that with a Write|Edit matcher, "If Claude writes .env with a Bash command, the hook doesn't run," and that matchers only match tool names, not file paths. Close the gap by removing Bash with disallowed_tools, enabling the SDK sandbox for Bash writes, or adding a Bash guard that inspects the command.

Keep most checks at this level and save live runs for behavior only the model controls. Even Anthropic's own end-to-end tests for the Python SDK "make actual API calls to Claude, which incur costs," though the README says the complete suite "should cost less than $0.10 to run." The same scripted-model approach exists in another SDK, covered in how to test OpenAI Agents SDK agents.

Test Subagents as Separate Units

A subagent works in its own context, and the subagents documentation says intermediate tool calls and results stay inside it, so "only its final message returns to the parent." A test that reads the parent's summary sees a description of the subagent's work, never the work itself.

What to checkHow to check it
Delegation happenedFind tool_use blocks named Agent, and match Task too, since versions before Claude Code v2.1.63 used that name
Which work belongs to itMessages from inside a subagent carry parent_tool_use_id, and hook inputs carry agent_id
Tool restrictions holdGive the subagent a tools list and assert the hook log shows no other tool under its agent_id; a tool left out isn't in its session at all
Its effectsAssert on the files and records the subagent was meant to change, using git diff and the hook log
Fan-out and spendConcurrency defaults to 20 subagents; cap the whole tree with maxBudgetUsd and fail on error_max_budget_usd

Subagents also break a common cleanup shortcut. The SDK's file checkpointing page says subagent edits aren't tracked and that rewinding doesn't undo what a Bash command changed, so a rewind can leave state behind for the next test. Use a fresh checkout per test, and see Claude Code subagents for how definitions and tool lists are set up.

Assert on What Changed, Not on the Result Message

The ResultMessage is useful, but each field answers a narrower question than it seems to:

FieldWhat it tells youWhat it can't tell you
subtypesuccess, or why the loop stopped: error_max_turns, error_max_budget_usd, error_during_executionWhether the task was done
resultThe agent's final textWhether its claims are true
permission_denialsTool calls the permission flow refused during the runCalls that went through and did the wrong thing
num_turns, total_cost_usdHow long and how expensive the run wasWhether the spend produced the outcome

Pair every test with a state assertion: git diff --stat on the checkout, a read of the record the agent wrote, or the request log of a stubbed API. Asking the agent for evidence helps too. In Failure-Transparent Agents (arXiv, September 2026), false-success rates were 22.8% under a baseline policy and 0.8% with a structured evidence contract, so having the agent return the IDs and paths it touched gives your assertions something specific to check.

Testing Claude Agent SDK Agents With Agent Assurance

TestMu AI Agent Assurance tests how your agents actually behave across workflows, tools, and actions, and catches failures and vulnerabilities before they ship. Claude Code-style agents are its home ground: discovery reads .claude/agents/*.md definitions, skills, and MCP server configs without a model, and for each MCP server it connects and asks which tools the server really has instead of trusting the config.

CheckYour SDK testsAgent Assurance adds
ScenariosWritten by hand for the paths you thought ofDerived from the agent's code and definitions: functional and adversarial by default, cost and quality on request
Adversarial casesWhatever injections you writePrompt injection, jailbreak, data exfiltration, PII leakage, and policy violation, mapped to OWASP and MITRE ATLAS
EvidenceHook logs and git diff you wire upFiles under the paths the profile declares, artifacts, tool calls against the declared tools, and read-only checks through an MCP tool you approve
Unverifiable checksOften silentReported as Unable to Verify, kept out of the pass rate
Agent Assurance agent summary for a release agent discovered from src/agents/release.mjs with high confidence, showing 2 features, 28 scenarios, 5 tools, 3 runs, a 4% last pass rate, 66% coverage, the source materials, and the HTTP invoke profile

In this Agent Assurance workspace, discovery traced a release agent to src/agents/release.mjs, listed 5 tools and 2 features, and generated 28 scenarios. The invoke profile on the right is the one input it can't derive, and for an SDK agent it's usually a command that passes each scenario's goal to your script. A first run in the terminal UI:

npm install -g @testmuai/rook
rook login
rook
/project
/explore .
/agent
/profile add staging
/generate
/scenarios list
/sync
/run
/ui

Agent Assurance is pre-alpha, so commands and stored file formats can still change. It invokes the agent for real and can't roll its writes back, so point it at staging.

Start a 14-day free trial of Agent Assurance by TestMu AI, with 4,000 credits, to test how your AI agents behave across workflows, tools, and actions

Next Steps

Begin Claude Agent SDK testing with the coverage test. List every tool your agent is allowed, mark the ones that can write, and fail the build when any of them sits outside a guard, the sandbox, or disallowed_tools; then add the hook recorder and one state assertion per write tool.

When you want generated scenarios against the real agent, follow the Agent Assurance quickstart and point it at your staging checkout.

Author

...

Samyak Goyal

Blogs: 34

  • Linkedin

Samyak Goyal is a Senior Member of Technical Staff at TestMu AI engineering Kane CLI, the command-line tool that runs browser automation from the terminal, where a flow described in natural language executes in a real Chrome browser and returns pass or fail with shareable proof. He is a backend engineer with 4+ years of experience, previously an SDE at Innovaccer, where he built APIs, introduced Kafka, and cut deployment from weeks to hours. Samyak also builds multi-agent systems, skill-orchestration frameworks, and a personal copilot that indexes 200+ microservice repositories.

Reviewer

...

Anubhav Singhmaar

Reviewer

  • Linkedin

Anubhav Singhmaar is an AI Product Manager at TestMu AI driving Kane CLI, the command-line tool that brings browser automation to the terminal, turning natural-language flows into runs in a real Chrome browser that return pass or fail with shareable proof. He owns the roadmap and prioritization and works with engineering to ship developer-facing features. Before TestMu AI, he spent over four years at Sprinklr owning enterprise voice AI across APAC and EMEA. A mechanical engineer turned product manager, he grounds guidance in real QA workflows.

Add to Google preferred sources

Summarise with AI

Copied to Clipboard!
...

3000+ Browsers. One Platform.

See exactly how your site performs everywhere.

Try it free
...

Write Tests in Plain English with KaneAI

Create, debug, and evolve tests using natural language.

Try for free

Claude Agent SDK Testing FAQs

Did you find this page helpful?

More Related Blogs

TestMu AI forEnterprise

Get access to solutions built on Enterprise
grade security, privacy, & compliance

  • Advanced access controls
  • Advanced data retention rules
  • Advanced Local Testing
  • Premium Support options
  • Early access to beta features
  • Private Slack Channel
  • Unlimited Manual Accessibility DevTools Tests