EvalLayerTry the demo

Adding commercial guardrails to Claude Agent SDK agents

Published 2026-07-27 · EvalLayer

You add commercial guardrails to Claude Agent SDK agents with a PreToolUse hook: before a matched tool runs, the hook posts the tool input to EvalLayer's /authorize endpoint and translates the decision into the hook's own vocabulary, allow passes the call, rewrite passes it with updated input, require_approval returns an ask, and block returns a deny with the reason. The mapping is unusually clean because the SDK's hook output was designed for exactly these four verbs.

Why Agent SDK deployments create commercial risk

The Claude Agent SDK is Claude Code's harness packaged as a library: the full agent loop with built-in filesystem, Bash, and web tools, plus subagents, sessions, and MCP for everything else. Teams use it to build agents that do real work unattended, and the moment one of those agents gets an email tool, a CRM connector, or a payment-capable MCP server, its output stops being text and starts being commitments. An autonomous agent that drafts and sends in the same loop has no gap between a generous impulse and a delivered promise unless you install one.

The SDK's control story is genuinely strong on access: permission modes, allow and deny rules, hooks at multiple lifecycle points, and a canUseTool callback for interactive approval. All of it governs which tools may run. None of it evaluates what the tool call contains, whether the discount inside the email body is under the cap, whether the reply promises a delivery date, whether the required disclaimer is present. That content-level judgment is the layer you attach, and the PreToolUse hook is the attachment point the SDK provides for it.

The interception point: a PreToolUse hook

PreToolUse hooks are in-process callbacks that fire before a matched tool executes. The hook receives the tool name and input, and its output can permit the call, deny it with a reason, escalate to an ask, and even supply updated input, which is precisely the rewrite path. Field names below follow the documented hook output shape; verify against the SDK's hooks reference for your version.

async function commercialGuardrail(input) {
  if (input.tool_name !== 'send_email') return {};

  const res = await fetch('https://api.evallayer.ai/authorize', {
    method: 'POST',
    headers: {
      'Authorization': 'Bearer ' + process.env.EVALLAYER_KEY,
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      action_type: 'send_email',
      content: input.tool_input.body,
      context: { recipient: input.tool_input.to },
      policy: {
        max_discount_pct: 10,
        commitment_authority_usd: 1000,
        forbidden_commitments: ['delivery_date', 'refund_promise'],
        approved_claims: ['soc2_certified'],
        required_disclaimers: ['pricing_subject_to_change'],
        blocked_topics: []
      }
    })
  });
  const result = await res.json();

  const out = { hookEventName: 'PreToolUse' };
  if (result.decision === 'allow') {
    out.permissionDecision = 'allow';
  } else if (result.decision === 'rewrite') {
    out.permissionDecision = 'allow';
    out.updatedInput = Object.assign({}, input.tool_input, {
      body: result.approved_rewrite
    });
  } else if (result.decision === 'require_approval') {
    out.permissionDecision = 'ask';
    out.permissionDecisionReason =
      'Exceeds commercial authority: ' + result.violations[0].detail;
  } else {
    out.permissionDecision = 'deny';
    out.permissionDecisionReason =
      'Blocked by policy. Ref ' + result.authorization_id;
  }
  return { hookSpecificOutput: out };
}

// In your query options:
// hooks: { PreToolUse: [{ matcher: 'send_email|send_quote|issue_refund',
//                         hooks: [commercialGuardrail] }] }

The four decisions in hook vocabulary

  • allow maps to permission decision allow: the tool runs with its original input.
  • rewrite maps to allow plus updatedInput: the tool runs, but the outbound body is the approved_rewrite. The agent's flow never breaks, and the customer receives the compliant version.
  • require_approval maps to ask: the SDK surfaces the call for a human decision through canUseTool or your UI, with the violation detail as the reason. Your reviewer sees why it was escalated, not just that it was.
  • block maps to deny: the tool never runs, the model sees the reason and can change course, and the authorization_id preserves the attempt.

Latency and matcher scope

Scope the matcher to side-effect tools; Read, Grep, and Bash used for local work should not pay an authorization round trip (gate Bash separately if it can reach the network in ways that send). Deterministic-only policies return in milliseconds, semantic checks in one to two seconds, negligible inside an agent loop where each model turn already costs more. Decide the failure mode in the hook: on timeout, deny for refunds and high-value sends, allow-and-log for low-stakes actions if you prefer availability.

What to log

Put the authorization_id in the permission decision reason (as above) so it lands in session history, and mirror it with the session ID to your own store. The result is an audit trail the SDK cannot produce alone: every consequential tool call carries the policy applied, the violations found, the decision, and the exact text that went out after any rewrite.

Try the mapping

Wire the hook to /demo/authorize (three free checks per day, no key) with a sandbox send tool, or preview decisions in the interactive demo. The decision semantics live in the authorization guide, and policy design covers what belongs in the policy object before you scale the agent's authority.

Frequently asked questions

Hook or canUseTool callback: which layer should hold the check?

The PreToolUse hook. Hooks run for every matched tool call regardless of permission mode, which is the property a policy control needs; canUseTool fires only for calls the earlier layers left unresolved, and is built for interactive session-specific decisions. Put the non-negotiable commercial check in the hook and keep canUseTool for the human prompt experience.

Does the SDK's permission system not already cover this?

It covers a different question. Permission rules and modes decide whether a tool may run at all: can this agent use Bash, can it write files, can it call send_email. The commercial question is whether this particular email, with this discount and this promise, complies with policy. The hook is where the SDK lets you answer content-level questions, and EvalLayer supplies the answer.

How do I test the mapping before giving an agent real send tools?

Point the hook at /demo/authorize, which allows three free checks per day without a key, and run the agent against a sandbox send tool. The interactive demo at /authorize-demo shows the decision card for a quote with planted violations. Production uses a Bearer sk_ key or $0.01 per request over x402.

See the authorization layer live

Paste a message, set a policy, get allow / rewrite / require approval / block in seconds.

Try the demo