Permission

What a coding agent may run without asking you

An agent that can run commands has to be told what counts as a question and what counts as ordinary work. Get that line wrong one way and it prompts on every file read until the plan is unusable. Get it wrong the other way and it has quietly been allowed to do whatever it likes for an hour. The design worth looking at is not the prompt. It is the scope, the mode, and what a decision is worth once you have made it.

Disclosure: I build Ordewell, so this is the policy it applies rather than a survey of the field. The tool is free and Apache 2.0. The mechanics are worth arguing about whether or not you install it.

The unit of approval is a scope, not a command

The obvious design is a queue of exact commands: the agent wants to run ls /tmp/foo, you say yes, and next time it runs ls /tmp/bar it asks again. That design fails on contact with a planning loop. A planner doing real research reads forty things in a row, and forty prompts turn a useful feature into a wall of dialogs.

So the decision is keyed on a scope instead. Approving a read of /tmp/foo/a.log grants /tmp/foo/*. Approving az group list grants az group. The grant is broader than the thing you approved, deliberately, and it is remembered for the rest of the session so the same class of call never asks twice. Saying yes once covers everything under that path or command family, including arguments the agent has not thought of yet.

Three modes, and a floor under all of them

Underneath the scope there is a mode, and it is what you would set for a run you are not sitting next to.

The pre-approved list is honored under every mode, including deny. That ordering is on purpose: it is an explicit operator decision, not a default that a mode setting is allowed to override. In practice this is the knob that makes an unattended run possible. You write the handful of things this plan genuinely needs, set the mode to deny, and walk away.

Every decision is labelled with where it came from, so a log or a UI can say why a call was allowed rather than only that it was:

the source of a decision
  type ApprovalSource =
    'pre-approved' | 'remembered' | 'mode' | 'asked' | 'no-channel'

That list is the honesty of the feature: a call allowed by mode is a standing rule, and a call allowed by asked is a person.

A denial is remembered too

Yes and no get the same treatment. If a policy denies a lookup, that denial is kept, so a model that retries the same blocked call burns one tool round instead of putting the same question to you again. The refusal carries back as a normal, actionable tool result: the model is told the answer was no, which is information it can plan around, rather than being left to hang.

A call can touch more than one scope, and a no to all of them together is remembered as one refusal rather than several, because your no may have been about any one of them.

One ask per scope, and a session that forgets on purpose

A parallel tool round can produce several requests for the same thing at once. They collapse into one question: the first goes to you and the rest wait on the same answer, so a burst of parallel calls cannot become a burst of prompts.

Grants live for the session rather than forever. On reset the granted and refused sets are dropped, and an answer still in flight from before the reset may not write into the new state. A stale yes landing a second late cannot silently re-open something you cleared.

There is also an answer beyond yes and no. Where the requester offered a suggestion of a broader grant, you can allow it for this task alone. Where none was offered, that answer degrades to a plain allow: the code will not make a grant the requester never proposed, because a button is not a mandate.

What happens when nobody answers

This is where a research loop and a task runner get different treatment, deliberately.

A planner turn that blocks forever on an unanswered prompt would hang the whole research loop with no visible cause, so a planner prompt expires. The default is five minutes. On expiry the request resolves to denied and the model receives a normal tool result, which means the loop continues and the cause is legible. Nothing sits there holding the process open on a question nobody is watching.

A task runner's request is the exception. That session is parked on the answer rather than looping, so it waits for a person however long that takes. The cost is honest: a run left unattended in ask mode with a runner waiting will sit there until you come back.

In both cases the request is cleaned up when the attempt ends. Every way an attempt can finish reaches the runner as a stop, and open requests are denied before the session goes, so nothing waits on a process that no longer exists. A request the runner withdraws is settled as denied so it comes off every surface showing it. Anything still outstanding is replayed to a surface that connects late, so opening the UI after the fact does not show an empty list.

Limits

Source and design notes

A policy that answers out-of-envelope calls once, remembers the answer, and knows the difference between a standing rule and a person. The code behind that: the approval policy, the request bridge, runner permission requests, and github.com/ordewell/ordewell.