[{"data":1,"prerenderedAt":636},["ShallowReactive",2],{"blog-en-from-prompts-to-patterns":3,"header-blog-translations-/en/blog/from-prompts-to-patterns":633},{"id":4,"title":5,"author":6,"body":7,"date":616,"description":617,"draft":618,"extension":619,"image":620,"meta":621,"navigation":622,"path":623,"seo":624,"stem":625,"tags":626,"translationKey":631,"__hash__":632},"blog_en/blog/en/from-prompts-to-patterns.md","The Agent Knows What It Knows","Patrick Hofmann",{"type":8,"value":9,"toc":603},"minimark",[10,14,35,38,43,65,90,93,97,100,127,130,154,157,161,164,175,183,198,201,205,223,230,267,270,281,284,288,291,298,301,311,341,348,358,362,391,398,427,445,448,452,455,465,495,513,523,527,530,540,543,547,558,562,565,568,581,584],[11,12,13],"p",{},"Widening was always clear to me.",[11,15,16,17,21,22,25,26,30,31,34],{},"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 ",[18,19,20],"em",{},"class"," of commands in one step. Otherwise you spend the first ten minutes of every session doing nothing but tapping ",[18,23,24],{},"Approve"," on ",[27,28,29],"code",{},"ls"," and ",[27,32,33],{},"cat",". I built that into the first version of OpenApe. Client-side. Into the agent.",[11,36,37],{},"That was my mistake, and I need a whole article to explain why.",[39,40,42],"h2",{"id":41},"the-assumption","The Assumption",[11,44,45,46,53,54,64],{},"The client-side version worked like this: in its grant request, the agent could add a broader pattern alongside the concrete action. Instead of ",[18,47,48,49,52],{},"\"let me run ",[27,50,51],{},"ls /home/patrick"," once\"",", the agent could say ",[18,55,48,56,59,60,63],{},[27,57,58],{},"ls {path}"," on anything below ",[27,61,62],{},"/home/patrick/"," for the next hour.\""," I approve once. It has what it needs. No further prompt for the rest of the session.",[11,66,67,68,72,73,76,77,79,80,79,82,85,86,89],{},"The assumption behind the design: ",[69,70,71],"strong",{},"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 — ",[18,74,75],{},"it"," knows it's about to run ",[27,78,29],{},", ",[27,81,33],{},[27,83,84],{},"head",", and maybe a ",[27,87,88],{},"grep"," over a directory tree. Nobody else knows at that moment.",[11,91,92],{},"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.",[39,94,96],{"id":95},"what-the-agent-doesnt-do","What the Agent Doesn't Do",[11,98,99],{},"In real sessions, the agent never used it. Not once.",[11,101,102,103,105,106,108,109,105,112,108,114,116,117,108,119,105,122,108,124,126],{},"It asked for ",[27,104,29],{},". Approved. Ran ",[27,107,29],{},". Then ",[27,110,111],{},"cat package.json",[27,113,33],{},[27,115,29],{}," on a different directory. Approved. Ran ",[27,118,29],{},[27,120,121],{},"git status",[27,123,121],{},[27,125,111],{}," again, because the output was no longer in its context window. Approved. Ran. Thirty times through, not a single widening request.",[11,128,129],{},"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.",[11,131,132,133,136,137,139,140,142,143,145,146,149,150,153],{},"Then it hit me: ",[69,134,135],{},"the agent knows what it knows."," It can formulate an ",[27,138,29],{}," because ",[27,141,29],{}," is part of its world. It can formulate a ",[27,144,111],{},". It can even formulate a ",[27,147,148],{},"git log --oneline --since=yesterday",". What it doesn't do is pause deeper inside the tool-use flow and make a meta request — ",[18,151,152],{},"\"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.",[11,155,156],{},"The agent is a good typist. It isn't the product owner of its own work process.",[39,158,160],{"id":159},"whos-the-annoyed-one","Who's the Annoyed One?",[11,162,163],{},"This is where the architecture tipped.",[11,165,166,167,170,171,174],{},"If the agent doesn't apply widening itself, the question becomes: ",[18,168,169],{},"who is actually annoyed by the absence of widening?"," Not the agent. The agent has no nervous system. The agent gets ",[27,172,173],{},"Approve → Execute → Approve → Execute"," and just keeps going.",[11,176,177,178,25,180,182],{},"Annoyed is me. The user. The one tapping ",[18,179,24],{},[27,181,29],{}," for the tenth time on his phone.",[11,184,185,186,189,190,193,194,197],{},"If ",[18,187,188],{},"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 ",[18,191,192],{},"about"," the agent, not context ",[18,195,196],{},"inside"," the agent.",[11,199,200],{},"That's the shift. Widening doesn't belong in the agent. It belongs on the server. And it belongs with the user.",[39,202,204],{"id":203},"standing-grants","Standing Grants",[11,206,207,208,211,212,215,216,30,219,222],{},"The thing is now called a ",[69,209,210],{},"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 ",[27,213,214],{},"grants"," table with ",[27,217,218],{},"status='approved'",[27,220,221],{},"decided_by = \u003Cme>",". No agent is involved. The grant exists before anyone needs it.",[11,224,225,226,229],{},"When an agent then sends a regular request to ",[27,227,228],{},"POST /api/grants",", it runs through this chain:",[231,232,233,240,255,261],"ol",{},[234,235,236,239],"li",{},[69,237,238],{},"Reuse check."," Is there already an approved grant with exactly the same details? Reuse it.",[234,241,242,245,246,79,248,79,251,254],{},[69,243,244],{},"Standing-Grant check"," (new). Is there a Standing Grant whose pattern covers the request details? Create a new grant with ",[27,247,218],{},[27,249,250],{},"decided_by = \u003CSG owner>",[27,252,253],{},"decided_by_standing_grant = \u003CSG id>"," as audit pointer.",[234,256,257,260],{},[69,258,259],{},"Similarity check."," Is there a similar grant (same CLI+action, different resource)? Show it to the approver as context.",[234,262,263,266],{},[69,264,265],{},"HITL."," If everything before falls through: notification to the approver, approval page in the browser.",[11,268,269],{},"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.",[11,271,272,273,276,277,280],{},"The audit trail stays complete. Every grant is still in the table. On top of that, ",[27,274,275],{},"decided_by_standing_grant"," is recorded, so I can retrace: ",[18,278,279],{},"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.",[11,282,283],{},"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.",[39,285,287],{"id":286},"safe-commands-when-the-user-is-the-new-one","Safe Commands: When the User Is the New One",[11,289,290],{},"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.",[11,292,293,294,297],{},"The honest version would have been to tell the user: ",[18,295,296],{},"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.",[11,299,300],{},"So fourteen defaults ship along. When a new agent is enrolled, the IdP automatically creates Standing Grants for these commands:",[302,303,308],"pre",{"className":304,"code":306,"language":307},[305],"language-text","ls, cat, head, tail, wc, file, stat, which, echo, date, whoami, pwd, find, grep\n","text",[27,309,306],{"__ignoreMap":310},"",[11,312,313,314,317,318,321,322,325,326,325,329,332,333,336,337,340],{},"Each one ",[27,315,316],{},"risk=low",", no resource constraint, no expiry. All fourteen are read-only, non-mutating, non-networking, not credential-touching. ",[27,319,320],{},"rm"," isn't in there. ",[27,323,324],{},"curl"," isn't. ",[27,327,328],{},"ssh",[27,330,331],{},"git"," isn't. The list contains what you type to ",[18,334,335],{},"see what's there"," — and none of what you type to ",[18,338,339],{},"change what's there",". That's the line I want to keep sharp.",[11,342,343,344,347],{},"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 ",[27,345,346],{},"/agents",".",[11,349,350,351,354,355],{},"Auto-approvals through that group get a ",[18,352,353],{},"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: ",[18,356,357],{},"did this grant pass because I personally curated it, or because it was in the maintainer-defined defaults group?",[39,359,361],{"id":360},"when-fourteen-isnt-enough","When Fourteen Isn't Enough",[11,363,364,365,108,367,108,370,373,374,376,377,79,380,79,383,386,387,390],{},"After a few days I had data on which commands were still prompting. The most common was ",[27,366,121],{},[27,368,369],{},"git log",[27,371,372],{},"git diff",". None of them are in the defaults, because ",[27,375,331],{}," as a CLI also knows ",[27,378,379],{},"git push",[27,381,382],{},"git commit",[27,384,385],{},"git reset --hard"," — and a default list that lets all of that through wouldn't be ",[18,388,389],{},"safe"," anymore.",[11,392,393,394,397],{},"I could have created a Standing Grant per read subcommand. I didn't, because that's not how I think about git. I think ",[18,395,396],{},"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.",[11,399,400,401,404,405,408,409,412,413,30,416,419,420,423,424,426],{},"That's what glob is for. ",[27,402,403],{},"cliAuthorizationDetailCovers()"," — the function that decides whether a granted scope covers an incoming request — now treats ",[27,406,407],{},"*"," in granted selector values as a POSIX shell glob. A Standing Grant with ",[27,410,411],{},"resource_chain_template: \"file://{path:/home/patrick/projects/*}\""," matches ",[27,414,415],{},"/home/patrick/projects/openape/README.md",[27,417,418],{},"/home/patrick/projects/blog/2026-04-21.md",", but not ",[27,421,422],{},"/etc/passwd",". Selector values without ",[27,425,407],{}," stay literal equality. All existing Standing Grants keep matching the same as before.",[11,428,429,430,433,434,79,437,440,441,444],{},"In the UI this sits behind a wizard I built mobile-first. First step: ",[18,431,432],{},"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 ",[18,435,436],{},"Literal",[18,438,439],{},"Any",", or ",[18,442,443],{},"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.",[11,446,447],{},"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.",[39,449,451],{"id":450},"two-approval-layers","Two Approval Layers",[11,453,454],{},"The real thing that became clear to me while rebuilding this isn't the mechanics. It's the layer cut.",[11,456,457,458,460,461,464],{},"There are two levels at which approval can happen. One is ",[18,459,443],{},": a user decision about which class of things is pre-authorized. The other is ",[18,462,463],{},"Command",": a live decision about whether this one concrete thing is okay right now.",[11,466,467,468,471,472,475,476,479,480,483,484,487,488,30,491,494],{},"The obvious alternative — cache — extends Command in time (",[18,469,470],{},"this approved command is still valid for five minutes on anything similar","), without introducing a new layer. That turns ",[18,473,474],{},"approved once"," into ",[18,477,478],{},"approved forever, for N minutes",". sudo timestamp. ",[27,481,482],{},"sudoers"," with ",[27,485,486],{},"timestamp_timeout",". Manageable for humans; for an agent, the distinction between ",[18,489,490],{},"one approved command",[18,492,493],{},"all commands for the next N minutes"," collapses entirely.",[11,496,497,498,501,502,512],{},"Standing Grants introduce the new layer explicitly. Pattern becomes a first-class artifact: UI, owner, audit log, revoke button, inventory. I can go into ",[27,499,500],{},"/agents/claude@home"," and see: ",[18,503,504,505,508,509,347],{},"I've granted this agent three Standing Grants. One for ",[27,506,507],{},"git.status",", one for Safe Commands, one for ",[27,510,511],{},"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.",[11,514,515,516,519,520,347],{},"The question isn't ",[18,517,518],{},"how long does an approval last",". The question is ",[18,521,522],{},"which class am I willing to describe in advance",[39,524,526],{"id":525},"the-intelligence-was-in-the-wrong-layer","The Intelligence Was in the Wrong Layer",[11,528,529],{},"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.",[11,531,532,533,79,536,539],{},"The intelligence about the pattern was in the wrong place. It belongs with the user. The user has the workflow context — ",[18,534,535],{},"this agent is my coding agent, reads a lot, writes occasionally",[18,537,538],{},"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.",[11,541,542],{},"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.",[39,544,546],{"id":545},"hygiene","Hygiene",[11,548,549,550,553,554,557],{},"What became visible after two weeks: I have more Standing Grants than I expected. Sooner or later you need ",[18,551,552],{},"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 ",[18,555,556],{},"yes"," from the past.",[39,559,561],{"id":560},"the-cut","The Cut",[11,563,564],{},"Not every command needs a human approval. But every approved pattern needs a human author.",[11,566,567],{},"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.",[11,569,570,571,576,577,580],{},"In the ",[572,573,575],"a",{"href":574},"/en/blog/today-the-agent-does-what-he-isnt-allowed-to","previous article"," it said: ",[18,578,579],{},"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.",[582,583],"hr",{},[11,585,586],{},[18,587,588,589,595,596,599,600,347],{},"Code: ",[572,590,594],{"href":591,"rel":592},"https://github.com/openape-ai/openape",[593],"nofollow","github.com/openape-ai/openape",", MIT-licensed. The mechanics live in ",[27,597,598],{},"@openape/grants",", the UI in ",[27,601,602],{},"openape-free-idp",{"title":310,"searchDepth":604,"depth":604,"links":605},2,[606,607,608,609,610,611,612,613,614,615],{"id":41,"depth":604,"text":42},{"id":95,"depth":604,"text":96},{"id":159,"depth":604,"text":160},{"id":203,"depth":604,"text":204},{"id":286,"depth":604,"text":287},{"id":360,"depth":604,"text":361},{"id":450,"depth":604,"text":451},{"id":525,"depth":604,"text":526},{"id":545,"depth":604,"text":546},{"id":560,"depth":604,"text":561},"2026-04-21","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.",false,"md",null,{},true,"/blog/en/from-prompts-to-patterns",{"title":5,"description":617},"blog/en/from-prompts-to-patterns",[627,628,204,629,630],"OpenApe","AI Agents","Infrastructure","Building in Public","from-prompts-to-patterns","dGRHVmrZeVSWQICmeEludyABozZux7wUnlP7KBJGw4Y",{"en":634,"de":635},"/en/blog/from-prompts-to-patterns","/de/blog/von-prompts-zu-mustern",1776970806601]