Screening for agent tool calls, and for what comes back.

agent-chaperone sits between an AI agent and its tools. It screens a call before it runs, and a result before the agent reads it. Every judgment comes back as a probability, every threshold is a number in a policy file rather than a prompt, and every decision is written to a local log with the numbers that produced it.

It starts in shadow mode, which blocks nothing. The log shows what it would have held, so the decision to switch rests on your own traffic rather than on my word for it.

npm install -g agent-chaperone

Screening needs an API key for the model backend. Without one the deterministic rules still run, which is the allow and deny lists and the secret patterns, and every judgment records that no model was asked.

14:03:11 call   forward  read_file  destructive 0.02 exfiltration 0.01
14:03:11 call   forward  write_file (would have held it)  destructive 0.91 severity 2.0
14:03:11 result WITHHELD fetch  instructs_reader 0.97

That is shadow mode. Nothing was stopped. The middle line is the kind of thing that changes if you switch.

The measured results, including the misses, and the source.

What it is not #

Read this part first.

Two ways in #

The first is a proxy. One line in a client's MCP configuration wraps any server, and every tool call and result crossing it is screened. Everything after -- is the server that would have run anyway.

{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": ["-y", "agent-chaperone", "--",
               "npx", "-y", "@modelcontextprotocol/server-filesystem", "."]
    }
  }
}

The second exists because a proxy sees MCP traffic and nothing else. It does not see the shell, the file edits or the web fetches a client runs itself, and on the clients people actually use those are where most of the damage lives. Two commands read a client's hook payload and answer on stdout, against the same policy file and the same log.

agent-chaperone hook pre     # before the client runs a tool
agent-chaperone hook post    # after it returns, before the model reads it

The exact hook entries are in the hooks guide, which also says what the hooks do not see.

How it decides #

Deterministic rules run first: allow and deny lists, secret patterns, dangerous shell forms, and hidden-text detection. What survives goes to a small battery of typed judgments, each of which comes back as a probability rather than a sentence.

There are two tiers for a result, and the difference matters when reading the numbers.

Three ways to check any of this without taking my word for it: run it in shadow mode and read your own log, rebuild the published numbers from the recorded model responses in the repository, or run it with no API key at all and get the deterministic rules on their own.

What it caught, and what it missed#

One run of jev-1.13.0 on 2026-09-19, 1,942 requests, costing $0.061 at the published price. Counts are at the annotate threshold the package ships with, which is 0.5.

Counts rather than rates, because a rate hides how many items it was measured on.
SetItemsTo catchCaughtMissedFlagged in error
InjecAgent tool responses1,3941,0548482069
BIPIA email250200165350
Benign documents that discuss injection63none--7
Hand-labeled tool calls100514921

The largest set is also the weakest. Of 1,054 injected tool results, 206 were not flagged at all, and of the 848 that were, 359 crossed the bar to be withheld rather than annotated. Lowering the threshold to 0.3 turns those 206 misses into 54, and the benign flags from 7 into 46. That trade is yours to set, which is the reason the number lives in a file.

Latency added to each screened call was 405 ms at the median and 876 ms at the 95th percentile, measured from a laptop. The full picture, with every threshold and the misses named.

Shadow to enforce #

Run it in shadow for a while and read the log. The entries marked as would-have are the list of things that change when you switch, which makes the switch a decision rather than a leap.

agent-chaperone log

If you disagree with a line, move the threshold rather than the mode. When the log stops surprising you, set mode: enforce in the policy file.

When a call is held #

In enforce and strict, a call that crosses a threshold does not run. This is what the agent is handed:

agent-chaperone held this call to write_file for the user to approve: it looks
like it changes something in a way that is hard to undo. The possible damage was
rated high. Nothing ran. The user can allow it by running:
agent-chaperone approve b2c4e6a8f0

The agent has not been told the call failed, and it has not been told to find another route. It has been told that a person is deciding. You then look at what it wanted to do and allow it if you agree. The token names that one call rather than that tool, so agreeing to a write to one path does not release a write to another, and it expires.