Skip to main content
ToolApprovalGuard inspects the tool calls an agent proposes while pausing for human approval, before those calls are ever surfaced for review. It can block, log, or fully delegate handling to a custom callback.

Why this middleware exists

Every other Intercept middleware inspects what goes out: the prompt, and the text a human supplies when resolving a paused run. None of them see what comes back in. Tool results, retrieved documents, and conversation history never pass through the middleware pipeline, which is exactly where indirect prompt injection lives. When an agent is manipulated by content Intercept never saw, the damage almost always surfaces as a tool call:
ToolApprovalGuard inspects those proposed calls. It is the first point downstream of that blind spot where the middleware pipeline can still act. By default it is the card number that flags this call, not the address — a mail tool is expected to carry an email address. See secret-like data for why the defaults are drawn that way.
This middleware only acts when a run pauses for approval. Agents whose tools are not approval-gated are unaffected, so adding it is safe even before you adopt human-in-the-loop.

Installation

The package depends on the PII Redactor and the Injection Guard packages, because it reuses their detectors and patterns.

Basic usage

How it differs from the other middleware

This is the first Intercept middleware that acts on the response rather than the prompt, because the tool calls it guards are proposed by the model.

What it checks

Three checks run over every proposed tool call.

Tool policy

An empty allowed_tools list permits every tool. A non-empty list permits only the tools named in it. Anything in denied_tools is always refused.

Secret-like data

The middleware runs the same detectors as PII Redactor, including the Luhn check on card numbers. By default it scans for only three entities: credit_card, api_key and bearer_token. Entities listed in block_entities stop the run regardless of the configured action, matching PII Redactor’s behaviour.
The default entity list is deliberately narrower than PII Redactor’s, and you should think carefully before widening it. In a prompt, an email address is user data worth redacting. In a proposed tool argument it is usually the function signature — send_email(to: ...) cannot work without one.Exfiltration is about destination, not presence, and the destination cannot be judged from the value alone. Adding email, phone, url, ip_address or mac_address will flag legitimate calls made by any agent that owns a mail, SMS or HTTP tool.To control where a tool may send data, use allowed_tools / denied_tools and domain allowlisting inside the tool itself.

Prompt injection patterns

A proposed argument that matches an injection pattern suggests the model was manipulated by content the middleware never saw. Uses the same patterns as Injection Guard. This scan is off by default. Prose a model writes for a human reader routinely contains phrases the patterns match — “You are now subscribed to weekly updates.”, “From now on we will email you every Monday.”, “System: scheduled maintenance at 3pm.” None of those indicate manipulation in that context. Enable it when a proposed argument feeds another model or agent rather than a person:
Either scan can be turned off:

Supported actions

There is deliberately no mutating action. A proposed tool call is part of the paused turn the provider recorded, so rewriting it would desynchronise the run when it is resumed — the same constraint that applies to resumed prompts.
action: 'log' on its own is not observe-only here. Every entity in the default list is also in block_entities, and blocked entities stop the run whatever the action is set to. With the shipped defaults, log and block behave identically for data findings, differing only for denied tools.For a genuinely observe-only rollout, clear block_entities as well:
Review what real traffic produces, then restore block_entities and move to block.

Streaming

On a streamed run, the middleware cannot block. $next() returns before the model has proposed anything, and by the time the completion hook fires the caller has already received the streamed text. The action therefore degrades to logging, recorded as degraded_from, matching the pattern used for tool approval resumes. The security outcome still holds: the tool has not executed. Approval is a separate later call, so a logged-but-unblocked proposal still requires a human to act on it before anything happens.

Blocked runs

The exception names the offending tool call and field, but never the matched value:
Catch it like any other Intercept exception. See handling blocked prompts.

Reading the logs

Findings are logged with a source of pending_approvals:
Matched values are always recorded as SHA-256 hashes. Set log_preview to true to add a short cleartext preview, which is off by default because proposed arguments can carry sensitive data. The type is one of denied_tool, pii, or injection. The detail carries the entity type or the matched pattern.

Custom callback handling

A callback receives the prompt, the response, and the findings, and fully replaces the configured action:
Each finding is an ApprovalFinding exposing toolCallId, tool, type, field, detail, and value. Use reference() for a string that names the location without leaking the matched value.

Configuration

All options may be set globally in config/intercept.php under tool_approval_guard, or per agent through the constructor. Constructor values always win.
See the configuration guide for the full option list.

Limitations

This middleware inspects the tool calls the model proposed. It does not inspect the tool results, retrieved documents, or conversation history that may have influenced them — those never reach the middleware pipeline. It also does not validate arguments against the tool’s schema, or re-scope owner keys such as user_id server-side. Do that in your own tool implementations. Read the security notes for the full picture of what Intercept can and cannot see.