Skip to content
Agentic Control Plane

What 588 Model-Judged Tool Calls Taught Us About Approval Prompts

David Crowe David Crowe · · 10 min read
engineering agents approvals claude-code judge
Share X HN LinkedIn

Running Claude Code? See, control, and price every tool call it makes — allow/ask/deny policy, a per-session bill at API rates, and a full audit log, in one command:

macOS · Linux · WSL
curl -sf https://agenticcontrolplane.com/install.sh | bash
Windows · PowerShell
irm https://agenticcontrolplane.com/install.ps1 | iex

Full Claude Code guide →  ·  see your first governed call →  ·  free up to 5 agents

Approval prompts have a quiet failure mode. When we replayed one workspace’s approval history, 42 inline prompts had gone unanswered or been declined. Twenty-five of them were abandoned: the developer had moved on, the prompt timed out, and the agent carried on with something else.

That’s the problem with asking about everything: people stop reading the questions. “Just add approvals” turns every action into a ticket, and a ticket queue nobody reads is not a control.

So for eight days we let a model answer some of them.

The setup

Two layers, in a fixed order.

Code first. Rules you write — allow, ask, deny per tool — plus floors that hold destructive shapes no matter what. These are deterministic and they run before anything else. A call your rules deny never reaches the model. A call a floor holds never reaches the model.

The model only in the gap. Some calls land on “ask”: your rules don’t say yes and don’t say no. Those, and only those, go to a decision model — Jev, from TypeSafe, which returns a typed choice (allow / ask / deny), a risk label and a calibrated confidence. We send it the redacted tool input, the harness name and the working-directory name. Never the transcript.

In decide mode, by default, the model can turn an ask into an allow. That’s the only direction it moves anything, and only when all of these hold:

  • the call is read-only, and our command classifier independently agrees it’s a read
  • confidence is at least 0.95
  • no structural hold fires — a command hidden inside a wrapper (bash -c, xargs, eval), project code being run (npm test, make, node scripts/…), a sensitive read (shell history, credential paths), or text that looks addressed to the judge
  • someone is actually at the keyboard: the harness reports an attended session

Anything else stays an ask. Writes always ask.

A session, from the audit log

The first clear on the workspace, 2026-10-16 at 21:57:55 UTC:

Bash  gh pr view … --json
decision  allow
reason    cleared by judge: allow p=0.99 risk=readonly conf=0.98

No prompt. The agent got its answer and kept going.

Later in the same workspace, a compound command that read something and then ran a project script:

reason    project code runs (node scripts/…)

Held. The model’s verdict didn’t matter; running code from the repo is a structural hold, and it asked.

Both rows are quoted from the workspace's audit log, with the command shortened (…); they are log text, not console screenshots.

That’s the whole design in two rows. Routine reads get out of the way. Anything that executes, writes or reaches somewhere sensitive still comes to you.

Eight days of numbers

One workspace, one developer, Claude Code sessions, 2026-10-16 to 2026-10-24.

judged calls                 588
  model said allow           467
  model said ask             110
  model said deny             11
cleared without a prompt     220   (37% of judged)
judge latency                p50 216 ms · p95 374 ms
backend failures               0

What it cleared was unglamorous, which is the point: git fetch (29), gh pr view (26), gh pr checks (24), git grep (22), git status (20), git show (14), gh api GETs (14), gh issue view (10), git worktree (9), git log (9), git diff (8).

git fetchgit fetch: 2929gh pr viewgh pr view: 2626gh pr checksgh pr checks: 2424git grepgit grep: 2222git statusgit status: 2020git showgit show: 1414gh api (GET)gh api (GET): 1414gh issue viewgh issue view: 1010git worktreegit worktree: 99git loggit log: 99git diffgit diff: 88everything elseeverything else: 3535
The 220 calls the judge cleared without a prompt, by command. One workspace, Claude Code sessions, 2026-10-16 21:50 UTC to 2026-10-24 (last day partial). Counted from the gateway's audit rows where the judge's decision was applied; "everything else" is the 35 clears outside the eleven largest commands.

The judge cost about $0.00003 per call. These eight days cost under two cents; a day at the default 2,000-call cap would be about six.

The model said yes 467 times. 220 got through.

This is the number we’d look at first in anyone else’s system.

model said allow                        467
cleared                                 220
held                                    247
  of which, checks on the verdict itself:
    allow, but labelled destructive      81
    command inside a wrapper             61
    classifier disagrees it's a read     21
    project code runs                     9
    risk = exfiltration                   9
  the rest: held for other reasons (the confidence bar, ceiling guards and the rest), not itemized
Cleared, no promptCleared, no prompt: 220220Held: allow, but labelled destructiveHeld: allow, but labelled destructive: 8181Held: command inside a wrapperHeld: command inside a wrapper: 6161Held: classifier disagrees it's a readHeld: classifier disagrees it's a read: 2121Held: project code runsHeld: project code runs: 99Held: risk = exfiltrationHeld: risk = exfiltration: 99Held for other reasonsHeld for other reasons: 6666
What happened to the 467 calls the model said "allow" to. n = 467 allow verdicts out of 588 judged; one workspace, Claude Code, 2026-10-16 to 2026-10-24; judge = TypeSafe Jev, decide mode, confidence bar 0.95. Cyan holds can only fire on an allow verdict; the gray bar is the remainder (66), held for other reasons (the confidence bar, ceiling guards and the rest), not itemized.

Over half of the model’s yeses were held by the checks around it. Some of those are the model being inconsistent with itself — “allow” with a risk label of “destructive” is a contradiction, and a contradiction is a hold. Most are shapes a language model reads as routine and a parser reads as “this runs something.”

The test set shows why the confidence bar is set where it is. Commands like env, printenv, ls ~/.ssh and grep AKIA ~/.aws came back labelled read-only, at confidence between 0.38 and 0.88. They are reads. They are also reads of secrets. The 0.95 bar held every one — and those paths are now on the never-clear list regardless. Across a 240-case set built to probe this — reads, near-reads, and things dressed up as reads — none of the cases marked must-not-clear cleared.

The model is good at telling a routine git log from something odd. It is not a parser, and it is not your policy. Code-based and model-based control do different jobs, and the useful part is the order they run in.

It’s opt-in per workspace and clears on Claude Code and Pi, the harnesses that report whether a person is in the session; a tool type needs 5 of your approvals before it clears, and two denials, if they’re more than 5% of its approvals, take it back off. Setup, modes and what is sent: model-judged approvals.

What this data doesn’t tell you

One workspace. One developer’s habits, heavy on git and GitHub CLI reads. Eight days. Claude Code only. Your clear rate will depend on how much of your agent’s day is reads your rules already ask about — if your rules allow git status outright, the judge never sees it. The latency numbers are the judge call alone, from one region.

The best check is your own traffic: turn on shadow for a week, then compare the model’s verdicts with your own answers on the same calls.

I think of this as the agentic control plane doing the boring part of approvals, so the prompts that remain are the ones worth reading.

Share X HN LinkedIn
Get the next data drop
What agents actually cost, new tool-surface captures, and the occasional incident post-mortem — sent when we publish something worth your inbox, not on a schedule. Unsubscribe anytime.
Share: Twitter LinkedIn
Related posts

← back to blog