The Agent Knows What It Knows
I built widening into OpenApe from the start — into the agent, client-side, because I thought the agent was the intelligence and would know what it needed next. It never used it. How I realized widening belongs with the user, not the agent — and what came out of that.
Widening was always clear to me.
It's an obvious property of a system where every command is approved individually: at some point there has to be a way to approve a class of commands in one step. Otherwise you spend the first ten minutes of every session doing nothing but tapping Approve on ls and cat. I built that into the first version of OpenApe. Client-side. Into the agent.
That was my mistake, and I need a whole article to explain why.
The Assumption
The client-side version worked like this: in its grant request, the agent could add a broader pattern alongside the concrete action. Instead of "let me run ls /home/patrick once", the agent could say "let me run ls {path} on anything below /home/patrick/ for the next hour." I approve once. It has what it needs. No further prompt for the rest of the session.
The assumption behind the design: the agent is the intelligence. It sees the current task, it knows which commands it's likely going to need, it proactively asks for a matching scope. The user approves once, not thirty times. The agent is the one with the context — it knows it's about to run ls, cat, head, and maybe a grep over a directory tree. Nobody else knows at that moment.
That was elegant on paper. It also worked exactly once — in a test script I wrote myself to prove the widening endpoint did what it was supposed to.
What the Agent Doesn't Do
In real sessions, the agent never used it. Not once.
It asked for ls. Approved. Ran ls. Then cat package.json. Approved. Ran cat. Then ls on a different directory. Approved. Ran ls. Then git status. Approved. Ran git status. Then cat package.json again, because the output was no longer in its context window. Approved. Ran. Thirty times through, not a single widening request.
That bothered me for a while. I thought I'd written the tool description badly, or that the system prompt wasn't clear enough about when widening was appropriate. Sharpened both. No change.
Then it hit me: the agent knows what it knows. It can formulate an ls because ls is part of its world. It can formulate a cat package.json. It can even formulate a git log --oneline --since=yesterday. What it doesn't do is pause deeper inside the tool-use flow and make a meta request — "given the next fifteen minutes of this session, the following broader scope would be efficient." That's not its mode of thinking. Tool use is reactive, one step at a time. Meta-reflection on your own workflow does not happen inside the flow.
The agent is a good typist. It isn't the product owner of its own work process.
Who's the Annoyed One?
This is where the architecture tipped.
If the agent doesn't apply widening itself, the question becomes: who is actually annoyed by the absence of widening? Not the agent. The agent has no nervous system. The agent gets Approve → Execute → Approve → Execute and just keeps going.
Annoyed is me. The user. The one tapping Approve on ls for the tenth time on his phone.
If I'm the one who benefits from widening, then widening is a user decision. Not an agent decision. The user knows what this agent is likely to do over the coming weeks — he set it up, he knows what he's using it for, he has a rough sense of its tasks. The agent only knows what it's doing in the current tool-use step. The widening decision needs context about the agent, not context inside the agent.
That's the shift. Widening doesn't belong in the agent. It belongs on the server. And it belongs with the user.
Standing Grants
The thing is now called a Standing Grant in OpenApe's vocabulary. A grant template the user creates themselves — before any agent has made a request. It describes: which agent, which CLI, which action, up to what risk level, optionally on which resource, optionally with an expiration date. A row in the grants table with status='approved' and decided_by = <me>. No agent is involved. The grant exists before anyone needs it.
When an agent then sends a regular request to POST /api/grants, it runs through this chain:
- Reuse check. Is there already an approved grant with exactly the same details? Reuse it.
- Standing-Grant check (new). Is there a Standing Grant whose pattern covers the request details? Create a new grant with
status='approved',decided_by = <SG owner>,decided_by_standing_grant = <SG id>as audit pointer. - Similarity check. Is there a similar grant (same CLI+action, different resource)? Show it to the approver as context.
- HITL. If everything before falls through: notification to the approver, approval page in the browser.
Step 2 is the shift. The agent request is checked server-side against a library of user-defined pre-authorizations. Only on a miss do I end up in my browser.
The audit trail stays complete. Every grant is still in the table. On top of that, decided_by_standing_grant is recorded, so I can retrace: this grant matched Standing Grant #42, which I created on April 18. Nothing disappears. Only the prompt disappears — for the patterns I explicitly pre-authorized.
I didn't delete the client-side widening code. It sits in a branch, waiting for agents to one day actually meta-reflect. For now, it's dead code.
Safe Commands: When the User Is the New One
There's a case the model doesn't yet cover: the brand-new agent on day one. Zero Standing Grants. Every request is a prompt. Exactly the problem I wanted to solve, just for the onboarding phase.
The honest version would have been to tell the user: your agent is running now — please spend the next ten minutes setting up Standing Grants, then it'll quiet down. Nobody does that. Nobody reads setup docs to the end, nobody pre-emptively configures policies.
So fourteen defaults ship along. When a new agent is enrolled, the IdP automatically creates Standing Grants for these commands:
ls, cat, head, tail, wc, file, stat, which, echo, date, whoami, pwd, find, grep
Each one risk=low, no resource constraint, no expiry. All fourteen are read-only, non-mutating, non-networking, not credential-touching. rm isn't in there. curl isn't. ssh isn't. git isn't. The list contains what you type to see what's there — and none of what you type to change what's there. That's the line I want to keep sharp.
The fourteen are an advance decision I make as a maintainer for every user who hasn't made their own. Whoever wants them, gets them. Whoever doesn't, deactivates the group with a toggle. Whoever wants to retroactively seed existing agents can find a Bulk-Apply modal on /agents.
Auto-approvals through that group get a Safe cmd badge in the activity list. That's not cosmetic. It's there because a month later I want to be able to distinguish, in retrospect: did this grant pass because I personally curated it, or because it was in the maintainer-defined defaults group?
When Fourteen Isn't Enough
After a few days I had data on which commands were still prompting. The most common was git status. Then git log. Then git diff. None of them are in the defaults, because git as a CLI also knows git push, git commit, git reset --hard — and a default list that lets all of that through wouldn't be safe anymore.
I could have created a Standing Grant per read subcommand. I didn't, because that's not how I think about git. I think anything read-only on git is fine. That's a user decision on the class level, and user decisions on the class level are exactly what Standing Grants are for — if the language supports it.
That's what glob is for. cliAuthorizationDetailCovers() — the function that decides whether a granted scope covers an incoming request — now treats * in granted selector values as a POSIX shell glob. A Standing Grant with resource_chain_template: "file://{path:/home/patrick/projects/*}" matches /home/patrick/projects/openape/README.md and /home/patrick/projects/blog/2026-04-21.md, but not /etc/passwd. Selector values without * stay literal equality. All existing Standing Grants keep matching the same as before.
In the UI this sits behind a wizard I built mobile-first. First step: type in an example command you recently needed. The wizard resolves it against the registered shape and pulls out the typed slots. Second step: set each slot to Literal, Any, or Pattern, with a live preview showing a few examples the pattern matches and a few it doesn't. Third step: risk cap, optional duration, reason.
Most of the approvals I do today I do on my phone. If authoring pre-authorizations was desktop-bound, it would be exactly outside the moment in which I want to do it.
Two Approval Layers
The real thing that became clear to me while rebuilding this isn't the mechanics. It's the layer cut.
There are two levels at which approval can happen. One is Pattern: a user decision about which class of things is pre-authorized. The other is Command: a live decision about whether this one concrete thing is okay right now.
The obvious alternative — cache — extends Command in time (this approved command is still valid for five minutes on anything similar), without introducing a new layer. That turns approved once into approved forever, for N minutes. sudo timestamp. sudoers with timestamp_timeout. Manageable for humans; for an agent, the distinction between one approved command and all commands for the next N minutes collapses entirely.
Standing Grants introduce the new layer explicitly. Pattern becomes a first-class artifact: UI, owner, audit log, revoke button, inventory. I can go into /agents/claude@home and see: I've granted this agent three Standing Grants. One for git.status, one for Safe Commands, one for file://projects/*. Each one revocable without touching the others. Command stays live, per-command, bound to an approver. Anything no Pattern covers ends up on the approval page.
The question isn't how long does an approval last. The question is which class am I willing to describe in advance.
The Intelligence Was in the Wrong Layer
Back to the beginning. The client-side version left the widening decision to the agent because I thought agent intelligence also extends to meta-reflection on its own workflow. It doesn't, at least not inside current tool-use flows. The agent knows what it knows — the concrete next command. It doesn't think about the pattern behind it.
The intelligence about the pattern was in the wrong place. It belongs with the user. The user has the workflow context — this agent is my coding agent, reads a lot, writes occasionally, this other agent is a sync job, narrow scope but runs often. The user derives deliberate class statements from that context. The agent executes its concrete tool-use steps, and for the ones that fall inside a class, approval happens silently.
That's the same pattern as the rest of OpenApe: Infrastructure over Instructions. I don't make the agent smarter. I hang the structure differently. Widening decisions aren't made by the agent; they're made by the user, and the infrastructure makes sure both sides benefit without having to negotiate directly.
Hygiene
What became visible after two weeks: I have more Standing Grants than I expected. Sooner or later you need show me everything I granted in the last N days and haven't used since and a one-click revoke. That's not a security feature, that's cleanup. Without it, the model dilutes — and a diluted model quickly stops being a model and turns into a long list of yes from the past.
The Cut
Not every command needs a human approval. But every approved pattern needs a human author.
The author of the pattern is me — with a name, an audit row, a revoke button. The approver of the individual command is either me (when no pattern matches) or the pattern I built (when one does). At no point is the agent itself on the authorization path. That wasn't the case before — not in theory, but in the quiet reliance on the idea that the agent would actually make widening requests of its own. It didn't.
In the previous article it said: it's not the user that gets approved, but the crossing. Standing Grants don't change that. They only make the model keep working when the agent does something a hundred times an hour — not because the agent got smarter, but because I can make the right statements at the right level.
Code: github.com/openape-ai/openape, MIT-licensed. The mechanics live in @openape/grants, the UI in openape-free-idp.