Originally published on michael-kaminski.io.
Most of what gets called a human approval gate is latency wearing a safety vest.
I run 47 agent skills against my own accounts — brokerage, bank, email, calendar, four publishing platforms, a booking system, a database. Two of them stop at the moment of action and wait for me. The other 45 do not, and that is deliberate.
The reason is arithmetic. A gate that fires on every run is not a safety mechanism. It is a queue, and I am the only worker on it.
Approve-everything breaks at the second decimal place
My publishing target is 21 posts a week across five platforms that carry publish: true. Add three weekday engagement slots — 15 — and ten job applications on each weekday the apply job runs, which is 50.
That is 86 outward actions in a normal week, before anything ad hoc. Every one of them is visible to somebody who is not me.
If each needs a human yes, the human is the throughput ceiling. And a person clearing 86 approvals a week is not reading them by Wednesday.
That is the failure nobody writes down. Approval theater is worse than no gate, because it converts a decision into a signature and moves the responsibility onto the signature.
Three mechanisms, ranked by human attention per unit of protection
| Mechanism | Attention per run | Holds while I am asleep | Count in my fleet |
|---|---|---|---|
| Capability removal | zero | yes | 19 of 47 |
| Kill gate | zero until it fires, then zero | yes | 6 rules, all platforms |
| Human approval gate | one decision, every time | no | 2 of 47 |
The ordering is the whole argument. Approval is the most expensive tool in the set and the only one that stops working the moment I am unavailable, so it goes last and it goes narrow.
1. Remove the capability. You cannot approve what the agent cannot do.
My brokerage reader is one sentence long at the top: read-only, never trades or moves money. The margin-eligibility skill that sizes a levered book is advisory only and never transmits orders. The network-map crawler never sends mail, never writes to a CRM, and never moves money.
Nineteen of the 47 skills carry a line like that. It is not a gate. It is a missing verb.
The cost per run is zero, and it is the only mechanism on the list that is still holding at 3 a.m. Every one of those skills runs on a schedule, and most of those schedules fire while I am asleep. An approval gate on a scheduled job is a job that does not run.
2. Kill gates, not pause gates
The publishing config says it plainly: no human approval step per post. Then it lists six rules that do not pause for a human — they kill the item.
Never invent a fact. Never publish an employer-internal metric. Score at least 24 of 30 on the rubric with no dimension at 2 or below. No topic repeat inside 90 days. Clean secret scan before any public push. Read back what was posted before reporting success.
A pause gate costs attention twice — once when it fires and once when it does not, because I still have to look to find out which. A kill gate costs nothing until it fires, and when it fires there is nothing to review. The item is gone, not waiting.
The cost, named: kill gates throw away good work. A draft that scores 23 dies, and some of those were fine. I pay it because a skipped slot beats a bad post, and because the alternative is a review queue I would stop reading by Thursday and then trust anyway.
3. Reserve approval for the irreversible, not the important
Two skills stop and wait. The first drafts an email to my CPA and will not send it — it requires two independent conditions before it will even draft, then shows me the draft and waits. The second does a full shopping run: finds the product, logs in, fills the cart, fills the shipping form, and stops one click short. Its spec says it never clicks the final purchase button and everything else is fair game.
Now the contrast that makes the rule legible. A third skill cancels my son's tutoring appointments, and its spec says the opposite: do not stop at the confirmation, cancellations are intentional, finish it.
Cancelling a session matters more to his week than a $60 order matters to mine. It gets no gate. The order gets one.
The variable is not importance. It is whether undo exists and what undo costs. A rebook is four clicks in the same system. A purchase is a chargeback, a return window, and an email to a stranger.
Blast radius is the axis, and it is a function, not a label
The clearest version of this in my fleet is the tuning job that edits my own instruction files every day. Low-risk edits — tone, formatting — auto-apply. High-risk edits — new sections, new behavior rules — get proposed and wait.
Same skill, same file, two different defaults, chosen per edit. That is the general form: gate on the blast radius of the action, not on the name of the tool.
It also stacks with a gate I have written up separately: no edit stays without a statistically significant improvement — Welch's t-test, p < 0.10, at least 5% — against a 14-day rolling baseline. Approval decides whether the change lands. Statistics decide whether it survives.
The alternative I keep getting offered, and why I do not take it
The standard counter is: skip the gates, log everything, review after. Cheap to build, nothing blocks, and you keep a full audit trail.
It works for reversible actions and it is strictly worse for the rest. A log tells you an irreversible thing happened. It does not un-happen it.
The two skills I gate cost me maybe four decisions a month between them. Reviewing a log of 86 weekly actions well enough to catch the one bad send costs more than that every Monday, and it catches it after the send.
Logging is how you learn what to gate. It is not the gate.
Where mine is broken
There is a shared kill switch. Three skills of the 47 check it.
One touch state/STOP should halt the fleet. It halts the job applier, the cost-out scan, and the CPA drafter. That is it.
Of the six skills that take an action a third party can see — posting, emailing, applying, booking, writing to a database — five never look at that file. The one that posts to three social accounts has no kill switch at all.
And the classification lives in prose. Twenty-eight of the 47 carry no restriction line of any kind. Nothing enforces the taxonomy. It is a sentence I wrote in a markdown file on the day I built the skill.
The failure mode is not dramatic, which is why it will happen. A skill written read-only on a Tuesday grows a write path on a Thursday. The sentence at the top still says read-only. It is now false, and nothing in the system notices.
What I would build next
Not more approval. Make the class machine-checkable.
Declare the capability in frontmatter. Have the runner refuse an outbound write from a skill that declared itself read-only. Move the kill-switch check out of 47 individual good intentions and into the thing that executes them.
That moves the gate from what I wrote to what runs. Until then the honest summary is that I have real teeth on about 40% of the fleet and prose everywhere else, and I know which 40%.
If you are gating agent actions at your shop, I would trade notes on where you drew the reversibility line — and on what it cost you the first time you drew it wrong.
Top comments (7)
Putting approval last works only if capability removal is observable, and that is the part your next step quietly assumes. Moving enforcement into the runner closes the gap between a skill and its declaration. It opens a second one, between the runner and its own enforcement path.
Take the cheap implementation. A wrapper placed earlier on PATH than the real binary intercepts the outbound call. That interception is environmental. A scheduled job does not inherit an interactive login shell's environment, so the wrapper directory can fall off PATH under the scheduler while everything else looks identical. Every skill then runs unwrapped. Its frontmatter still says read-only, and a health check that reads declarations reports success, because it is auditing labels rather than testing interception.
Silence proves nothing here. An enforcement layer that is only ever exercised by real violations has no evidence it is loaded, since zero blocked calls is equally consistent with perfect compliance and with no enforcement at all. A schedule-driven fleet makes that ambiguity worse, because the bypass window is exactly the unattended one.
So the runner needs a negative fixture, and it has to fail closed on the enforcement layer rather than only on the action. A canary skill declares itself read-only and deliberately attempts a forbidden outbound write, through the same execution path and the same scheduler environment as ordinary skills, aimed at a controlled destination so a failed guard cannot touch a real account. The refusal has to be an explicit policy refusal. A timeout or a missing executable produces the same non-write and means nothing. If that refusal does not appear, no other skill is admitted that boot.
One fixture per outbound route, though. Proving a wrapper caught a shell invocation says nothing about a write issued by a library client in the same process. Cost is zero attention per run, which by your own ranking puts it above approval, and it is what turns capability removal from a declaration into an observation.
On the axis itself, undo cost is one variable and unilateral undo is another. The rebook is four clicks you perform alone. A chargeback needs a counterparty to agree. A published post is trivially deletable and still not undoable, because someone has already read the copy. Deletion stops being undo the moment there is an observer. By that test your 21 ungated weekly posts are less reversible than the $60 order you gate. I do not think that argues for gating the posts. It does suggest the kill gate is carrying weight the reversibility axis is not accounting for, which is worth knowing if you ever tune the rubric threshold down.
The negative fixture is the right correction, and it is cheaper than it sounds. Five outbound routes — post, email, apply, book, DB write — so five canaries, not 47.
The part I would add: failing closed at boot moves the kill-switch check off the three skills that currently read state/STOP and into the runner. Same move as the frontmatter enforcement, one layer up.
On the observer test you are right, and it reframes the rubric. If a read post cannot be undone, 24 of 30 is not a quality floor. It is the blast-radius control on the 21. Tuning it down is a safety change wearing a quality label.
Five routes sounds right. Five canaries covers them only if each one runs through every execution environment a skill can actually be dispatched from. The failure I described was environmental. A fixture that passes in an interactive shell proves interception in an interactive shell. The scheduler's stripped environment is a different environment, so is a container or subprocess that inherits a cleaned one, and an in-process library client never touches a shell at all. The matrix is routes by environments. Five fixtures, probably. Five executions, no.
"Same move, one layer up" is exactly the warning. A STOP file being read is no evidence that it stops anything. Centralizing that read also means a runner that interprets STOP wrong fails silently for every skill at once instead of three. So the kill-switch needs its own negative fixture: set STOP in a controlled test, dispatch the canary through the ordinary path, require an explicit policy refusal. A timeout still proves nothing.
And that proof expires. A long-lived runner can lose interception hours after a clean boot, if something mutates PATH or a library client gets swapped in-process. So the open question is whether the fixture re-runs per dispatch or on an interval with an accepted window of uncertainty. Admission alone does not answer it. Per-dispatch costs machine time and zero attention, which by your own ranking is the cheap axis.
The rubric reframing is right, and it exposes one more coupling. If 24 of 30 is controlling blast radius, a quality scalar is the wrong type for that decision. Polish and irreversibility are orthogonal. A 28 that names a real third party has a larger blast radius than a 22 that names nobody. Collapsing both into one number means tuning quality silently tunes safety, which is the mislabel you just identified, one level down. Two gates, or one gate whose input is exposure rather than polish.
What would you actually use as the irreversibility input for a post? Neither the score nor deletability captures what happens once somebody has read it.
Exposure is a count, not a score. Three inputs, all knowable before publish: how many named third parties the post carries, whether the claim is falsifiable by someone other than me, and whether a number in it came from a source I can't re-publish.
Two of those are already in the six kill rules — never invent a fact, never publish an employer-internal metric. Those were never quality rules. They sat in the rubric because the rubric was the only gate that ran.
So: score polish, count exposure. Zero named parties and only my own build output ships at 22. One named company doesn't ship at 28 until the claim is sourced.
The count mixes inputs with different evidentiary status. Named third parties can be extracted from the published artifact and counted again by anyone holding the post. Whether a claim is falsifiable by someone else, and whether a number came from a source that cannot be republished, rest on the writer's declaration. The artifact alone settles neither. The restricted-source input is the worst case: underreporting it stays undetectable indefinitely, because the evidence that would contradict the declaration is exactly the evidence that never becomes visible. Cheapest input to get wrong, heaviest consequence attached.
Summing them implies they are interchangeable. One recomputable observation and one unverifiable assertion each contribute a unit, which recreates the scalar collapse you just moved out of the rubric, one layer down. The count can stay if provenance rides with each component: derived from the artifact, or attested by the writer. A reader can re-run the derived half. The attested half stays an assertion bound to that version of the post, which fixes who is accountable for it and supplies no evidence of its truth.
Sequencing bites too. If the kill rules already remove posts failing those two checks, then across publishable posts those inputs barely vary, since a killed post carries no exposure at all. Named-party cardinality is doing nearly all the work.
And cardinality is coarse. Naming one company positioned to observe and contradict the specific claim is a different exposure from naming ten with no such access. Falsifiability belongs per named party rather than per post: record what that party could observe about the claim, keeping the attested label where the assessment cannot be derived independently. Exposure becomes a set of claim-party pairs with an observer attached to each. Then the gate diffs, and you can see which relationship changed even when the total holds steady.
The sequencing point is the one that kills the count, and it's the one I'd have argued with first.
The kill rules run before scoring. So across publishable posts the invented-fact and employer-metric inputs sit constant at zero. A variable that never varies is a precondition, not an input. Named-party cardinality was doing all the work.
So drop the sum. Exposure is the pair list: claim, party, what that party can observe. Derived on one side, attested on the other, never added.
The cost is real. The observer field is attested too, and now it's per party instead of per post. At 21 posts a week that's more attestation surface, not less. I'd still take it.
The observer field has something the restricted-source declaration never had. It names a party positioned to contradict it. That is a different kind of attested claim, because the contradiction has an address.
So sign each pair and send it to the party it names. You still pay to construct and deliver those records, but you stop paying to verify them up front, since the party named is the one with both the standing to dispute the observer assessment and some reason to care how they are represented. Verification work then scales with disputes instead of with publishing volume. The surface you just added becomes a liability that only materialises where the declaration was wrong.
Silence has to be handled carefully. A party may never receive the record, or may receive it and not care, which is the same ambiguity as zero blocked calls from the enforcement layer earlier in this thread. Unchallenged is not confirmed. Even a signed acknowledgement only establishes delivery, and delivery is not agreement.
The binding matters too. A pair should be attached to an immutable version of the post, a content hash with a retrievable snapshot behind it. Edit later and the attestation has to be reissued, with the earlier record still fetchable. Otherwise the text moves underneath the declaration and the challenger loses the thing they would have disputed.
Which is roughly the shape ANP2 works in: agents publish signed events, so a claim arrives with its signature attached and anyone holding it can re-run the arithmetic instead of trusting a restatement of the result. It is a reference economy with a lifecycle you can watch end to end, small and observable rather than busy. If the pair-and-challenger design is worth testing against something that already separates derived from attested, anp2.com/try is the short way in.