SpotAgentIQ

Human-in-the-loop approval for AI agents, without babysitting

Asking a person before an agent acts is the most reliable safety control we have. It only works if approvals are rare enough that people actually read them.

Updated 7 November 202610 minute read

Why "approve everything" fails

The obvious first move is to make the agent ask before every action. Within a week the approver is clicking yes on fifty requests a day without reading them, and the control has become a delay with a checkbox attached. Security teams call this alert fatigue, and it's why the design question is not "should a human be in the loop?" but "for which calls?".

A useful target: an approver should see few enough requests that each one gets real attention, and every request should be for something they'd genuinely want to know about.

Sorting actions into allow, hold and deny

Two questions sort almost every tool call. Can it be undone? Does it leave the company?

Kind of actionExamplesDefault
Readsget_customer, search_orders, read_file, list_issuesAllow
Internal, reversible writesadd a ticket comment, update a draft, create a branchAllow, and log
Anything sent to peoplesend_email, post_message to a public channel, SMSHold
Moneyrefunds, charges, payouts, creditsHold above a threshold
Destructivedelete_*, drop, close account, revokeHold
Permissions and configadd user, change role, rotate key, edit DNSHold
Never acceptablecard numbers or secrets in arguments, writes on a replicaDeny

Deny is underused. If you would reject a call every single time, don't make a person do it. Deny it with a clear reason and let the agent explain to its user.

What a good approval request shows

An approver needs to make the decision from the request alone, quickly. That means:

  • The exact arguments, not a summary written by the agent. Prompt injection hides in the details: a to address on the wrong domain, an amount with an extra zero.
  • Why it was held: the rule and its reason, such as "Outbound communication".
  • Who it's for: which agent, acting for which person.
  • Secrets masked, so the approval channel doesn't become a new leak.
  • A way to say why not. A rejection note that reaches the agent lets it recover ("the manager said refunds over $500 need a ticket") instead of trying again.

And when nobody answers, the call should time out as a rejection, never as an approval.

Keeping the volume down

Use thresholds, not tool names alone

"Hold all refunds" becomes "hold refunds of $100 or more" with one argument pattern. Most of the volume is small and routine; the risk is concentrated in the tail.

{ "match": "stripe/create_refund", "action": "require_approval", "if_args_match": "\"amount\":\\s*[0-9]{5,}", "reason": "Refund of $100 or more" },
{ "match": "stripe/create_refund", "action": "allow" }

Allow known-safe destinations

Email to your own domain, messages to the team's private channel, writes to a scratch table. Allow those explicitly above the hold rule.

Promote boring approvals into rules

Once a week, read the log for calls that were approved every time with no edits. Each one is a candidate for an allow rule. Each rejection is a candidate for a deny rule. The approval queue should shrink over the first month.

Route to the right person

The finance lead should see refunds; the on-call engineer should see infrastructure changes. Sending everything to one shared channel is how approvals get ignored.

What the agent experiences

From the agent's side, a held call is just a slow tool call. The request stays open until there's a decision. A few practical details matter:

  • Timeouts. Many MCP clients give up on a tool call after a few minutes. Set the approval timeout shorter than the client's, so the agent gets a clean "timed out waiting for approval" instead of a broken connection.
  • Progress. If the client sends a progress token, SpotAgentIQ sends a progress notification every 15 seconds while the call waits. That keeps the connection alive and lets the agent tell its user it's waiting.
  • Readable refusals. Rejections and denials come back as tool results with a reason, so the agent can explain the situation rather than retry the same call.

Setting it up

With SpotAgentIQ in front of your MCP servers, the table above becomes a dozen lines:

"approval_timeout": "5m",
"default_action": "allow",
"rules": [
  { "match": "*",            "action": "deny", "if_args_match": "\\b\\d{13,19}\\b", "reason": "Card number in arguments" },
  { "match": "send_email",   "action": "allow", "if_args_match": "\"to\":\\s*\"[^\"@]+@yourcompany\\.com\"" },
  { "match": "send_*",       "action": "require_approval", "reason": "Outbound communication" },
  { "match": "post_message", "action": "require_approval", "reason": "Posts to Slack" },
  { "match": "*refund*",     "action": "require_approval", "reason": "Moves money" },
  { "match": "delete_*",     "action": "require_approval", "reason": "Destructive" }
]

Held calls show up in the local inbox and in Slack. Approving straight from Slack, routing by tool to different approvers, and SSO are what we're building in SpotAgentIQ Cloud.