[{"data":1,"prerenderedAt":4979},["ShallowReactive",2],{"header-blog-translations-/de/blog":3,"blog-list-de":4},null,[5,343,941,1787,2701,3166,4660],{"id":6,"title":7,"author":8,"body":9,"date":327,"description":328,"draft":329,"extension":330,"image":3,"meta":331,"navigation":329,"path":332,"seo":333,"stem":334,"tags":335,"translationKey":341,"__hash__":342},"blog_de/blog/de/die-convenience-die-ich-bezahlt-habe.md","Vercel macht vieles leicht. Agents halt auch.","Patrick Hofmann",{"type":10,"value":11,"toc":313},"minimark",[12,16,24,27,32,35,42,45,49,60,71,77,80,84,91,98,102,105,108,111,115,127,138,142,145,196,199,203,206,212,230,240,243,247,258,261,265,268,271,275,278,281,291,294,297,300],[13,14,15],"p",{},"Ich musste nicht outsourcen. Ich wollte.",[13,17,18,19,23],{},"Vercel war lange die beste Antwort auf eine Frage, die ich gar nicht selbst beantworten wollte: ",[20,21,22],"em",{},"wie bekomme ich eine reibungslose CI — für Tests und für Production gleichermaßen?"," Das ist keine Frage mit einer offensichtlichen Antwort. Vercel liefert out-of-the-box, was selbst schon schwer definierbar ist: Commit-Push → Tests → Preview-Deploy pro Branch → grüne Production-URL, ohne dass man sich den Flow erst ausdenken muss. Es war super. Es war einfach. Es war sogar lange gratis, und als ich irgendwann in den bezahlten Tarif gewechselt bin, habe ich das aus gutem Grund getan: ich habe bekommen, was ich bezahlt habe.",[13,25,26],{},"Was sich geändert hat, ist nicht Vercel. Was sich geändert hat, bin ich — oder genauer: das, was ich heute an meinem Arbeitsplatz einsetze.",[28,29,31],"h2",{"id":30},"was-vercel-für-mich-war","Was Vercel für mich war",[13,33,34],{},"Vercel hat aus einem Haufen beweglicher Teile eine Pipeline gemacht, die man reibungslos bedienen kann. Commit-Push → Build → Deploy → CDN → URL, mit Preview-Deployments, automatischen Rollbacks, eingebauten Metrics. Kein nginx-Config selber schreiben, kein Zertifikat provisionieren, kein Deploy-Script bauen, kein Secret-Management koordinieren, kein Monitoring aufsetzen.",[13,36,37,38,41],{},"Ich hätte das alles selbst machen können. Ich habe es aber nicht gemacht, weil die Summe aus diesen Schritten als ",[20,39,40],{},"zu viel Aufmerksamkeit"," in Zeit verbucht wurde — und weil Vercel es wirklich gut macht. Wer weiß, was Vercel macht, weiß auch, wie mühsam es ist, so etwas selbst zu bauen — und dass diese Arbeit nie einmal erledigt ist. Sie kehrt zurück, im ungünstigen Moment, ohne Ankündigung.",[13,43,44],{},"Für diese Abnahme habe ich gerne bezahlt. Ich stehe auch heute dazu.",[28,46,48],{"id":47},"was-sich-geändert-hat","Was sich geändert hat",[13,50,51,52,55,56,59],{},"Zwischen ",[20,53,54],{},"damals"," und ",[20,57,58],{},"jetzt"," ist eine Sache schneller passiert, als die meisten Abonnement-Rechnungen mitgekommen sind: Agents sind gut geworden.",[13,61,62,63,66,67,70],{},"Nicht brauchbar. Nicht unterhaltsam. ",[20,64,65],{},"Gut",". Gut in dem Sinne, dass die vielen Schritte, die Vercel mir abgenommen hat, heute mein Agent ausführt. Die Pipeline, die Vercel zuverlässig gebaut hat, lässt sich mit einem guten Agent-Setup auch selbst führen — in der gleichen Zeit, mit derselben Trefferquote, mit demselben ",[20,68,69],{},"es geht einfach",". Vercel macht vieles leicht. Agents halt auch.",[72,73,74],"blockquote",{},[13,75,76],{},"Ich habe nicht für Infrastruktur bezahlt. Ich habe für die Aufmerksamkeit bezahlt, die Infrastruktur braucht. Diese Aufmerksamkeit kann ich jetzt delegieren, ohne eine Rechnung dafür zu bezahlen.",[13,78,79],{},"Der Unterschied ist nicht ideologisch, er ist einfach praktisch: zwei Wege führen jetzt zum selben Ergebnis, und der eine gibt mir mehr Kontrolle.",[28,81,83],{"id":82},"der-agent-denkt-mit","Der Agent denkt mit",[13,85,86,87,90],{},"Ein weiterer Punkt wurde mir erst beim Arbeiten klar: der Agent baut die CI/CD-Kette nicht einmal und dann steht sie. Er baut sie on the way. Ich sage einmal ",[20,88,89],{},"\"so will ich es deployen\""," — spontan, formlos, vielleicht unvollständig — und er setzt ein passgenaues Skript auf. Wenn sich später herausstellt, dass ich einen zusätzlichen Healthcheck brauche, einen anderen Rollback-Trigger, einen Branch-spezifischen Build-Step, dann passt er das Skript an. Er denkt nicht jedesmal alles neu; er ergänzt und modifiziert.",[13,92,93,94,97],{},"Embrace the change. Wenn sich die Anforderungen ändern, ändert sich die Kette. Kein ",[20,95,96],{},"jetzt steht die Pipeline, jetzt ist die Config eingefroren, jetzt bewegen wir uns in dem, was vor sechs Monaten sinnvoll war",". Der Agent denkt mit, und er zwingt mich nicht in eine Kiste.",[28,99,101],{"id":100},"was-der-agent-nicht-kann","Was der Agent nicht kann",[13,103,104],{},"Eine Sache kann mir der Agent nicht abnehmen: ein globales CDN und echte Edge-Skalierung.",[13,106,107],{},"Das ist keine Convenience, das ist Engineering-Arbeit an einer Ebene, die man nicht an einem Nachmittag löst. Ein Netzwerk von Edge-Knoten, Cache-Invalidierung, DDoS-Mitigation, intelligentes Routing — das ist kein Deploy-Script-Problem. Das ist Infrastruktur-im-eigentlichen-Sinne, die man nicht mit lokalen Tools und einem Agent ersetzt.",[13,109,110],{},"Wenn ich an den Punkt komme, wo eines meiner Projekte echte Nutzerzahlen sieht und meine VM das nicht mehr mit einem Standardzustand bedient, dann würde ich jederzeit wieder bezahlen. Nicht zurück, weil der Wechsel ein Fehler war, sondern weiter — mit einem klaren Gefühl dafür, wofür genau ich Vercel dann bezahle. Nicht für die Convenience, sondern für die Skalierung.",[28,112,114],{"id":113},"der-zweite-trigger","Der zweite Trigger",[13,116,117,118,122,123,126],{},"Parallel stand sowieso die Domain-Migration an — die langfristige Antwort auf die Frage, warum eine deutsche Domain ein Projekt trägt, dessen Zielgruppe international ist. ",[119,120,121],"code",{},"openape.at"," → ",[119,124,125],{},"openape.ai",", mit GitHub-Org, IdP und allem, was dranhängt.",[13,128,129,130,133,134,137],{},"Wenn du eine Domain-Migration planst, fasst du sowieso DNS, SSL, nginx-Vhosts, Mail-Routing und Service-Discovery an. In dem Moment, in dem ich die ",[119,131,132],{},".ai","-Subdomains ohnehin anfasse, kostet es kaum Extra-Aufwand, das Ziel frei zu wählen. Die Migration war sowieso Operations. Die Wahl, ",[20,135,136],{},"wo"," die Services liegen, war dann frei — und ich habe mich für die eigene VM entschieden.",[28,139,141],{"id":140},"was-umgezogen-ist","Was umgezogen ist",[13,143,144],{},"Eine VM, läuft bei mir. Das, was jetzt dort liegt:",[146,147,148,158,166,180,188],"ul",{},[149,150,151,157],"li",{},[152,153,154],"strong",{},[119,155,156],{},"id.openape.ai"," — der Identity Provider. Der, der JWTs signiert. Der mit allen sensiblen Secrets.",[149,159,160,165],{},[152,161,162],{},[119,163,164],{},"docs.openape.ai"," — die Dokumentation.",[149,167,168,173,174,179],{},[152,169,170],{},[119,171,172],{},"mail.openape.ai"," / ",[152,175,176],{},[119,177,178],{},"proxy.openape.ai"," — Transport-Layer. Mail-Sender für IdP-Notifications, Proxy für Agent-Webhook-Flows.",[149,181,182,187],{},[152,183,184],{},[119,185,186],{},"www.openape.ai"," — die Landing.",[149,189,190,195],{},[152,191,192],{},[119,193,194],{},"delta-mind.at"," (dieses Blog) — auch mit umgezogen. rsync + nginx. Auto-Deploy per GitHub Action, die die Build-Artefakte über SSH auf die VM pusht.",[13,197,198],{},"Dazu kam ein dedizierter Service-User, systemd-Units pro Service, ein auto-deploy-Script für jeden Push, Logrotate- und Health-Check-Regeln. Alles Arbeit, die ich früher nicht gemacht hätte. Alles Arbeit, die der Agent neben mir zum größten Teil geschrieben hat.",[28,200,202],{"id":201},"was-ich-beim-umzug-gelernt-habe","Was ich beim Umzug gelernt habe",[13,204,205],{},"Self-Hosting ist nicht frei von Kosten — nur von anderen Kosten. Drei Dinge, die mir aufgefallen sind:",[13,207,208,211],{},[152,209,210],{},"Silent Failures werden sichtbar."," Vercel hat mir nie gesagt, wenn eine Mail nicht zugestellt wurde — der Resend-Call lief durch, der Response war 2xx, die Mail kam nicht an. Auf der eigenen VM habe ich das Delivery-Failure-Surfacing explizit rein gebaut: wenn Resend eine Zustellung ablehnt, wird der IdP-Request mit einem sichtbaren Fehler zurückgegeben, nicht stillschweigend geschluckt. Das hätte ich auf Vercel genauso machen müssen. Habe ich aber nicht, weil das ganze System sich unsichtbar angefühlt hat — und unsichtbare Systeme werden selten gehärtet.",[13,213,214,217,218,221,222,225,226,229],{},[152,215,216],{},"Ops-Hygiene wird zur Verantwortung."," Auf Vercel hatte ich keinen Grund, mich zu fragen, ob meine ",[119,219,220],{},"users","- und ",[119,223,224],{},"shapes","-Tabellen beim ersten Start des Services existieren. Auf der eigenen VM ist genau das einer der ersten Bugs, auf die ich gestoßen bin: der IdP startet auf einem frischen Setup nicht, weil die Tabelle nicht da ist. Fix: Auto-Erstellung auf Startup. Keine Magie. Aber jetzt ",[20,227,228],{},"mein"," Job, nicht jemandes anders.",[13,231,232,235,236,239],{},[152,233,234],{},"Pre-Checks sauber stapeln."," OAuth-Codes, Refresh-Tokens, JTIs, Signing Keys — alles Sachen, die auf Vercel als Environment-Variable oder im KV-Store lagen, funktionierten, und nie hinterfragt wurden. Beim Umzug habe ich alle in dieselbe Sequenz gepackt: ",[20,237,238],{},"Service-Start → Tabellen prüfen → Keys prüfen → JTIs prüfen → fertig starten."," Einen einzelnen Ort dafür gab es vorher nicht. Jetzt gibt es einen.",[13,241,242],{},"Das sind keine neuen Lessons. Sie waren nur unsichtbar, weil die Plattform sie unsichtbar gemacht hat — was kein Vorwurf ist, sondern exakt die Haltung, für die man eine Plattform nutzt.",[28,244,246],{"id":245},"die-finanzielle-nebenwirkung","Die finanzielle Nebenwirkung",[13,248,249,250,253,254,257],{},"Ich habe das bewusst in die Mitte gestellt und nicht an den Anfang, weil es nicht der Haupttreiber war: mit steigender Nutzung — viele Test-Projekte, höhere Iterationen, mehr Builds — wird eine Plattform-Rechnung naturgemäß weniger berechenbar. Eine feste VM hat einen festen Preis. Das macht in einer Phase, in der ich aktiv neue Sachen ausprobiere, ",[20,251,252],{},"fix pro Monat"," sicherer planbar als ",[20,255,256],{},"je mehr ich iteriere, desto mehr Builds, desto teurer wird der Versuch",".",[13,259,260],{},"Das ist ein schöner Seiteneffekt der Entscheidung. Es ist nicht der Grund.",[28,262,264],{"id":263},"die-größere-sache","Die größere Sache",[13,266,267],{},"Ich habe ein Projekt gebaut, das sich damit beschäftigt, wie Kontrolle über Entscheidungen an der richtigen Stelle liegt. OpenApe zieht die Policy-Entscheidung vom Agent zum User, weil der Agent den Workflow-Kontext nicht hat. Dasselbe Prinzip gilt, nur auf einer anderen Ebene, für die eigene Infrastruktur: wer die Plattform kontrolliert, kontrolliert die Entscheidungen, die auf ihr getroffen werden.",[13,269,270],{},"Solange eine Plattform dir passt, ist das ein guter Deal. Vercel hat mir lange gepasst und passt vielen Projekten weiter. Aber bei einem Projekt, das seinerseits Identity und Grants baut, um solche Strukturen transparent und revokeable zu machen, wird es irgendwann inkonsistent, die Struktur darüber nicht in die eigene Hand zu nehmen. Das ist keine Abkehr von Managed-Platforms. Das ist eine Konsequenz aus dem, was ich gerade baue.",[28,272,274],{"id":273},"was-das-nicht-ist","Was das nicht ist",[13,276,277],{},"Es ist keine Empfehlung für Self-Hosting als Default.",[13,279,280],{},"Für ein neues Projekt, das in den nächsten drei Monaten einen schnellen Launch braucht, ist Vercel weiter der richtige Weg. Die ersten hundert Nutzer sind wichtiger als ein eigener nginx-Stack. Das ist nicht geändert.",[13,282,283,284,287,288,257],{},"Was sich geändert hat, ist die Grenze, an der Self-Hosting wieder eine realistische Option wird. Früher lag sie bei ",[20,285,286],{},"großer Scale + dediziertem Ops-Team",". Jetzt liegt sie bei ",[20,289,290],{},"ein Entwickler mit einem guten Agent-Setup und einem Nachmittag pro Service",[13,292,293],{},"Ich baue meine Services inzwischen sowieso edge-kompatibel für die eigene Instanz. Wenn der Punkt kommt, an dem ich CDN und Edge-Skalierung ernsthaft brauche, bin ich in einer Domain-TTL zurück auf Vercel — dann genau für das, wofür Vercel wirklich gebaut ist.",[13,295,296],{},"Ich habe nicht gewechselt, weil Vercel schlecht geworden wäre. Ich habe gewechselt, weil der Agent heute das macht, was ich früher an Vercel outsourced habe. Vercel macht vieles leicht. Agents halt auch. Und für das, was Vercel leichter macht als jeder Agent — CDN, Edge, Skalierung auf einem Niveau, das kein Wochenend-Setup je erreichen wird — würde ich jederzeit wieder bezahlen.",[298,299],"hr",{},[13,301,302],{},[20,303,304,305,312],{},"Setup: eine einzelne VM, Debian, nginx, systemd, rsync-basiertes Deploy pro Service. Die Services sind die, die ich oben aufgeführt habe. Code für den IdP: ",[306,307,311],"a",{"href":308,"rel":309},"https://github.com/openape-ai/openape",[310],"nofollow","github.com/openape-ai/openape",", MIT-lizenziert. Die eigentliche Server-Config liegt im privaten Ops-Repo — nicht weil sie geheim ist, sondern weil sie meine ist.",{"title":314,"searchDepth":315,"depth":315,"links":316},"",2,[317,318,319,320,321,322,323,324,325,326],{"id":30,"depth":315,"text":31},{"id":47,"depth":315,"text":48},{"id":82,"depth":315,"text":83},{"id":100,"depth":315,"text":101},{"id":113,"depth":315,"text":114},{"id":140,"depth":315,"text":141},{"id":201,"depth":315,"text":202},{"id":245,"depth":315,"text":246},{"id":263,"depth":315,"text":264},{"id":273,"depth":315,"text":274},"2026-04-23","Ich habe meinen Stack von Vercel weggezogen. Nicht weil Vercel schlecht geworden wäre — Vercel ist weiter super. Sondern weil der Agent heute genau die CI-Schritte ausführt, die Vercel mir früher out-of-the-box abgenommen hat.",true,"md",{},"/blog/de/die-convenience-die-ich-bezahlt-habe",{"title":7,"description":328},"blog/de/die-convenience-die-ich-bezahlt-habe",[336,337,338,339,340],"Infrastructure","Platform Sovereignty","AI Agents","Self-Hosting","Building in Public","paid-convenience-vs-agent","4G9ZSSZpoubQJ67j8CDy5YUQ-m3mUn66MDvIJfhPt7U",{"id":344,"title":345,"author":8,"body":346,"date":930,"description":931,"draft":932,"extension":330,"image":3,"meta":933,"navigation":329,"path":934,"seo":935,"stem":936,"tags":937,"translationKey":939,"__hash__":940},"blog_de/blog/de/von-prompts-zu-mustern.md","Der Agent kennt sich mit dem aus, was er kennt",{"type":10,"value":347,"toc":918},[348,351,365,368,372,394,418,421,425,428,457,460,484,487,491,494,505,514,521,524,528,546,553,590,593,604,607,611,614,621,624,633,662,669,679,683,711,718,747,765,768,772,775,785,815,833,843,847,850,860,863,867,878,882,885,888,900,902],[13,349,350],{},"Widening war mir immer klar.",[13,352,353,354,357,358,55,361,364],{},"Es ist eine offensichtliche Eigenschaft eines Systems, in dem jeder Command einzeln approved wird: irgendwann muss es einen Weg geben, eine ",[20,355,356],{},"Klasse"," von Commands in einem Schritt zu approven. Sonst sitzt man bei ",[119,359,360],{},"ls",[119,362,363],{},"cat"," die ersten zehn Minuten nur im Prompt-Fenster. Ich habe das in der ersten Version von OpenApe eingebaut. Client-seitig. Im Agent.",[13,366,367],{},"Das war mein Fehler, und ich brauche einen ganzen Artikel, um zu erklären warum.",[28,369,371],{"id":370},"die-annahme","Die Annahme",[13,373,374,375,382,383,393],{},"Die client-seitige Variante sah so aus: der Agent kann in seinem Grant-Request zusätzlich zur konkreten Aktion ein breiteres Pattern beantragen. Statt ",[20,376,377,378,381],{},"\"gib mir die Erlaubnis, ",[119,379,380],{},"ls /home/patrick"," einmal auszuführen\""," hätte der Agent sagen können ",[20,384,377,385,388,389,392],{},[119,386,387],{},"ls {path}"," auf allem unterhalb von ",[119,390,391],{},"/home/patrick/"," zu machen, für die nächste Stunde.\""," Ich approve einmal. Er hat was er braucht. Kein weiterer Prompt in dieser Session.",[13,395,396,397,400,401,404,405,407,408,407,410,413,414,417],{},"Die Annahme hinter dem Design: ",[152,398,399],{},"Der Agent ist die Intelligenz."," Er sieht seine aktuelle Aufgabe, er weiß welche Commands er voraussichtlich brauchen wird, er beantragt proaktiv einen passenden Scope. Der User approved einmal, nicht dreißig Mal. Der Agent ist derjenige, der den Kontext hat — ",[20,402,403],{},"er"," weiß, dass er gleich ",[119,406,360],{},", ",[119,409,363],{},[119,411,412],{},"head"," und vielleicht ein ",[119,415,416],{},"grep"," auf einem Verzeichnisbaum braucht. Niemand sonst weiß das zu diesem Zeitpunkt.",[13,419,420],{},"Das war auf dem Papier elegant. Es hat auch genau einmal funktioniert — in einem Testskript, das ich selbst geschrieben habe, um zu zeigen, dass der Widening-Endpoint tut was er soll.",[28,422,424],{"id":423},"was-der-agent-nicht-tut","Was der Agent nicht tut",[13,426,427],{},"In echten Sessions hat der Agent das nie benutzt. Nicht einmal.",[13,429,430,431,433,434,436,437,440,441,436,443,445,446,436,448,440,451,453,454,456],{},"Er hat ",[119,432,360],{}," angefragt. Approved. ",[119,435,360],{}," ausgeführt. Dann ",[119,438,439],{},"cat package.json",". Approved. ",[119,442,363],{},[119,444,360],{}," auf einem anderen Verzeichnis. Approved. ",[119,447,360],{},[119,449,450],{},"git status",[119,452,450],{},". Dann ",[119,455,439],{}," nochmal, weil er die Ausgabe nicht mehr im Kontext hatte. Approved. Ausgeführt. Dreißig Mal durch, kein einziger Widening-Request dabei.",[13,458,459],{},"Das hat mich eine Weile gewurmt. Ich dachte zuerst, ich hätte die Tool-Definition schlecht gestaltet, oder der System-Prompt wäre unklar darüber, wann Widening angebracht ist. Habe beides nachgeschärft. Keine Änderung.",[13,461,462,463,466,467,469,470,472,473,475,476,479,480,483],{},"Dann habe ich gemerkt: ",[152,464,465],{},"Der Agent kennt sich mit dem aus, was er kennt."," Er kann ein ",[119,468,360],{}," formulieren, weil ",[119,471,360],{}," zu seiner Welt gehört. Er kann ein ",[119,474,439],{}," formulieren. Er kann sogar einen ",[119,477,478],{},"git log --oneline --since=yesterday"," formulieren. Was er nicht macht, ist tiefer im Tool-Use-Flow innezuhalten und eine Meta-Anfrage zu stellen — ",[20,481,482],{},"\"im Hinblick auf die nächsten fünfzehn Minuten dieser Session wäre folgender breiterer Scope effizient\"",". Das ist nicht sein Denk-Modus. Tool-Use ist reaktiv, pro Step. Meta-Reflexion über den eigenen Workflow findet nicht im Flow statt.",[13,485,486],{},"Der Agent ist eine gute Schreibkraft. Er ist kein Product Owner seines eigenen Arbeitsprozesses.",[28,488,490],{"id":489},"wer-ist-der-genervte","Wer ist der Genervte?",[13,492,493],{},"Das ist der Punkt, an dem die Architektur gekippt ist.",[13,495,496,497,500,501,504],{},"Wenn der Agent Widening nicht selbst applied, ist die Frage: ",[20,498,499],{},"wer ist überhaupt genervt von fehlendem Widening?"," Der Agent nicht. Der Agent hat kein Nerv-System. Der Agent kriegt ",[119,502,503],{},"Approve → Execute → Approve → Execute"," und macht einfach weiter.",[13,506,507,508,511,512,257],{},"Genervt bin ich. Der User. Der, der sitzt und auf seinem Handy zum zehnten Mal ",[20,509,510],{},"Approve"," tippt für ",[119,513,360],{},[13,515,516,517,520],{},"Wenn aber ",[20,518,519],{},"ich"," derjenige bin, dem Widening einen Nutzen bringt, dann ist Widening eine User-Entscheidung. Keine Agent-Entscheidung. Der User weiß was dieser Agent in den nächsten Wochen voraussichtlich tun wird — er hat den Agent eingerichtet, weiß wofür er ihn einsetzt, hat eine Vorstellung der Aufgaben. Der Agent weiß nur, was er im aktuellen Tool-Use-Step gerade tut. Die Widening-Entscheidung braucht Kontext über den Agent, nicht Kontext im Agent.",[13,522,523],{},"Das ist der Shift. Widening gehört nicht in den Agent. Es gehört auf den Server. Und es gehört zum User.",[28,525,527],{"id":526},"standing-grants","Standing Grants",[13,529,530,531,534,535,538,539,55,542,545],{},"Das Ding heißt im OpenApe-Vokabular jetzt ",[152,532,533],{},"Standing Grant",". Ein Grant-Template, das der User selbst anlegt — bevor der Agent irgendetwas angefragt hat. Er beschreibt: welcher Agent, welche CLI, welche Action, bis zu welchem Risk, optional auf welcher Resource, optional mit Ablaufdatum. Eine Row in der ",[119,536,537],{},"grants","-Tabelle mit ",[119,540,541],{},"status='approved'",[119,543,544],{},"decided_by = \u003Cich>",". Kein Agent ist daran beteiligt. Der Grant existiert, bevor jemand ihn braucht.",[13,547,548,549,552],{},"Wenn ein Agent dann einen regulären Request an ",[119,550,551],{},"POST /api/grants"," schickt, läuft der durch folgende Kette:",[554,555,556,562,578,584],"ol",{},[149,557,558,561],{},[152,559,560],{},"Reuse-Check."," Gibt es bereits einen approved Grant mit exakt denselben Details? Wiederverwenden.",[149,563,564,567,568,570,571,407,574,577],{},[152,565,566],{},"Standing-Grant-Check"," (neu). Gibt es einen Standing Grant, dessen Pattern die Request-Details covered? Neuen Grant mit ",[119,569,541],{}," erzeugen, ",[119,572,573],{},"decided_by = \u003CSG-Owner>",[119,575,576],{},"decided_by_standing_grant = \u003CSG-ID>"," als Audit-Pointer.",[149,579,580,583],{},[152,581,582],{},"Similarity-Check."," Gibt es einen ähnlichen Grant (gleiche CLI+Action, andere Resource)? Dem Approver als Kontext zeigen.",[149,585,586,589],{},[152,587,588],{},"HITL."," Fällt alles davor durch: Notification an den Approver, Approve-Seite im Browser.",[13,591,592],{},"Step 2 ist der Shift. Der Agent-Request wird server-seitig gegen eine Bibliothek von user-definierten Pre-Authorizations geprüft. Und bei Miss lande ich in meinem Browser.",[13,594,595,596,599,600,603],{},"Der Audit-Trail bleibt vollständig. Jeder Grant steht weiter in der Tabelle. Zusätzlich steht da ",[119,597,598],{},"decided_by_standing_grant",", also kann ich rückwirkend nachvollziehen: ",[20,601,602],{},"dieser Grant war gegen Standing Grant #42 gematcht, den ich am 18. April erstellt habe."," Nichts verschwindet. Nur der Prompt verschwindet — für Pattern, die ich explizit pre-authorized habe.",[13,605,606],{},"Den client-seitigen Widening-Code habe ich nicht gelöscht. Er liegt in einem Branch und wartet darauf, dass Agents in ein paar Jahren eventuell doch meta-reflektieren. Aktuell ist er toter Code.",[28,608,610],{"id":609},"safe-commands-wenn-der-user-der-neue-ist","Safe Commands: wenn der User der Neue ist",[13,612,613],{},"Es gibt einen Fall, den das Modell noch nicht abdeckt: den ganz frischen Agent am ersten Tag. Null Standing Grants. Jeder Request ein Prompt. Genau das Problem, das ich lösen wollte, nur diesmal für die Einrichtungs-Phase.",[13,615,616,617,620],{},"Die ehrliche Variante wäre gewesen, dem User zu sagen: ",[20,618,619],{},"dein Agent läuft jetzt, bitte verbringe die nächsten zehn Minuten damit, Standing Grants einzurichten, danach ist es ruhiger."," Das macht niemand. Niemand liest Setup-Dokumentation bis zum Ende, niemand richtet präemptiv Policies ein.",[13,622,623],{},"Deshalb kommen vierzehn Defaults mit. Bei Agent-Enrollment legt der IdP automatisch Standing Grants für diese Commands an:",[625,626,631],"pre",{"className":627,"code":629,"language":630},[628],"language-text","ls, cat, head, tail, wc, file, stat, which, echo, date, whoami, pwd, find, grep\n","text",[119,632,629],{"__ignoreMap":314},[13,634,635,636,639,640,643,644,643,647,643,650,653,654,657,658,661],{},"Jedes als ",[119,637,638],{},"risk=low",", ohne Resource-Constraint, ohne Ablauf. Alle vierzehn sind read-only, nicht-mutierend, nicht-netzwerkend, nicht-credential-berührend. ",[119,641,642],{},"rm"," ist nicht dabei. ",[119,645,646],{},"curl",[119,648,649],{},"ssh",[119,651,652],{},"git"," ist nicht dabei. Die Liste enthält was man tippt um ",[20,655,656],{},"zu sehen, was ist"," — und nichts davon, was man tippt um ",[20,659,660],{},"zu verändern, was ist",". Das ist die Linie, die ich scharf halten will.",[13,663,664,665,668],{},"Die vierzehn sind eine Vorweg-Entscheidung, die ich als Maintainer für alle User treffe, die keine eigene getroffen haben. Wer sie will, kriegt sie. Wer sie nicht will, deaktiviert die Gruppe mit einem Toggle. Wer bestehende Agents retroaktiv seed'en will, findet auf ",[119,666,667],{},"/agents"," einen Bulk-Apply-Modal.",[13,670,671,672,675,676],{},"Auto-Approvals über diese Gruppe kriegen in der Activity-Liste ein ",[20,673,674],{},"Safe cmd"," Badge. Das ist keine Kosmetik. Das ist, weil ich an einem Monat später rückwirkend unterscheiden können will: ",[20,677,678],{},"ist dieser Grant durchgelaufen, weil ich ihn persönlich gepflegt habe, oder weil er in der vom Maintainer definierten Defaults-Gruppe war?",[28,680,682],{"id":681},"wenn-die-vierzehn-nicht-reichen","Wenn die Vierzehn nicht reichen",[13,684,685,686,453,688,453,691,694,695,697,698,407,701,407,704,707,708,257],{},"Nach ein paar Tagen hatte ich Daten darüber, welche Commands weiter prompten. Der häufigste war ",[119,687,450],{},[119,689,690],{},"git log",[119,692,693],{},"git diff",". Keiner davon ist in den Defaults, weil ",[119,696,652],{}," als CLI auch ",[119,699,700],{},"git push",[119,702,703],{},"git commit",[119,705,706],{},"git reset --hard"," kennt — und eine Default-Liste, die das alles zulässt, wäre nicht mehr ",[20,709,710],{},"safe",[13,712,713,714,717],{},"Ich hätte jetzt einen Standing Grant pro Read-Subcommand anlegen können. Mache ich nicht, weil ich nicht so über Git denke. Ich denke ",[20,715,716],{},"alles read-only bei Git ist okay",". Das ist eine User-Entscheidung auf der Klassen-Ebene, und User-Entscheidungen auf der Klassen-Ebene sind genau was Standing Grants sein sollen — wenn die Sprache es hergibt.",[13,719,720,721,724,725,728,729,732,733,55,736,739,740,743,744,746],{},"Dafür kam Glob. ",[119,722,723],{},"cliAuthorizationDetailCovers()"," — die Funktion, die entscheidet ob ein gewährter Scope einen eingehenden Request covered — behandelt ",[119,726,727],{},"*"," in granted Selector-Values als POSIX-Shell-Glob. Ein Standing Grant mit ",[119,730,731],{},"resource_chain_template: \"file://{path:/home/patrick/projects/*}\""," matched ",[119,734,735],{},"/home/patrick/projects/openape/README.md",[119,737,738],{},"/home/patrick/projects/blog/2026-04-21.md",", aber nicht ",[119,741,742],{},"/etc/passwd",". Selector-Werte ohne ",[119,745,727],{}," bleiben literale Gleichheit. Alle bestehenden Standing Grants matchen weiter wie vorher.",[13,748,749,750,753,754,407,757,760,761,764],{},"Im UI sitzt das hinter einem Wizard, den ich mobile-first gebaut habe. Erster Schritt: ",[20,751,752],{},"tipp ein Beispiel-Kommando ein, das du zuletzt gebraucht hast",". Der Wizard resolved das gegen die registrierte Shape und zieht die typed Slots heraus. Zweiter Schritt: jeden Slot auf ",[20,755,756],{},"Literal",[20,758,759],{},"Any"," oder ",[20,762,763],{},"Pattern"," setzen, mit Live-Preview die ein paar Beispiele zeigt, die der Pattern match bzw. nicht match. Dritter Schritt: Risk-Cap, optionale Duration, Begründung.",[13,766,767],{},"Die meisten Approvals, die ich heute mache, mache ich vom Handy. Wenn das Authoring von Pre-Authorizations am Desktop gebunden wäre, wäre es genau ausserhalb des Moments, in dem ich es tun will.",[28,769,771],{"id":770},"zwei-approval-layer","Zwei Approval-Layer",[13,773,774],{},"Das Eigentliche, das mir beim Umbau klar wurde, ist nicht die Mechanik. Es ist der Layer-Schnitt.",[13,776,777,778,780,781,784],{},"Es gibt zwei Ebenen, auf denen Approval stattfinden kann. Die eine ist ",[20,779,763],{},": eine User-Entscheidung, welche Klasse von Dingen pre-authorized ist. Die andere ist ",[20,782,783],{},"Command",": eine live Entscheidung, ob dieses eine konkrete Ding jetzt gerade okay ist.",[13,786,787,788,791,792,795,796,799,800,803,804,807,808,55,811,814],{},"Die naheliegende Alternative — Cache — erweitert Command zeitlich (",[20,789,790],{},"dieser approved Command gilt noch fünf Minuten für alle ähnlichen","), ohne eine neue Ebene einzuziehen. Das verwandelt ",[20,793,794],{},"einmal approved"," in ",[20,797,798],{},"immer approved, für N Minuten",". sudo-timestamp. ",[119,801,802],{},"sudoers"," mit ",[119,805,806],{},"timestamp_timeout",". Für Menschen handhabbar, für einen Agent kollabiert die Unterscheidung zwischen ",[20,809,810],{},"einem approved Kommando",[20,812,813],{},"allen Kommandos in den nächsten N Minuten"," vollständig.",[13,816,817,818,821,822,832],{},"Standing Grants ziehen die neue Ebene explizit ein. Pattern ist ein erstklassiges Artefakt: UI, Owner, Audit-Log, Revoke-Funktion, Inventar. Ich kann auf ",[119,819,820],{},"/agents/claude@home"," reingehen und sehen: ",[20,823,824,825,828,829,257],{},"diesem Agent habe ich drei Standing Grants erteilt. Eines für ",[119,826,827],{},"git.status",", eines für Safe Commands, eines für ",[119,830,831],{},"file://projects/*"," Jedes einzelne revokebar, ohne die anderen zu berühren. Command bleibt live, per Kommando, Approver-gebunden. Alles, was kein Pattern covered, landet in der Approve-Seite.",[13,834,835,836,839,840,257],{},"Die Frage ist nicht ",[20,837,838],{},"wie lange gilt ein Approval",". Die Frage ist ",[20,841,842],{},"welche Klasse bin ich bereit, vorher zu beschreiben",[28,844,846],{"id":845},"die-intelligenz-war-im-falschen-layer","Die Intelligenz war im falschen Layer",[13,848,849],{},"Zurück zum Anfang. Die client-seitige Version hat die Widening-Entscheidung dem Agent überlassen, weil ich dachte, Agent-Intelligenz läuft auch über Meta-Reflexion über den eigenen Workflow. Das tut sie nicht, zumindest nicht in aktuellen Tool-Use-Flows. Der Agent kennt sich mit dem aus, was er kennt — dem konkreten nächsten Command. Er denkt nicht über das Muster dahinter.",[13,851,852,853,407,856,859],{},"Die Intelligenz über das Muster war an der falschen Stelle. Sie gehört zum User. Der User hat den Workflow-Kontext — ",[20,854,855],{},"dieser Agent ist mein Coding-Agent, der liest viel, schreibt gelegentlich",[20,857,858],{},"dieser andere Agent ist ein Sync-Job, der einen engen Scope hat, aber oft läuft",". Der User macht aus diesem Kontext bewusste Klassen-Aussagen. Der Agent führt seine konkreten Tool-Use-Steps aus, und für die, die in einer der Klassen liegen, passiert das Approval stillschweigend.",[13,861,862],{},"Das ist dasselbe Pattern wie im Rest von OpenApe: Infrastructure over Instructions. Ich mache nicht den Agent schlauer. Ich hänge die Struktur anders. Widening-Entscheidungen werden nicht vom Agent getroffen, sie werden vom User getroffen, und die Infrastruktur sorgt dafür, dass beide Seiten voneinander profitieren, ohne direkt zu verhandeln.",[28,864,866],{"id":865},"hygiene","Hygiene",[13,868,869,870,873,874,877],{},"Was nach zwei Wochen sichtbar geworden ist: ich habe mehr Standing Grants als ich erwartet hatte. Irgendwann braucht es ",[20,871,872],{},"zeig mir alle, die ich in den letzten N Tagen erteilt und nie benutzt habe"," und einen One-Click-Revoke. Das ist kein Sicherheits-Feature, das ist Reinigung. Ohne die verwässert das Modell — und ein verwässertes Modell ist nach kurzer Zeit kein Modell mehr, sondern nur eine lange Liste von ",[20,875,876],{},"ja"," aus der Vergangenheit.",[28,879,881],{"id":880},"der-schnitt","Der Schnitt",[13,883,884],{},"Nicht jedes Kommando braucht einen Approval. Aber jedes approved Pattern braucht einen menschlichen Autor.",[13,886,887],{},"Der Autor des Patterns bin ich — mit Namen, mit Audit-Row, mit Revoke-Knopf. Der Approver des einzelnen Commands bin entweder ich (wenn kein Pattern greift) oder das Pattern, das ich gebaut habe (wenn eines greift). An keiner Stelle ist der Agent selbst im Authorizierungs-Pfad. Das war vorher anders — nicht in der Theorie, aber in dem stillen Verlass darauf, dass der Agent schon selbst Widening-Anfragen stellen würde. Hat er nicht.",[13,889,890,891,895,896,899],{},"Im ",[306,892,894],{"href":893},"/de/blog/heute-soll-der-agent-tun-was-er-nicht-darf","vorigen Artikel"," hieß es: ",[20,897,898],{},"nicht der User wird approved, sondern der Crossing",". Standing Grants ändern daran nichts. Sie machen nur, dass das Modell weiter funktioniert, wenn der Agent hundertmal pro Stunde etwas tut — nicht weil der Agent schlauer geworden ist, sondern weil ich auf der richtigen Ebene die richtigen Aussagen machen kann.",[298,901],{},[13,903,904],{},[20,905,906,907,910,911,914,915,257],{},"Code: ",[306,908,311],{"href":308,"rel":909},[310],", MIT-lizenziert. Die Mechanik lebt in ",[119,912,913],{},"@openape/grants",", das UI in ",[119,916,917],{},"openape-free-idp",{"title":314,"searchDepth":315,"depth":315,"links":919},[920,921,922,923,924,925,926,927,928,929],{"id":370,"depth":315,"text":371},{"id":423,"depth":315,"text":424},{"id":489,"depth":315,"text":490},{"id":526,"depth":315,"text":527},{"id":609,"depth":315,"text":610},{"id":681,"depth":315,"text":682},{"id":770,"depth":315,"text":771},{"id":845,"depth":315,"text":846},{"id":865,"depth":315,"text":866},{"id":880,"depth":315,"text":881},"2026-04-21","Ich hatte Widening von Anfang an eingebaut — im Agent, client-seitig, weil ich dachte der Agent ist die Intelligenz und weiß was er als nächstes braucht. Er hat's nie benutzt. Wie ich erkannt habe, dass Widening zum User gehört, nicht zum Agent — und was daraus wurde.",false,{},"/blog/de/von-prompts-zu-mustern",{"title":345,"description":931},"blog/de/von-prompts-zu-mustern",[938,338,527,336,340],"OpenApe","from-prompts-to-patterns","REWgh4hDUS0ZMqBIuOgqFBPQ8zVwiaLTrG4hkFeEkBE",{"id":942,"title":943,"author":8,"body":944,"date":1777,"description":1778,"draft":932,"extension":330,"image":3,"meta":1779,"navigation":329,"path":1780,"seo":1781,"stem":1782,"tags":1783,"translationKey":1785,"__hash__":1786},"blog_de/blog/de/heute-soll-der-agent-tun-was-er-nicht-darf.md","Heute soll der Agent tun, was er eigentlich nicht darf",{"type":10,"value":945,"toc":1765},[946,949,960,963,967,985,988,996,1006,1021,1032,1036,1043,1077,1080,1084,1091,1101,1106,1120,1124,1131,1134,1140,1155,1268,1271,1279,1285,1290,1364,1384,1387,1391,1404,1410,1420,1424,1431,1554,1557,1561,1568,1581,1588,1592,1599,1605,1611,1615,1621,1624,1630,1633,1639,1646,1668,1677,1683,1688,1691,1697,1703,1710,1714,1721,1724,1726,1733,1735,1761],[13,947,948],{},"Heute soll mein Agent etwas tun, was er eigentlich nicht darf.",[13,950,951,952,955,956,959],{},"Das ist nicht metaphorisch gemeint. openclaw läuft auf einem Mini-PC zuhause als eigener OS-User namens ",[119,953,954],{},"openclaw",". Dieser User hat keinen sudo-Eintrag. Kein Passwort. Nichts in ",[119,957,958],{},"/etc/sudoers",". Kein NOPASSWD. Das ist Absicht — der ganze Sinn davon, den Agent als eigenen unprivilegierten User laufen zu lassen, ist dass ich seinem Handeln niemals implizit vertrauen muss.",[13,961,962],{},"Heute braucht er einen Befehl als root.",[28,964,966],{"id":965},"der-standard-reflex","Der Standard-Reflex",[13,968,969,970,973,974,977,978,981,982],{},"Wenn man im Internet nach ",[20,971,972],{},"\"how to give an AI agent sudo\""," sucht, bekommt man bei so ziemlich jedem Tutorial denselben Vorschlag: trage den Agent-User in ",[119,975,976],{},"/etc/sudoers.d/"," ein, setz ein ",[119,979,980],{},"NOPASSWD: ALL",", und fertig. Manchmal wird die Zeile etwas eingeschränkt auf bestimmte Binaries, aber die Grundhaltung ist dieselbe: ",[20,983,984],{},"einmal vertraut, immer vertraut, keine Rückfragen.",[13,986,987],{},"Ich habe das nie gemacht und werde es nie machen. Die Gründe sind nicht Paranoia, sondern Architektur.",[13,989,990,995],{},[152,991,992,994],{},[119,993,980],{}," ist keine Sicherheitsaussage. Es ist der Verzicht auf Sicherheit."," Der sudo-Mechanismus existiert, um einen Nachweis der Authorisierung einzufordern. Wenn dieser Nachweis entfällt, ist der verbliebene Effekt von sudo nur noch das Umschalten der Effective UID. Der gesamte Audit- und Policy-Anteil ist weg.",[13,997,998,1001,1002,1005],{},[152,999,1000],{},"Der Agent wird zum ewig-privilegierten User."," Wer Zugriff auf den laufenden Agent-Prozess bekommt — ein kompromittiertes npm-Package in einem Tool, eine Prompt-Injection in einem Remote-Dokument, ein fehlerhafter Hook — hat ab diesem Moment Root auf der Maschine. Nicht eine Stunde, nicht ",[20,1003,1004],{},"\"wenn der Agent aktiv arbeitet\"",", sondern strukturell und permanent.",[13,1007,1008,1017,1018,1020],{},[152,1009,1010,1011,1013,1014,257],{},"Cache macht aus ",[20,1012,794],{}," automatisch ",[20,1015,1016],{},"immer approved"," Standard-sudo hat ein ",[119,1019,806],{}," von 5 bis 15 Minuten. Für interaktive Menschen ist das bequem. Für einen Agent, der theoretisch im Sekundentakt Commands absetzen könnte, bedeutet es: ein einziges approved Kommando genügt, und der komplette Zeitraum danach ist offen.",[13,1022,1023,1024,1027,1028,1031],{},"Zusammen ergibt das: ",[119,1025,1026],{},"NOPASSWD"," ist nicht eine ",[20,1029,1030],{},"etwas schwächere"," Variante von richtigem sudo. Es ist kategorial etwas anderes. Es gibt den User auf als Sicherheitsgrenze.",[28,1033,1035],{"id":1034},"was-sudo-annimmt-und-warum-diese-annahmen-bei-agents-brechen","Was sudo annimmt, und warum diese Annahmen bei Agents brechen",[13,1037,1038,1039,1042],{},"sudo ist aus einer Welt gewachsen, in der ",[20,1040,1041],{},"der User"," vor dem Terminal sitzt und tippt. Drei Annahmen sind in dieses Design eingeschrieben:",[146,1044,1045,1058,1068],{},[149,1046,1047,1050,1051,55,1054,1057],{},[152,1048,1049],{},"Der Invoker ist die authorisierte Person."," Das Passwort ist die Brücke zwischen ",[20,1052,1053],{},"Körper vor Tastatur",[20,1055,1056],{},"Eintrag in /etc/passwd",". Wenn der Invoker ein Prozess ohne Gedächtnis ist, gibt es keine solche Brücke. Ein Prozess kann ein Passwort halten, aber das Halten ist keine Authentifizierung — es ist nur Speicherung.",[149,1059,1060,1063,1064,1067],{},[152,1061,1062],{},"Cache-Optimierung ist gut."," Stimmt für interaktive Menschen, die im Laufe einer Admin-Session mehrere Kommandos absetzen und nicht jedes Mal ihr Passwort wiederholen wollen. Ist katastrophal für Agents, für die ",[20,1065,1066],{},"\"mehrere Kommandos hintereinander\""," der Normalfall ist und genau die Situation, die nicht-pauschal approved werden sollte.",[149,1069,1070,1073,1074,1076],{},[152,1071,1072],{},"Policy ist zur Konfigurationszeit entscheidbar."," Stimmt für Admin-Workflows mit bekannter Rollen-Matrix. Stimmt nicht für Agent-Workflows, die per Definition unvorhersehbar sind — niemand weiß um 14:32, welches Kommando um 14:37 nötig sein wird, also kann niemand es um 14:00 vorab in ",[119,1075,976],{}," schreiben.",[13,1078,1079],{},"Die drei Annahmen sind nicht falsch — sie sind für eine andere Welt gebaut. Die Welt der Menschen. Wenn man sie unverändert für Agents übernimmt, übernimmt man implizit das Vertrauensmodell der Menschen-Welt, ohne die Feedback-Schleifen, die es in der Menschen-Welt tragfähig machen (Sozialdruck, Auditierbarkeit auf Person, Gedächtnis).",[28,1081,1083],{"id":1082},"die-inversion","Die Inversion",[13,1085,1086,1087,1090],{},"Ich brauchte also einen Mechanismus, der die Frage ",[20,1088,1089],{},"\"darf dieser Prozess jetzt gerade diesen einen Befehl als root ausführen?\""," live beantwortet — nicht vorab, nicht gecacht, sondern einmalig pro Kommando, und nicht vom Agent selbst, sondern von einem Menschen.",[13,1092,1093,1094,1097,1098],{},"Das Ding, das ich dafür gebaut habe, heißt ",[119,1095,1096],{},"escapes",". Es ist ein setuid-root Binary, geschrieben in Rust, liegt öffentlich auf GitHub und hat genau eine Aufgabe: ",[20,1099,1100],{},"Einen Command mit elevated privileges ausführen, aber nur, wenn ein signierter Grant-Token aus einem OpenApe-IdP vorliegt, der für genau dieses Kommando, auf genau dieser Maschine, genau einmal gültig ist.",[72,1102,1103],{},[13,1104,1105],{},"Die harte Grenze bleibt hart. Nur der Übertritt wird protokolliert.",[13,1107,1108,1109,1112,1113,1115,1116,1119],{},"Der Agent-User bekommt ",[152,1110,1111],{},"nichts"," dazu. Er ist weiter unprivilegiert. Er hat weiter keinen Eintrag in ",[119,1114,802],{},". Was er bekommt, ist die Möglichkeit, einen Grant zu ",[20,1117,1118],{},"requesten"," — und wenn ich als Approver zustimme, dann ist für genau einen Bruchteil der Zeit, für genau einen Command, die Grenze offen. Nicht der User wird approved, sondern der Crossing.",[28,1121,1123],{"id":1122},"der-konkrete-flow","Der konkrete Flow",[13,1125,1126,1127,1130],{},"Hier ist, was passiert, wenn openclaw heute ",[119,1128,1129],{},"whoami"," als root ausführen will.",[13,1132,1133],{},"Der Agent ruft:",[625,1135,1138],{"className":1136,"code":1137,"language":630},[628],"apes run --as root -- whoami\n",[119,1139,1137],{"__ignoreMap":314},[13,1141,1142,1143,1146,1147,1150,1151,1154],{},"Ein einziger Command. Kein ",[119,1144,1145],{},"sudo",". Kein Passwort-Prompt. Keine bestehende Session. Die CLI ",[119,1148,1149],{},"apes"," sieht das ",[119,1152,1153],{},"--as root"," Flag und schaltet auf den escapes-Audience-Flow um. Sie erstellt einen Grant-Request am IdP mit einer Payload in dieser Form:",[625,1156,1160],{"className":1157,"code":1158,"language":1159,"meta":314,"style":314},"language-json shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","{\n  \"audience\": \"escapes\",\n  \"target_host\": \"mini.local\",\n  \"command\": [\"whoami\"],\n  \"decided_by\": \"patrick@hofmann.eco\"\n}\n","json",[119,1161,1162,1171,1197,1218,1242,1262],{"__ignoreMap":314},[1163,1164,1167],"span",{"class":1165,"line":1166},"line",1,[1163,1168,1170],{"class":1169},"sMK4o","{\n",[1163,1172,1173,1176,1180,1183,1186,1189,1192,1194],{"class":1165,"line":315},[1163,1174,1175],{"class":1169},"  \"",[1163,1177,1179],{"class":1178},"spNyl","audience",[1163,1181,1182],{"class":1169},"\"",[1163,1184,1185],{"class":1169},":",[1163,1187,1188],{"class":1169}," \"",[1163,1190,1096],{"class":1191},"sfazB",[1163,1193,1182],{"class":1169},[1163,1195,1196],{"class":1169},",\n",[1163,1198,1200,1202,1205,1207,1209,1211,1214,1216],{"class":1165,"line":1199},3,[1163,1201,1175],{"class":1169},[1163,1203,1204],{"class":1178},"target_host",[1163,1206,1182],{"class":1169},[1163,1208,1185],{"class":1169},[1163,1210,1188],{"class":1169},[1163,1212,1213],{"class":1191},"mini.local",[1163,1215,1182],{"class":1169},[1163,1217,1196],{"class":1169},[1163,1219,1221,1223,1226,1228,1230,1233,1235,1237,1239],{"class":1165,"line":1220},4,[1163,1222,1175],{"class":1169},[1163,1224,1225],{"class":1178},"command",[1163,1227,1182],{"class":1169},[1163,1229,1185],{"class":1169},[1163,1231,1232],{"class":1169}," [",[1163,1234,1182],{"class":1169},[1163,1236,1129],{"class":1191},[1163,1238,1182],{"class":1169},[1163,1240,1241],{"class":1169},"],\n",[1163,1243,1245,1247,1250,1252,1254,1256,1259],{"class":1165,"line":1244},5,[1163,1246,1175],{"class":1169},[1163,1248,1249],{"class":1178},"decided_by",[1163,1251,1182],{"class":1169},[1163,1253,1185],{"class":1169},[1163,1255,1188],{"class":1169},[1163,1257,1258],{"class":1191},"patrick@hofmann.eco",[1163,1260,1261],{"class":1169},"\"\n",[1163,1263,1265],{"class":1165,"line":1264},6,[1163,1266,1267],{"class":1169},"}\n",[13,1269,1270],{},"Der IdP schickt diesen Request an mich zur Approval. In der Browser-UI sehe ich den vollständigen Command, den Ziel-Host, den Agent, und die Approve/Deny-Buttons. Ich entscheide.",[13,1272,1273,1278],{},[1274,1275],"img",{"alt":1276,"src":1277},"Browser-Approval-Screen: Permission Request mit Command whoami, Target MinivonPatrick.fritz.box, Run as root, Approval Type Once — Approve und Deny Buttons","https://sos-at-vie-2.exo.io/dm-public/blog/2026-04-21/escapes-approval-screen.png"," Wenn ich approve, signiert mein Passkey den Grant, der IdP gibt ein JWT zurück, die CLI nimmt das JWT und ruft:",[625,1280,1283],{"className":1281,"code":1282,"language":630},[628],"escapes --grant \u003Cjwt> -- whoami\n",[119,1284,1282],{"__ignoreMap":314},[13,1286,1287,1289],{},[119,1288,1096],{}," läuft mit Effective UID 0 (setuid-Bit), verifiziert sieben Eigenschaften des Grants bevor es überhaupt daran denkt, irgendetwas auszuführen:",[554,1291,1292,1302,1308,1320,1333,1346,1355],{},[149,1293,1294,1297,1298,1301],{},[152,1295,1296],{},"Issuer"," ist in ",[119,1299,1300],{},"allowed_issuers"," — nur JWKS dieser IdPs werden gefetcht",[149,1303,1304,1307],{},[152,1305,1306],{},"JWT-Signatur"," ist gültig gegen die JWKS",[149,1309,1310,1297,1313,1316,1317,1319],{},[152,1311,1312],{},"Approver",[119,1314,1315],{},"allowed_approvers"," (das ist die Entsprechung zu ",[119,1318,802],{}," — aber für Menschen, nicht für Prozesse)",[149,1321,1322,1297,1325,1328,1329,1332],{},[152,1323,1324],{},"Audience",[119,1326,1327],{},"allowed_audiences"," (Default: ",[119,1330,1331],{},"[\"escapes\"]",")",[149,1334,1335,1339,1340,1342,1343],{},[152,1336,1337],{},[119,1338,1204],{}," matched den tatsächlichen Hostnamen dieser Maschine — ein Grant für ",[119,1341,1213],{}," funktioniert nicht auf ",[119,1344,1345],{},"server01",[149,1347,1348,1354],{},[152,1349,1350,1351],{},"Command / ",[119,1352,1353],{},"cmd_hash"," matched exakt den tatsächlich übergebenen Command",[149,1356,1357,1363],{},[152,1358,1359,1360],{},"IdP ",[119,1361,1362],{},"/consume"," bestätigt: dieses Grant-Token wurde noch nie eingelöst — Replay-Schutz",[13,1365,1366,1367,1369,1370,407,1373,1376,1377,1380,1381,257],{},"Erst wenn alle sieben Checks grün sind, sanitized ",[119,1368,1096],{}," das Environment (weg mit ",[119,1371,1372],{},"LD_PRELOAD",[119,1374,1375],{},"PATH"," auf Default, etc.) und ruft ",[119,1378,1379],{},"execvp(\"whoami\", [])",". Der Command läuft als root, genau einmal, sieht genau die argv, die ich approved habe, auf der Maschine, auf der ich approved habe, und schreibt einen vollständigen Audit-Log-Eintrag in ",[119,1382,1383],{},"/var/log/openape/audit.log",[13,1385,1386],{},"Nach dem Exit ist die Sache vorbei. Der Grant ist consumed. Wenn der Agent zwei Minuten später ein weiteres root-Kommando braucht, fängt der ganze Tanz wieder von vorne an. Keine Cache. Kein Timestamp. Kein Rest-Vertrauen.",[28,1388,1390],{"id":1389},"sudoers-ist-leer-geblieben","sudoers ist leer geblieben",[13,1392,1393,1394,1396,1397,1400,1401,1403],{},"Während ich diese Serie schreibe, habe ich mir die ",[119,1395,1145],{},"-History angeschaut, um zu sehen wann und wofür ich auf meinem Mini-PC in den letzten Wochen ",[20,1398,1399],{},"manuell"," sudo verwendet habe. Und um zu prüfen, dass der ",[119,1402,954],{},"-User tatsächlich leer durch jede Zeile geht.",[13,1405,1406],{},[1274,1407],{"alt":1408,"src":1409},"Terminal-Screenshot: Überprüfung von /etc/sudoers und /etc/sudoers.d/ — der openclaw-Benutzer taucht nirgendwo auf; gleichzeitig sieht man zwei kürzlich ausgeführte escapes-Kommandos mit verschiedenen Grant-IDs, die beide regulär approved und audited wurden","https://sos-at-vie-2.exo.io/dm-public/blog/TBD-escapes/sudoers-two-grants.png",[13,1411,1412,1413,1415,1416,1419],{},"Das ist der Punkt, auf den es ankommt. Die ",[119,1414,802],{},"-Konfiguration dieser Maschine hat sich durch den gesamten Prozess ",[152,1417,1418],{},"nicht verändert",". Kein neuer Eintrag. Kein NOPASSWD. Kein privilegierter User. Was sich geändert hat, ist dass es einen zweiten Pfad gibt — nicht durch sudo, sondern neben sudo — auf dem Commands per Grant statt per Passwort elevated werden können. sudo bleibt wofür es gedacht war: interaktive Menschen, die am Terminal sitzen und tippen. escapes übernimmt den Agent-Fall, den sudo nie modelliert hat.",[28,1421,1423],{"id":1422},"der-kategorische-unterschied","Der kategorische Unterschied",[13,1425,1426,1427,1430],{},"Die Frage, die unter meinem ape-shell-Post kam: ",[20,1428,1429],{},"\"Geht das nicht auch einfacher? Sudoers mit Command-Whitelisting?\""," Die Antwort ist nein, und der Unterschied ist nicht graduell. Er ist strukturell:",[1432,1433,1434,1449],"table",{},[1435,1436,1437],"thead",{},[1438,1439,1440,1444,1447],"tr",{},[1441,1442,1443],"th",{},"Achse",[1441,1445,1446],{},"sudo (mit NOPASSWD)",[1441,1448,1096],{},[1450,1451,1452,1468,1481,1497,1512,1528],"tbody",{},[1438,1453,1454,1460,1465],{},[1455,1456,1457],"td",{},[152,1458,1459],{},"Wann ist die Policy entschieden?",[1455,1461,1462,1463],{},"Zur Konfigurationszeit, statisch in ",[119,1464,958],{},[1455,1466,1467],{},"Zur Laufzeit, pro Kommando, fresh",[1438,1469,1470,1475,1478],{},[1455,1471,1472],{},[152,1473,1474],{},"Wer entscheidet?",[1455,1476,1477],{},"Der Invoker — also der Agent selbst",[1455,1479,1480],{},"Ein separater Approver, vom Invoker getrennt",[1438,1482,1483,1488,1491],{},[1455,1484,1485],{},[152,1486,1487],{},"Credential-Lifetime",[1455,1489,1490],{},"Cache, 5-15 Minuten Standard",[1455,1492,1493,1494,1496],{},"Single-use JWT, ",[119,1495,1362],{}," sperrt Replay",[1438,1498,1499,1504,1507],{},[1455,1500,1501],{},[152,1502,1503],{},"Command-Binding",[1455,1505,1506],{},"Path-Prefix Matching (notorisch leaky)",[1455,1508,1509,1511],{},[119,1510,1353],{}," im signierten JWT",[1438,1513,1514,1519,1525],{},[1455,1515,1516],{},[152,1517,1518],{},"Host-Binding",[1455,1520,1521,1522],{},"Statisch in ",[119,1523,1524],{},"Host_Alias",[1455,1526,1527],{},"Kryptographisch im JWT verankert",[1438,1529,1530,1535,1538],{},[1455,1531,1532],{},[152,1533,1534],{},"Audit",[1455,1536,1537],{},"Lokal, oft nicht aggregiert",[1455,1539,1540,1541,407,1544,407,1547,407,1549,407,1552],{},"JSONL mit ",[119,1542,1543],{},"grant_id",[119,1545,1546],{},"approver",[119,1548,1353],{},[119,1550,1551],{},"issuer",[119,1553,1204],{},[13,1555,1556],{},"Jede einzelne Zeile ist ein Vertrauens-Delegations-Punkt, den sudo an die Konfigurationszeit verlagert und escapes an die Laufzeit. Diese Verlagerung ist der eigentliche Inhalt. Alles andere — das Rust-Binary, das JWT, die sieben Checks — sind die Mechanismen, mit denen die Verlagerung technisch umgesetzt wird.",[28,1558,1560],{"id":1559},"mensch-und-agent-sind-auf-protokoll-ebene-gleich","Mensch und Agent sind auf Protokoll-Ebene gleich",[13,1562,1563,1564,1567],{},"Ein Nebeneffekt, der mir erst im Bauen klar wurde: ich benutze ",[119,1565,1566],{},"apes run --as root --"," jetzt auch für mich selbst. Wenn ich auf einem der Hosts, die meinem Team gehören, ein privilegiertes Kommando brauche, tippe ich denselben Command, den mein Agent tippen würde. Derselbe Flow. Dieselbe Grant-Anfrage. Derselbe Approval-Schritt.",[13,1569,1570,1571,1573,1574,1577,1578,1580],{},"Der einzige Unterschied: wenn ",[20,1572,519],{}," das Kommando anstoße, ist der Approver ein Team-Kollege. Wenn der ",[20,1575,1576],{},"Agent"," es anstößt, bin ich der Approver. Gleiche Infrastruktur, andere Rolle. Das ist nicht zufällig so. Es ist das Prinzip ",[20,1579,1560],{},", aus dem die ganze OpenApe-Story herkommt, konkret angewandt auf den Privilegien-Layer.",[13,1582,1583,1584,1587],{},"Die Konsequenz ist, dass escapes kein Agent-Spezial-Tool ist. Es ist allgemeine Infrastruktur, die nebenbei auch von Agents benutzt werden kann. Dieselbe Gleichbehandlung, die OpenApe beim Login-Flow in den Vordergrund gestellt hat (",[20,1585,1586],{},"\"der Mensch hat eine Session, der Agent hat eine Session — die Infrastruktur weiß nicht, welcher ist welcher\"","), wiederholt sich hier beim Elevation-Flow.",[28,1589,1591],{"id":1590},"was-das-kostet","Was das kostet",[13,1593,1594,1595,1598],{},"Wer im Grant-System arbeitet, wartet auf Menschen. Wenn openclaw um 3 Uhr nachts entscheidet, dass er ein privilegiertes Kommando braucht, dann wartet er. Er weckt mich nicht auf. Er fährt nicht fort ohne mich. Das gilt nicht nur für escapes, das gilt für jeden Grant im gesamten OpenApe-Stack, solange sich der Agent am Rand von dem bewegt was er darf. ",[20,1596,1597],{},"Ask first"," ist der ganze Punkt. Aber es ist Reibung, und wer diese Reibung nicht will, ist hier falsch.",[13,1600,1601,1602,1604],{},"Es braucht Infrastruktur. Ohne einen laufenden OpenApe-kompatiblen Identity Provider funktioniert nichts — jemand muss die Signatur-Keys halten, die JWKS veröffentlichen, ",[119,1603,1362],{}," als Replay-Schutz fahren. Kein IdP, kein escapes.",[13,1606,1607,1608,1610],{},"Und escapes ist Vibe-Coded Software. Das Sicherheits-Konzept dahinter ist das, was zählt — nicht ob mein Rust-Code fehlerfrei ist. ",[119,1609,1145],{}," hat über 40 Jahre Open-Source-Audit-Zeit hinter sich. escapes hat wahrscheinlich Bugs, und Linus Torvalds würde mir da vermutlich recht geben. Der Code ist klein, MIT-lizenziert, und auf GitHub — wer es produktiv einsetzen will, soll es sich vorher ansehen.",[28,1612,1614],{"id":1613},"wie-man-anfängt","Wie man anfängt",[625,1616,1619],{"className":1617,"code":1618,"language":630},[628],"cargo install openape-escapes\n",[119,1620,1618],{"__ignoreMap":314},[13,1622,1623],{},"Dann das Binary privilegieren — entweder via Linux Capabilities:",[625,1625,1628],{"className":1626,"code":1627,"language":630},[628],"sudo setcap cap_setuid+ep $(which escapes)\n",[119,1629,1627],{"__ignoreMap":314},[13,1631,1632],{},"Oder klassisch via setuid-Bit:",[625,1634,1637],{"className":1635,"code":1636,"language":630},[628],"sudo chown root:root $(which escapes) && sudo chmod u+s $(which escapes)\n",[119,1638,1636],{"__ignoreMap":314},[13,1640,1641,1642,1645],{},"Dann die Trust-Beziehung einrichten — ",[119,1643,1644],{},"/etc/openape/config.toml"," definiert, welchem IdP escapes vertraut und wer Grants approven darf:",[625,1647,1651],{"className":1648,"code":1649,"language":1650,"meta":314,"style":314},"language-toml shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","[security]\nallowed_issuers = [\"https://id.openape.at\"]\nallowed_approvers = [\"patrick@hofmann.eco\"]\n","toml",[119,1652,1653,1658,1663],{"__ignoreMap":314},[1163,1654,1655],{"class":1165,"line":1166},[1163,1656,1657],{},"[security]\n",[1163,1659,1660],{"class":1165,"line":315},[1163,1661,1662],{},"allowed_issuers = [\"https://id.openape.at\"]\n",[1163,1664,1665],{"class":1165,"line":1199},[1163,1666,1667],{},"allowed_approvers = [\"patrick@hofmann.eco\"]\n",[13,1669,1670,1671,1673,1674,1676],{},"Zwei Zeilen. ",[119,1672,1300],{}," ist die Liste der IdPs deren JWKS akzeptiert werden. ",[119,1675,1315],{}," ist das Äquivalent zu sudoers — aber für Menschen, nicht für Prozesse. Alles andere hat sinnvolle Defaults.",[13,1678,1679,1680,1682],{},"Dann ",[119,1681,1149],{}," installieren, einen IdP konfigurieren, und als erstes Kommando:",[625,1684,1686],{"className":1685,"code":1137,"language":630},[628],[119,1687,1137],{"__ignoreMap":314},[13,1689,1690],{},"Wenn alles richtig aufgesetzt ist, bekommst du einen Approval-Request im Browser, approvst, und siehst:",[625,1692,1695],{"className":1693,"code":1694,"language":630},[628],"root\n",[119,1696,1694],{"__ignoreMap":314},[13,1698,1699],{},[1274,1700],{"alt":1701,"src":1702},"Telegram-Chat: der Agent führt whoami als root aus — ein Grant wird angefordert, nach Approval kommt die Antwort: root","https://sos-at-vie-2.exo.io/dm-public/blog/2026-04-21/escapes-whoami-root.png",[13,1704,1705,1706,1709],{},"Das ist alles. Ein Wort. Und dahinter stehen sieben Verifikations-Schritte, ein signierter JWT, ein Audit-Log-Eintrag, und ein ausdrücklich zustimmender Mensch. Derselbe Output wie bei ",[119,1707,1708],{},"sudo whoami",", aber ein fundamental anderes Vertrauensmodell.",[28,1711,1713],{"id":1712},"warum-das-für-mich-das-richtige-muster-ist","Warum das für mich das richtige Muster ist",[13,1715,1716,1717,1720],{},"Ich habe in den letzten Wochen mehrere Layer von OpenApe öffentlich gebaut — Identity, ape-shell, Claude Grant Gate, jetzt escapes. Jeder einzelne Layer ist eine Variation auf dieselbe These: ",[20,1718,1719],{},"Infrastructure over Instructions",". Nicht der Agent wird gebeten, sich an Regeln zu halten. Die Umgebung lässt Regelverletzungen strukturell nicht zu.",[13,1722,1723],{},"escapes ist der Layer, auf dem das am deutlichsten wird, weil er am meisten wehtut, wenn man es falsch macht. Wer einem Agent root gibt, hat effektiv die Maschine abgegeben. Wer einem Agent einen Single-Use-Grant gibt, hat nur diese eine Operation abgegeben. Der Unterschied ist alles.",[13,1725,1105],{},[13,1727,1728,1729,1732],{},"Und ja — ",[119,1730,1731],{},"cat /etc/shadow"," geht auch. Audit. Denied.",[298,1734],{},[13,1736,1737],{},[20,1738,1739,1742,1743,1748,1749,1754,1755,1760],{},[119,1740,1741],{},"openape-escapes@0.4.0"," ist auf ",[306,1744,1747],{"href":1745,"rel":1746},"https://crates.io/crates/openape-escapes",[310],"crates.io"," und auf ",[306,1750,1753],{"href":1751,"rel":1752},"https://github.com/openape-ai/escapes",[310],"GitHub"," verfügbar, MIT-lizenziert. Die ",[306,1756,1759],{"href":1757,"rel":1758},"https://www.delta-mind.at/de/blog",[310],"vorigen Artikel dieser Serie"," erzählen wie OpenApe, ape-shell, und die Grant-Integration dahinter entstanden sind.",[1762,1763,1764],"style",{},"html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}",{"title":314,"searchDepth":315,"depth":315,"links":1766},[1767,1768,1769,1770,1771,1772,1773,1774,1775,1776],{"id":965,"depth":315,"text":966},{"id":1034,"depth":315,"text":1035},{"id":1082,"depth":315,"text":1083},{"id":1122,"depth":315,"text":1123},{"id":1389,"depth":315,"text":1390},{"id":1422,"depth":315,"text":1423},{"id":1559,"depth":315,"text":1560},{"id":1590,"depth":315,"text":1591},{"id":1613,"depth":315,"text":1614},{"id":1712,"depth":315,"text":1713},"2026-04-17","Mein Agent will einen Befehl als root ausführen. Er hat keinen sudo-Eintrag, kein Passwort, nichts in /etc/sudoers. Das ist Absicht. Hier ist der Weg, den ich gebaut habe, damit er trotzdem genau ein Kommando ausführen kann — approved von einem Menschen, auditiert, nicht cachebar, nicht wiederverwendbar.",{},"/blog/de/heute-soll-der-agent-tun-was-er-nicht-darf",{"title":943,"description":1778},"blog/de/heute-soll-der-agent-tun-was-er-nicht-darf",[938,1096,338,336,1784,340],"Security","todays-agent-forbidden-action","cb6HTOESsmBv88HaA4izXlPnS6dJ8jeMlhn8wS66otw",{"id":1788,"title":1789,"author":8,"body":1790,"date":2690,"description":2691,"draft":932,"extension":330,"image":3,"meta":2692,"navigation":329,"path":2693,"seo":2694,"stem":2695,"tags":2696,"translationKey":2699,"__hash__":2700},"blog_de/blog/de/wenn-dein-agent-nicht-tut-was-du-willst-frag-ihn-warum.md","Wenn dein Agent nicht tut was du willst, frag ihn warum",{"type":10,"value":1791,"toc":2675},[1792,1799,1802,1815,1818,1824,1827,1831,1838,1844,1851,1854,1858,1861,1866,1869,1876,1882,1888,1891,1895,1902,1909,1912,1940,1961,1972,1979,1988,1992,1995,1998,2032,2035,2074,2081,2095,2099,2113,2118,2139,2146,2156,2159,2215,2222,2226,2229,2235,2241,2254,2260,2276,2279,2283,2286,2291,2294,2301,2304,2307,2337,2340,2344,2362,2365,2368,2375,2382,2388,2398,2402,2409,2423,2432,2436,2443,2450,2476,2487,2493,2500,2503,2509,2520,2524,2527,2532,2538,2550,2557,2564,2568,2578,2593,2603,2610,2614,2619,2642,2649,2651,2656,2672],[13,1793,1794,1795,1798],{},"Heute morgen habe ich meinem AI-Agent eine einfache Aufgabe gegeben: einen Shell-Befehl auf meinem Rechner zuhause ausführen. Der Agent heißt openclaw, läuft lokal, und arbeitet gegen eine CLI die ich in den letzten Tagen schrittweise gebaut habe — ",[119,1796,1797],{},"@openape/apes",". Die CLI hat einen non-blocking async-Default-Modus: Command wird abgeschickt, ein Grant-Request wird am IdP erzeugt, die URL zum Approven wird in den Terminal-Output gedruckt, und der Command-Prozess exited sofort mit Status 0. Der User approved im Browser, der Agent holt sich später mit einem zweiten Call das Ergebnis.",[13,1800,1801],{},"Elegantes Muster. Funktioniert für Menschen am Terminal problemlos. Funktioniert bei openclaw heute morgen — nicht.",[13,1803,1804,1805,1811,1812],{},"Was openclaw gemacht hat: den Command abgeschickt, den Output gelesen (inklusive der Grant-ID, der Approve-URL, und der Zeile ",[20,1806,1807,1808],{},"\"Execute: apes grants run ",[1809,1810,1182],"id",{},"), und mir in Telegram gemeldet: ",[20,1813,1814],{},"\"Der Command wurde erstellt, bitte approve im Browser.\"",[13,1816,1817],{},"Und dann still.",[13,1819,1820,1821],{},"Ich habe im Browser approved, gewartet, und nichts ist passiert. Ich habe openclaw nochmal angeschrieben, ob er weiter weiß. Die Antwort: ",[20,1822,1823],{},"\"Ich warte auf deine Bestätigung dass du approved hast.\"",[13,1825,1826],{},"Das ist nicht was ich wollte. Das ist Blocking-Mode im Agent-Kostüm: non-blocking async auf der CLI-Ebene, aber der Agent verhält sich wie bei blocking, weil er mich manuell braucht um weiterzumachen. Beide Welten gleichzeitig, keiner der Vorteile.",[28,1828,1830],{"id":1829},"die-for-agents-zeile","Die \"For agents:\"-Zeile",[13,1832,1833,1834,1837],{},"Das Besondere an der Situation: ich hatte den Fall eigentlich vorgesehen. In der Output-Zeile die ",[119,1835,1836],{},"apes run"," im async-Modus druckt, steht explizit eine Instruktion direkt an den Agent adressiert:",[625,1839,1842],{"className":1840,"code":1841,"language":630},[628],"  For agents: poll `apes grants status \u003Cid> --json` every 10s, wait up to 5 minutes.\n              When .status == \"approved\", run `apes grants run \u003Cid>` to execute.\n              On \"denied\" or \"revoked\", stop and report to the user.\n              On timeout, stop and notify the user that approval has not happened.\n",[119,1843,1841],{"__ignoreMap":314},[13,1845,1846,1847,1850],{},"Diese Zeilen hatte ich einen Release vorher eingebaut, genau für den Fall. Ich wollte ",[152,1848,1849],{},"narrative Protokoll-Instruktionen"," als Kommunikationskanal zwischen CLI und LLM-Agent ausprobieren. Die Theorie: ein LLM liest den Output, findet die Anweisungen, folgt ihnen. Portable (jeder LLM-Agent sieht den Text), versioned (die Anweisungen embedden die aktuelle Policy), debugbar (auch ich kann sie mitlesen).",[13,1852,1853],{},"Die Theorie war richtig. Die Praxis war unzureichend.",[28,1855,1857],{"id":1856},"frag-ihn-warum","Frag ihn warum",[13,1859,1860],{},"Als ich merkte dass openclaw mich trotz der expliziten Anweisungen ignoriert, hätte ich vermutlich erst an die üblichen Debug-Schritte denken können: Logs checken, Output vergleichen, Protokoll-Traces lesen. Stattdessen habe ich etwas gemacht, das man bei einem deterministischen System nie machen würde: ich habe den Agent gefragt.",[13,1862,1863],{},[20,1864,1865],{},"\"Du hattest in dem Output eine klare Anweisung, was zu tun ist. Warum hast du sie nicht befolgt?\"",[13,1867,1868],{},"Die Antwort kam umgehend, und sie ist das wertvollste Artefakt der gesamten Woche:",[72,1870,1871],{},[13,1872,1873],{},[20,1874,1875],{},"\"Das war direkt an mich als Agent adressiert — ich hätte es einfach befolgen müssen. Ich hab's schlicht ignoriert.\"",[13,1877,1878],{},[1274,1879],{"alt":1880,"src":1881},"Agent-Antwort im Telegram-Chat: der Agent gibt zu, die For-agents-Instruktion im Output direkt ignoriert zu haben","https://sos-at-vie-2.exo.io/dm-public/blog/2026-04-16/quote-ignored.png",[13,1883,1884,1885],{},"Das ist die präziseste Selbstbeschreibung einer Agent-Fehlentscheidung die ich je gelesen habe. Kein Ausweichen, keine Rationalisierung, keine halluzinierte Alternativ-Begründung. Genau das was passiert ist: die Anweisung war da, der Agent hat sie gesehen, der Agent hat sie nicht befolgt, und er kann im Nachhinein nur sagen ",[20,1886,1887],{},"\"Ich hab's ignoriert.\"",[13,1889,1890],{},"Ein Mensch kann etwas ignorieren aus Widerstand, aus Überforderung, aus Unaufmerksamkeit. Ein LLM-Agent ignoriert etwas aus einem anderen Grund: weil seine inneren Prioritäts-Gewichte entschieden haben, dass dieser Teil des Inputs nicht wichtig ist. Die Frage ist dann: warum nicht?",[28,1892,1894],{"id":1893},"die-diagnose","Die Diagnose",[13,1896,1897,1898,1901],{},"Um das zu verstehen, habe ich den Exec-Runtime-Code von openclaw gelesen. openclaw ist der Agent-Gateway den ich für die Orchestrierung lokal laufen lasse. Der Tool-Call-Layer liegt in ",[119,1899,1900],{},"src/agents/bash-tools.exec.ts"," und den angrenzenden Files.",[13,1903,1904,1905,1908],{},"Das Erste was auffällt: stdout und stderr werden chronologisch interleaved. openclaw hat zwar zwei getrennte Handler, aber beide feeden in einen gemeinsamen ",[119,1906,1907],{},"aggregated","-Buffer. Was im internen State als zwei Streams existiert, wird für die Agent-Präsentation zu einem einzigen Content-Blob kollabiert. Content auf stderr statt stdout zu routen hätte null Effekt — der LLM sieht ohnehin alles als ein Dokument.",[13,1910,1911],{},"Interessanter ist, was mit dem exit code passiert. openclaw wickelt den Exec-Output in zwei verschiedene Tool-Result-Typen:",[146,1913,1914,1927],{},[149,1915,1916,1917,803,1920,1923,1924],{},"bei exit 0: ",[119,1918,1919],{},"textResult",[119,1921,1922],{},"status: \"completed\""," — das Framing für den LLM ist ",[20,1925,1926],{},"\"diese Aufgabe ist erfolgreich abgeschlossen\"",[149,1928,1929,1930,803,1933,1936,1937],{},"bei non-zero: ",[119,1931,1932],{},"failedTextResult",[119,1934,1935],{},"status: \"failed\""," — das Framing ist ",[20,1938,1939],{},"\"diese Aufgabe braucht Aufmerksamkeit, lies den Output sorgfältig\"",[13,1941,1942,1943,1946,1947,1949,1950,1952,1953,1956,1957,1960],{},"Das ist eine ",[152,1944,1945],{},"strukturelle"," Unterscheidung, nicht eine textuelle. Der Content ist technisch derselbe (sowohl ",[119,1948,1919],{}," als auch ",[119,1951,1932],{}," haben denselben ",[119,1954,1955],{},"content","-Array), aber die metadata sagt dem LLM etwas Unterschiedliches darüber, ",[20,1958,1959],{},"wie"," er den Content lesen soll.",[13,1962,1963,1964,1967,1968,1971],{},"Dazu kommt noch ein drittes Detail: bei non-zero exit hängt openclaw einen expliziten Suffix an den Output — ",[20,1965,1966],{},"\"(Command exited with code N)\"",". Der LLM sieht also sowohl die ",[20,1969,1970],{},"\"failed\"","-Annotation im metadata als auch den Exit-Code-Hinweis im Text selbst.",[13,1973,1974,1975,1978],{},"Alle drei Mechanismen arbeiten auf derselben Achse: exit code → tool result framing → LLM-Lesemodus. Und sie operieren ",[152,1976,1977],{},"vor"," dem eigentlichen Content. Der LLM hat bereits entschieden wie aufmerksam er den Content lesen wird, bevor er die erste Content-Zeile gelesen hat.",[13,1980,1981,1982,1984,1985,257],{},"Mein async-default-Output, so sauber er auch formuliert war, stand in einem ",[119,1983,1922],{}," Wrapper. Der LLM hatte keinen Grund, ihn aufmerksam zu lesen — das strukturelle Framing sagte ",[20,1986,1987],{},"\"alles gut, move on\"",[28,1989,1991],{"id":1990},"der-fix","Der Fix",[13,1993,1994],{},"Der Fix war eine Zeile. Genau eine.",[13,1996,1997],{},"Vorher:",[625,1999,2003],{"className":2000,"code":2001,"language":2002,"meta":314,"style":314},"language-typescript shiki shiki-themes material-theme-lighter material-theme material-theme-palenight","printPendingGrantInfo(grant, idp);\nreturn;\n","typescript",[119,2004,2005,2024],{"__ignoreMap":314},[1163,2006,2007,2011,2015,2018,2021],{"class":1165,"line":1166},[1163,2008,2010],{"class":2009},"s2Zo4","printPendingGrantInfo",[1163,2012,2014],{"class":2013},"sTEyZ","(grant",[1163,2016,2017],{"class":1169},",",[1163,2019,2020],{"class":2013}," idp)",[1163,2022,2023],{"class":1169},";\n",[1163,2025,2026,2030],{"class":1165,"line":315},[1163,2027,2029],{"class":2028},"s7zQu","return",[1163,2031,2023],{"class":1169},[13,2033,2034],{},"Nachher:",[625,2036,2038],{"className":2000,"code":2037,"language":2002,"meta":314,"style":314},"printPendingGrantInfo(grant, idp);\nthrow new CliExit(getAsyncExitCode());\n",[119,2039,2040,2052],{"__ignoreMap":314},[1163,2041,2042,2044,2046,2048,2050],{"class":1165,"line":1166},[1163,2043,2010],{"class":2009},[1163,2045,2014],{"class":2013},[1163,2047,2017],{"class":1169},[1163,2049,2020],{"class":2013},[1163,2051,2023],{"class":1169},[1163,2053,2054,2057,2060,2063,2066,2069,2072],{"class":1165,"line":315},[1163,2055,2056],{"class":2028},"throw",[1163,2058,2059],{"class":1169}," new",[1163,2061,2062],{"class":2009}," CliExit",[1163,2064,2065],{"class":2013},"(",[1163,2067,2068],{"class":2009},"getAsyncExitCode",[1163,2070,2071],{"class":2013},"())",[1163,2073,2023],{"class":1169},[13,2075,2076,2077,2080],{},"Und ",[119,2078,2079],{},"getAsyncExitCode()"," liefert per Default 75.",[13,2082,2083,2084,2086,2087,2090,2091,2094],{},"Wenn der async-Pfad genommen wird, wird der Process jetzt mit exit code 75 beendet statt mit 0. Der Output ist wortwörtlich identisch (dieselbe ",[119,2085,2010],{}," Funktion), aber der exit code ist anders. Das kippt das Tool-Result-Framing in openclaw von ",[119,2088,2089],{},"completed"," auf ",[119,2092,2093],{},"failed",", und das LLM liest den Body mit erhöhter Aufmerksamkeit.",[28,2096,2098],{"id":2097},"warum-75","Warum 75",[13,2100,2101,2102,2105,2106,2109,2110,1185],{},"75 ist kein zufälliger Wert. Es ist ",[119,2103,2104],{},"EX_TEMPFAIL"," aus ",[119,2107,2108],{},"sysexits.h",", einem BSD-Header der seit 1983 existiert. Die Konvention aus ",[119,2111,2112],{},"sysexits(3)",[72,2114,2115],{},[13,2116,2117],{},"EX_TEMPFAIL — temporary failure, indicating something that is not really an error. In sendmail, this means that a mailer (e.g.) could not create a connection, and the request should be reattempted later.",[13,2119,2120,2123,2124,2127,2128,2131,2132,2135,2136],{},[119,2121,2122],{},"sendmail"," hat das seit Jahrzehnten als ",[20,2125,2126],{},"\"mail delivery deferred, retry later\""," verwendet. ",[119,2129,2130],{},"postfix"," hat es übernommen. ",[119,2133,2134],{},"qmail"," auch. Es ist der Standard-Exit-Code für ",[20,2137,2138],{},"\"nicht kaputt, aber noch nicht fertig — probier es später nochmal.\"",[13,2140,2141,2142,2145],{},"Das ist semantisch exakt, was ein pending grant ist. Nicht ein Fehler (der Command ist syntaktisch korrekt, die Absicht ist klar, der Grant wurde erzeugt), sondern ein ",[152,2143,2144],{},"temporärer Aufschub"," bis eine zweite asynchrone Bedingung (menschliche Approval) erfüllt ist. Dann retry.",[13,2147,2148,2149,2151,2152,2155],{},"Und weil ",[119,2150,2108],{}," seit BSD-Zeiten Teil fast jedes Unix-Handbuchs ist, ist es auch in LLM-Trainingsdaten über Jahrzehnte verankert. Wenn der LLM nach ",[20,2153,2154],{},"\"exit code 75 meaning\""," sucht, findet er sofort eine klare Antwort: temporary failure, retry later. Das ist die semantische Brücke, die ich für die \"async grant\"-Semantik gebraucht habe, ohne sie selbst erfinden zu müssen.",[13,2157,2158],{},"Alternative Exit-Codes die ich erwogen habe:",[146,2160,2161,2170,2180,2197,2206],{},[149,2162,2163,2166,2167],{},[152,2164,2165],{},"1"," (POSIX general error) — zu generisch, ",[20,2168,2169],{},"\"etwas ist kaputt\"",[149,2171,2172,2175,2176,2179],{},[152,2173,2174],{},"2"," (shell usage error) — wird als ",[20,2177,2178],{},"\"User-Fehler\""," gelesen",[149,2181,2182,2185,2186,2189,2190,2193,2194],{},[152,2183,2184],{},"73"," (",[119,2187,2188],{},"EX_CANTCREAT",") — näher an ",[20,2191,2192],{},"\"resource unavailable\""," als an ",[20,2195,2196],{},"\"retry later\"",[149,2198,2199,2185,2202,2205],{},[152,2200,2201],{},"74",[119,2203,2204],{},"EX_IOERR",") — zu niedriglevelig",[149,2207,2208,2185,2211,2214],{},[152,2209,2210],{},"78",[119,2212,2213],{},"EX_CONFIG",") — spricht von Konfigurationsfehler",[13,2216,2217,2218,2221],{},"Alle schwächer gefittet als 75. Plus 75 hat die Bonus-Story mit sendmail's ",[20,2219,2220],{},"\"defer and retry\"","-Semantik.",[28,2223,2225],{"id":2224},"vorher-und-nachher","Vorher und nachher",[13,2227,2228],{},"Was openclaw jetzt sieht, mit dem Fix:",[13,2230,2231,2234],{},[152,2232,2233],{},"Output vor 0.10.0"," (exit 0):",[625,2236,2239],{"className":2237,"code":2238,"language":630},[628],"✔ Grant e887a7e3-... created (pending approval)\n  Approve:   https://id.openape.at/grant-approval?grant_id=e887a7e3-...\n  Execute:   apes grants run e887a7e3-...\n\n  For agents: poll `apes grants status e887a7e3-... --json` every 10s...\n",[119,2240,2238],{"__ignoreMap":314},[13,2242,2243,2244,407,2246,2249,2250,2253],{},"Tool-Wrapper: ",[119,2245,1922],{},[119,2247,2248],{},"exitCode: 0"," → LLM: ",[20,2251,2252],{},"\"task done, move on\""," → ignoriert den Agent-Block.",[13,2255,2256,2259],{},[152,2257,2258],{},"Output nach 0.10.0"," (exit 75):",[13,2261,2262,2263,407,2265,2268,2269,2249,2272,2275],{},"Identischer Output. Tool-Wrapper: ",[119,2264,1935],{},[119,2266,2267],{},"exitCode: 75",", plus automatischer Suffix ",[20,2270,2271],{},"\"(Command exited with code 75)\"",[20,2273,2274],{},"\"needs attention, read carefully\""," → findet die Agent-Instruktionen → pollt → approved → meldet Ergebnis.",[13,2277,2278],{},"Der Content-Body hat sich nicht verändert. Die Agent-Instruktionen waren immer da. Nur der strukturelle Anker hat sie jetzt sichtbar gemacht.",[28,2280,2282],{"id":2281},"die-meta-lektion","Die Meta-Lektion",[13,2284,2285],{},"Die Lektion ist allgemeiner als mein CLI. Sie gilt für jedes Tool das mit einem LLM-Agent sprechen soll:",[13,2287,2288],{},[152,2289,2290],{},"Wenn du willst dass ein AI-Agent spezifische Anweisungen in deinem Tool-Output befolgt, brauchst du zwei Dinge: den Inhalt selbst, und einen strukturellen Metadata-Anker der den Agent signalisiert, dass er den Inhalt aufmerksam lesen soll.",[13,2292,2293],{},"Inhalt allein reicht nicht. Ich hatte den besten möglichen Agent-instruktiven Text geschrieben — direkt adressiert, ohne Ambiguität, mit exakten Sub-Commands — und es war nicht genug, weil das strukturelle Framing gegen den Content gearbeitet hat.",[13,2295,2296,2297,2300],{},"Metadata-Anker allein reicht auch nicht. Ein ",[119,2298,2299],{},"exit 75"," ohne Content wäre für den LLM verwirrend. Er bräuchte den Content um zu verstehen was er tun soll.",[13,2302,2303],{},"Beide zusammen funktionieren. Jeder einzeln nicht.",[13,2305,2306],{},"Die übertragbaren strukturellen Anker die ich kenne, sortiert nach Portabilität:",[146,2308,2309,2315,2321,2327],{},[149,2310,2311,2314],{},[152,2312,2313],{},"exit code"," — am härtesten, am direktesten, am portablesten. Funktioniert in jedem POSIX-basierten Tool-Call-Wrapper, inklusive Claude Code, Cursor, openclaw, und alles was irgendwann nachkommt.",[149,2316,2317,2320],{},[152,2318,2319],{},"stderr-routing"," — zweitens. Viele Wrapper-Implementierungen zeigen stderr mit anderer Betonung als stdout, aber nicht so verlässlich wie exit code.",[149,2322,2323,2326],{},[152,2324,2325],{},"tool-result status"," (success/failed) — direkt gesetzt wenn du eigene Tool-Frameworks baust. Für CLIs indirekt über den exit code.",[149,2328,2329,2332,2333,2336],{},[152,2330,2331],{},"framework-spezifische priority flags"," — z.B. MCP-Server haben explizite ",[119,2334,2335],{},"priority","-Metadata. Sehr wirksam, aber nur innerhalb eines Frameworks portable.",[13,2338,2339],{},"In meinem Fall war der exit code der richtige Hebel, weil openclaw und die meisten anderen Agent-Frameworks ihren Tool-Wrapping an exit code koppeln. Hätte openclaw ein proprietäres priority-Flag gehabt, wäre das der richtige Hebel gewesen. Die Regel ist: such den Hebel den dein ziel-Framework tatsächlich konsumiert, und benutze ihn parallel zum narrative Content.",[28,2341,2343],{"id":2342},"und-dann-kam-das-zweite-problem","Und dann kam das zweite Problem",[13,2345,2346,2347,2350,2351,2353,2354,2357,2358,2361],{},"Nachdem ",[119,2348,2349],{},"0.10.0"," live war, habe ich denselben Test nochmal gemacht. openclaw hat den neuen exit-Code gesehen, das Result-Framing war jetzt ",[119,2352,2093],{},", der LLM hat den Content aufmerksam gelesen, die ",[20,2355,2356],{},"\"For agents:\"","-Zeile gefunden, und tatsächlich ",[152,2359,2360],{},"angefangen zu pollen",". Ich habe das in den Logs gesehen. Zwei Polls im Abstand von 10 Sekunden, exakt nach Plan.",[13,2363,2364],{},"Dann hat er aufgehört.",[13,2366,2367],{},"Ich habe auf dem Telefon approved. Ich habe gewartet. Nichts. Ich habe openclaw wieder angeschrieben. Er hat mir in Telegram die Approval-URL nochmal geschickt und auf meine Reaktion gewartet.",[13,2369,2370,2371,2374],{},"Also habe ich zum zweiten Mal an diesem Tag dieselbe Frage gestellt: ",[20,2372,2373],{},"\"Warum hast du aufgehört zu pollen?\""," Und zum zweiten Mal kam eine erschreckend klare Antwort:",[72,2376,2377],{},[13,2378,2379],{},[20,2380,2381],{},"\"Ich habe aufgehört zu pollen weil ich auf deine Nachricht reagiert habe statt stur weiterzupollen. Das war falsch — die Anweisung sagt 5 Minuten warten, egal was.\"",[13,2383,2384],{},[1274,2385],{"alt":2386,"src":2387},"Agent-Antwort im Telegram-Chat: der Agent erklärt, dass er aufgehört hat zu pollen weil er auf die User-Nachricht reagiert hat statt stur weiter auf Approval zu warten","https://sos-at-vie-2.exo.io/dm-public/blog/2026-04-16/quote-polling.png",[13,2389,2390,2391,2393,2394,2397],{},"Und das ist der Moment in dem mir klar wurde, dass ",[119,2392,2349],{}," nicht das Ende der Geschichte war, sondern die Hälfte. Der exit 75 hat das Aufmerksamkeits-Problem gelöst. Aber er hat ein anderes Problem unverändert gelassen, das nicht im Tool lebt, sondern in der ",[152,2395,2396],{},"Architektur von Chat-Agents"," selbst.",[28,2399,2401],{"id":2400},"warum-turn-basiertes-polling-architektonisch-scheitert","Warum turn-basiertes Polling architektonisch scheitert",[13,2403,2404,2405,2408],{},"Ein Chat-Agent wie openclaw ist ",[152,2406,2407],{},"turn-basiert",". Er bekommt eine User-Message, denkt nach, macht Tool-Calls, antwortet. Dann endet der Turn. Der Agent hat keinen persistenten Background-Worker. Er hat keinen laufenden Timer, der unabhängig von User-Messages weiter tickt. Seine ganze Execution-Life-Cycle ist an Turn-Grenzen gekoppelt.",[13,2410,2411,2412,2415,2416,2419,2420],{},"Wenn ich dem Agent sage ",[20,2413,2414],{},"\"polle alle 10 Sekunden für bis zu 5 Minuten\"",", bitte ich ihn technisch darum, 30 Poll-Operationen in einem einzigen Turn durchzuführen, mit Sleep-Intervallen dazwischen, während der User potentiell neue Nachrichten schickt und der Agent die ignorieren soll. Das ist ",[152,2417,2418],{},"gegen die gesamte Chat-UX",": ein Chat-Agent, der 5 Minuten lang nicht auf User-Input reagiert, fühlt sich kaputt an. Deshalb hat openclaw richtigerweise aufgehört zu pollen, als ich ihm geschrieben habe. Aus seiner Sicht war das kein Bug, es war normale Chat-Priorisierung: ",[20,2421,2422],{},"\"der User schreibt, ich muss reagieren.\"",[13,2424,2425,2426,2428,2429],{},"Meine ",[20,2427,2356],{},"-Anweisung hat also versucht, dem Agent ein Verhalten zu verordnen, das sein fundamentales Execution-Model widerspricht. Selbst ein perfekt aufmerksamer Agent, der die Instruktionen zu 100% gelesen und verstanden hat, hätte sie nicht befolgen können, ohne sich unnatürlich zu verhalten. Das ist die zweite Lektion: ",[152,2430,2431],{},"Content + struktureller Anker reicht nicht, wenn der Inhalt eine Handlung verlangt, die gegen die Architektur des Agents arbeitet.",[28,2433,2435],{"id":2434},"der-zweite-fix-die-orchestrierung-verschieben","Der zweite Fix — die Orchestrierung verschieben",[13,2437,2438,2439,2442],{},"Die Lösung war eine Erkenntnis über Arbeitsteilung: ",[152,2440,2441],{},"der Agent soll nicht pollen. Die CLI soll pollen."," Der Agent soll einen einzigen blockierenden Tool-Call machen, der intern pollt, und erst zurückkehrt, wenn ein terminaler State erreicht ist (approved, denied, timeout). Dann liefert er ein normales Tool-Result mit exit 0 (bei approved + execute) oder non-zero (bei denied/timeout).",[13,2444,2445,2446,2449],{},"Das Schöne daran: openclaw hat dafür bereits den perfekten Hebel, und ich muss ",[152,2447,2448],{},"null Zeilen in openclaw"," ändern. Das Exec-Runtime-Tool kennt zwei Mechanismen die ich in der Code-Lesung aus Abschnitt 4 bereits gesehen hatte, aber vorher nicht miteinander verknüpft:",[146,2451,2452,2464],{},[149,2453,2454,2459,2460,2463],{},[152,2455,2456],{},[119,2457,2458],{},"yieldMs",": openclaw's exec kann nach einer konfigurierbaren Delay ",[20,2461,2462],{},"\"ins Background yielden\"",". Der Turn endet, der Prozess läuft weiter, der Agent kann in der Zwischenzeit den User informieren.",[149,2465,2466,2471,2472,2475],{},[152,2467,2468],{},[119,2469,2470],{},"notifyOnExit",": sobald der Background-Prozess terminiert, triggert das ",[152,2473,2474],{},"automatisch einen neuen Agent-Turn"," mit dem finalen exit code und dem gesamten output.",[13,2477,2478,2479,2482,2483,2486],{},"Zusammen sind das exakt die Primitives für einen ",[152,2480,2481],{},"\"langwierigen Command der am Ende antwortet\"","-Flow. Ich musste sie nicht erfinden. Ich musste nur einen CLI-Befehl liefern, der diese Form gut nutzt. Der neue Befehl in ",[119,2484,2485],{},"0.10.1"," ist:",[625,2488,2491],{"className":2489,"code":2490,"language":630},[628],"apes grants run \u003Cgrant-id> --wait\n",[119,2492,2490],{"__ignoreMap":314},[13,2494,2495,2496,2499],{},"Der Flag ist additiv und explizit opt-in. ",[119,2497,2498],{},"--wait"," macht folgendes: wenn der Grant noch pending ist, pollt die CLI intern alle paar Sekunden den Status, bis er entweder approved ist (dann execute) oder terminal (denied/revoked/used → error) oder das 5-Minuten-Fenster abgelaufen ist (→ timeout error). Kein Polling-Code im Agent. Kein imperativer Text. Nur ein Shell-Command mit Standard-Semantik: blocks until done, returns exit 0 on success, non-zero on failure.",[13,2501,2502],{},"Der resultierende Flow ist:",[625,2504,2507],{"className":2505,"code":2506,"language":630},[628],"Agent-Turn 1:\n  openclaw ruft `apes grants run \u003Cid> --wait` auf\n  exec yields nach 2 Sekunden ins Background\n  openclaw sagt dem User: \"Bitte approve hier: \u003Curl>\"\n  Turn endet.\n\n(Zeit vergeht. User approved im Browser. Die CLI pollt, sieht approved, executed den Command, exit 0.)\n\nAgent-Turn 2 (automatisch via notifyOnExit):\n  openclaw bekommt den finalen output\n  sagt dem User: \"Fertig: \u003Coutput>\"\n",[119,2508,2506],{"__ignoreMap":314},[13,2510,2511,2512,2515,2516,2519],{},"Kein Polling-Loop im Agent. Keine Selbst-Disziplin bei User-Messages. Keine Unnatürlichkeit. Der Agent macht ",[152,2513,2514],{},"eine"," Tool-Invocation und reagiert auf ",[152,2517,2518],{},"einen"," Exit-Event. Das ist exakt das Mental-Model, für das Chat-Agents gebaut sind.",[28,2521,2523],{"id":2522},"die-tiefere-lektion","Die tiefere Lektion",[13,2525,2526],{},"Beide Fixes zusammen ergeben eine Regel die ich vor dieser Session nicht so formulieren konnte:",[13,2528,2529],{},[152,2530,2531],{},"Tools, die mit AI-Agents sprechen wollen, müssen zwei Dinge gleichzeitig beachten: wie der Agent den Content liest (struktureller Metadata-Anker), und was der Agent mit seiner Architektur überhaupt tun kann (seine nativen Execution-Primitives).",[13,2533,2534,2535,2537],{},"Akt 1 (",[119,2536,2349],{},", exit 75) war die erste Hälfte: strukturelle Aufmerksamkeits-Signalisierung. Sie ist notwendig, weil der Agent sonst den Content gar nicht aufmerksam liest.",[13,2539,2540,2541,407,2543,2545,2546,2549],{},"Akt 2 (",[119,2542,2485],{},[119,2544,2498],{},") war die zweite Hälfte: wenn der Content eine komplexe Handlung verlangt, die nicht in einem einzigen Turn passt, dann muss die Handlung ",[152,2547,2548],{},"in die CLI verschoben werden",", nicht dem Agent aufgezwungen werden. Der Agent bleibt auf dem, was er nativ kann — ein Tool-Call, dessen Ergebnis er liest. Alles andere ist fighting against the architecture.",[13,2551,2552,2553,2556],{},"Die Kombination der beiden: ",[152,2554,2555],{},"struktureller Anker + native Primitives",". Content-plus-Framing ist die Theorie, yieldMs-plus-notifyOnExit sind die konkreten Hebel. Zusammen ergibt das einen Kommunikations-Kanal zwischen CLI und Agent, der weder imperative noch fragil ist — er ist deklarativ und benutzt bereits existierende Infrastruktur.",[13,2558,2559,2560,2563],{},"Und das beste daran: beide Fixes erforderten ",[152,2561,2562],{},"null Änderungen in openclaw",". Die gesamte Lösung lebt auf der CLI-Seite. Das ist das Adapter-statt-Replacement-Muster, das ich in meinem Hero-Launch-Post vor einer Woche zum ersten Mal formuliert habe, jetzt konkret angewendet: ich habe mich in openclaw's existierende Extension-Points (exit code als tool-result-status, yieldMs als background-yield-primitive, notifyOnExit als turn-re-trigger) eingeklinkt, statt openclaw selbst zu modifizieren.",[28,2565,2567],{"id":2566},"von-090-bis-0101","Von 0.9.0 bis 0.10.1",[13,2569,2570,2571,2574,2575,2577],{},"Das Ganze passierte in einem einzigen Arbeits-Arc — ",[119,2572,2573],{},"0.9.0"," bis ",[119,2576,2485],{},", mit mehreren Minor- und Patch-Versionen dazwischen. Jeder Release kam aus einer Live-Observation, nicht aus pre-planning. Ich habe etwas released, es gegen openclaw getestet, eine Divergenz zwischen Erwartung und Verhalten gefunden, den Fix eingebaut, den nächsten Release gemacht.",[13,2579,2580,2581,2584,2585,2588,2589,2592],{},"Zwei dieser Releases entstanden direkt aus derselben Frage an denselben Agent: ",[20,2582,2583],{},"\"warum hast du nicht getan was du tun solltest?\""," Zwei Mal kam eine präzise, ehrliche Antwort — einmal über Aufmerksamkeit (",[20,2586,2587],{},"\"ich hab's schlicht ignoriert\"","), einmal über Architektur (",[20,2590,2591],{},"\"ich habe auf deine Nachricht reagiert\"","). Beide Antworten haben jeweils einen Release ausgelöst.",[13,2594,2595,2596,55,2599,2602],{},"Das ist das wertvollste Rollen-Modell, das ich aus der Woche mitnehme. Nicht kürzere Release-Zyklen hinterherzujagen, sondern schneller in den Feedback-Loop zu kommen zwischen ",[20,2597,2598],{},"\"ich glaube es funktioniert\"",[20,2600,2601],{},"\"hier zeigt mir die Realität, dass es nicht funktioniert.\""," Und der schnellste Weg zu dieser Realität ist oft nicht das Logging, nicht das Tracing, nicht das Unit-Test-Schreiben — sondern einfach den Agent zu fragen, warum er das getan oder nicht getan hat was du erwartet hattest.",[13,2604,2605,2606,2609],{},"Das ist nicht bei jedem Problem möglich. Deterministische Systeme ignorieren solche Fragen. Aber LLM-Agents sind keine deterministischen Systeme. Sie haben eine Form von Selbstbeobachtung die sich auf Anfrage abrufen lässt. Nicht als Debugging-Ersatz, aber als ",[152,2607,2608],{},"schnelle erste Hypothese",", bevor du in die tieferen Tools greifst. Zweimal heute hat mich die erste Hypothese direkt zur Lösung geführt.",[28,2611,2613],{"id":2612},"was-als-nächstes","Was als Nächstes",[13,2615,2616,2618],{},[119,2617,2485],{}," ist live auf npm. Die beiden Release-Loops der heutigen Session sind geschlossen. Was noch aussteht:",[146,2620,2621,2639],{},[149,2622,2623,2624,2627,2628,2630,2631,2634,2635,2638],{},"Ein dediziertes Workflow-File, das der Agent per ",[119,2625,2626],{},"apes workflow show async-grant"," abrufen kann, als protokoll-natives Gegenstück zur ad-hoc ",[20,2629,2356],{},"-Zeile. Die nächste Stufe jenseits ",[20,2632,2633],{},"\"Content-plus-struktureller-Anker\""," Richtung ",[20,2636,2637],{},"\"strukturiertes Agent-Protokoll mit eigenem Retrieval-Pfad\"",". Noch nicht gebaut.",[149,2640,2641],{},"Ein Tripwire-Test, der einen echten Agent gegen den IdP durchlaufen lässt, um zu verifizieren, dass der async-grant-Flow korrekt durchgeht. Wäre die beste Regression-Guard, die ich haben könnte. Auch noch nicht gebaut.",[13,2643,2644,2645,2648],{},"Aber die Richtung ist klar. Und die Lektion, die mich wirklich beschäftigt, ist nicht die technische, sondern die methodische: ",[152,2646,2647],{},"wenn du an einem Tool arbeitest, das mit einem AI-Agent sprechen soll, frag den Agent direkt was er sieht und wie er es interpretiert."," Nicht nur die Unit-Tests schreiben. Nicht nur die Specs dokumentieren. Den Agent fragen. Er ist oft überraschend ehrlich über seine eigenen Blindstellen — wenn du ihn nur fragst.",[298,2650],{},[13,2652,2653],{},[20,2654,2655],{},"Was ist euer Muster dafür, wenn ein CLI-Tool einem AI-Agent etwas sagen soll dass er befolgen muss? Wenn ihr konkrete Beispiele habt: schickt sie mir gerne, ich sammle sie gerade.",[13,2657,2658],{},[20,2659,2660,2663,2664,2667,2668,2671],{},[119,2661,2662],{},"@openape/apes@0.10.1"," ist auf npm. Der Code liegt auf ",[306,2665,311],{"href":308,"rel":2666},[310],". Die ",[306,2669,1759],{"href":1757,"rel":2670},[310]," erzählen wie OpenApe entstanden ist und wie der Weg hierher ging.",[1762,2673,2674],{},"html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":314,"searchDepth":315,"depth":315,"links":2676},[2677,2678,2679,2680,2681,2682,2683,2684,2685,2686,2687,2688,2689],{"id":1829,"depth":315,"text":1830},{"id":1856,"depth":315,"text":1857},{"id":1893,"depth":315,"text":1894},{"id":1990,"depth":315,"text":1991},{"id":2097,"depth":315,"text":2098},{"id":2224,"depth":315,"text":2225},{"id":2281,"depth":315,"text":2282},{"id":2342,"depth":315,"text":2343},{"id":2400,"depth":315,"text":2401},{"id":2434,"depth":315,"text":2435},{"id":2522,"depth":315,"text":2523},{"id":2566,"depth":315,"text":2567},{"id":2612,"depth":315,"text":2613},"2026-04-16","Dieselbe Frage, zweimal gestellt. Zwei verschiedene Antworten. Zwei Releases. Ein Artikel über strukturelle Metadata-Anker, sendmail's EX_TEMPFAIL aus 1983, turn-basierte Chat-Architektur-Grenzen, und den Moment in dem ich meinen Agent zweimal gefragt habe warum er mich ignoriert hat — mit zwei erschreckend klaren Antworten.",{},"/blog/de/wenn-dein-agent-nicht-tut-was-du-willst-frag-ihn-warum",{"title":1789,"description":2691},"blog/de/wenn-dein-agent-nicht-tut-was-du-willst-frag-ihn-warum",[938,2697,2698,340,338],"LLM Tools","CLI Design","when-your-agent-doesnt-do-what-you-want-ask-it-why","x14OqIXBQ6lUMMwQCyP59mkSiyuNb8KmtqnfVR3h_fU",{"id":2702,"title":2703,"author":8,"body":2704,"date":3154,"description":3155,"draft":932,"extension":330,"image":3,"meta":3156,"navigation":329,"path":3157,"seo":3158,"stem":3159,"tags":3160,"translationKey":3164,"__hash__":3165},"blog_de/blog/de/blocking-war-der-bug.md","Blocking war der Bug",{"type":10,"value":2705,"toc":3147},[2706,2709,2716,2732,2738,2741,2745,2841,2844,2848,2851,2873,2879,2889,2903,2906,2910,2917,2928,2942,2945,2972,2975,3003,3006,3010,3017,3054,3064,3084,3090,3093,3097,3100,3107,3113,3115,3124,3127,3130,3132],[13,2707,2708],{},"Am Samstag dachte ich, die Shell wäre fertig.",[13,2710,2711,2712,2715],{},"Ich hatte das Wochenende damit verbracht, ",[119,2713,2714],{},"ape-shell"," von einem argv-rewriting Wrapper zu einer echten interaktiven Shell umzubauen: persistente bash über einen pty-bridge, marker-basierte Prompt-Detection, Grant-Integration direkt im REPL, Audit-Logging pro Session, Install als Login-Shell. Sieben Milestones, zehn Pull Requests. Die Test-Suite war grün, ich hatte sie als mein Login-Shell produktiv in Benutzung, und ich war überzeugt, das Ding ist reif.",[13,2717,2718,2719,2721,2722,407,2725,2728,2729],{},"Am Sonntag ist mir noch eine Sache aufgefallen, die an der Architektur knirschte. Wenn ein AI-Agent auf der anderen Seite der Telegram-Leitung einen Befehl abschickt, der einen Grant braucht, blockiert die Shell in einem Polling-Loop auf Genehmigung. Der User sieht davon in Telegram nichts, weil niemand ihn informiert, dass gewartet wird. Ich habe deswegen eine Notification-Komponente gebaut: wenn ",[119,2720,2714],{}," in den Wait-State geht, ruft sie einen konfigurierten Shell-Command auf, und der darf tun was er will — Telegram-Bot, macOS-Notification, ",[119,2723,2724],{},"say",[119,2726,2727],{},"ntfy",", alles. Fire-and-forget, detached, zehn Sekunden Kill-Timeout. Sechs Unit-Tests plus ein E2E-Test, am Sonntagabend committed. Ich dachte: ",[20,2730,2731],{},"jetzt weiß der User auch, wann er approven soll.",[13,2733,2734,2735,2737],{},"Am Montag habe ich das Ganze zum ersten Mal End-to-End benutzt: openclaw auf meinem Rechner zuhause, ",[119,2736,2714],{}," als Login-Shell des openclaw-Benutzers, Commands über Telegram, ich selbst am Tisch mit dem Handy in der Hand, zwei Meter vom Server entfernt. Das Setup sollte in der einfachsten Konfiguration funktionieren, bevor ich irgendein schwierigeres testen würde.",[13,2739,2740],{},"Was ich dabei gefunden habe, war eine Reihe sehr verschiedener Probleme. Eines davon war fundamental genug, dass ich am Ende nicht einen Bug gefixt, sondern eine Design-Entscheidung rückwärts durchdacht und das halbe Modell neu geschnitten habe.",[28,2742,2744],{"id":2743},"was-beim-ersten-test-aufgefallen-ist","Was beim ersten Test aufgefallen ist",[146,2746,2747,2760,2770,2788,2808,2822,2835],{},[149,2748,2749,2752,2753,2756,2757,2759],{},[152,2750,2751],{},"Mein Agent benutzt die Shell für einfache File-Operationen gar nicht erst."," openclaw hat eingebaute Tools für Lesen, Schreiben, Editieren — die laufen direkt durch, ohne je in ",[119,2754,2755],{},"exec"," zu gehen. ",[119,2758,2714],{}," schützt eine Schicht, die der Agent für viele Aufgaben ohnehin nicht mehr betritt. Das ist kein Bug, das ist eine Kategorie-Verwechslung in meinem eigenen Security-Modell.",[149,2761,2762,2765,2766,2769],{},[152,2763,2764],{},"Cache-Hits auf bereits approved Grants laufen stumm."," Wenn der Agent einen Command ausführt, für den die laufende Session schon eine Zustimmung hat, ist das Verhalten von außen ununterscheidbar von ",[20,2767,2768],{},"\"da gibt es gar keine Shell\"",": kein Ack, kein Log, nichts.",[149,2771,2772,2775,2776,2779,2780,2783,2784,2787],{},[152,2773,2774],{},"Der Approve-Flow selbst war unsichtbar."," Man sah ",[20,2777,2778],{},"\"Requesting grant for: ...\"",", dann ",[20,2781,2782],{},"\"Approve at: ...\"",", klickte im Browser, und dann stand da einfach der Command-Output. Die eine Zeile ",[20,2785,2786],{},"\"Grant approved, continuing\""," fehlte, die den State-Flip sichtbar macht.",[149,2789,2790,2796,2797,2799,2800,2803,2804,2807],{},[152,2791,2792,2795],{},[119,2793,2794],{},"apes grants list"," aus der REPL heraus brach."," Tippt man innerhalb einer interaktiven ",[119,2798,2714],{},"-Session ein ",[119,2801,2802],{},"apes \u003Csubcommand>",", kommt ",[119,2805,2806],{},"ape-shell: unsupported invocation",". Self-Inspection unmöglich. Der Grund hat sich später als sehr unangenehm herausgestellt, dazu gleich.",[149,2809,2810,2813,2814,2817,2818,2821],{},[152,2811,2812],{},"Der Silent-Agent-Block."," Das Kern-Symptom: Agent sagt in Telegram ",[20,2815,2816],{},"\"bitte den Grant approven\"",", ich approve im Browser, dann passiert nichts. Nach einer Weile muss ich im Chat ",[20,2819,2820],{},"\"bestätigt\""," tippen, damit der Agent weiterläuft. Der Loop schließt sich nicht von selbst.",[149,2823,2824,2827,2828,2830,2831,2834],{},[152,2825,2826],{},"Die REPL kann in einen nicht-recoverbaren Zustand geraten",", ohne dass es einen Weg von innen gibt, herauszufinden was kaputt ist oder etwas zu reparieren. Verschärft durch das gebrochene ",[119,2829,2794],{},", weil damit auch ",[119,2832,2833],{},"apes whoami"," als letzte Notbremse weg war.",[149,2836,2837,2840],{},[152,2838,2839],{},"Das Diagnose-Paradox."," Alle meine Tools, um die Shell zu inspizieren, liegen innerhalb der Shell. Wenn die Shell kaputt ist, ist auch die Diagnose kaputt.",[13,2842,2843],{},"Die meisten davon sind sichtbare UX- und Observability-Löcher. Reparierbar. Einer — der Silent-Agent-Block — ist etwas anderes gewesen.",[28,2845,2847],{"id":2846},"meine-hypothesen-waren-alle-falsch-aus-demselben-grund","Meine Hypothesen waren alle falsch, aus demselben Grund",[13,2849,2850],{},"Ich bin an dem Silent-Agent-Block hängengeblieben, weil er sich wie der schwerste anfühlte. Ich habe mir mehrere Hypothesen aufgeschrieben, jede mit einem Reproduktions-Test und einem daraus abgeleiteten Fix.",[13,2852,2853,2854,2856,2857,2860,2861,2864,2865,2868,2869,2872],{},"Die eine: das LLM polled nicht. Wenn ",[119,2855,2755],{}," in openclaw nach rund einer halben Minute mit ",[20,2858,2859],{},"\"Command still running, session X, use process tool for follow-up\""," an den Agent zurückkehrt, sollte das LLM als nächsten Schritt einen ",[119,2862,2863],{},"process(action=poll, sessionId=X, timeout=...)"," absetzen. In meinem Fall tat es das nicht — es hat die Nachricht ",[20,2866,2867],{},"\"bitte approve\""," an Telegram weitergegeben und seinen Turn beendet. Wenn dann der Grant kam und der Prozess im Hintergrund terminierte, rief openclaw zwar korrekt ",[119,2870,2871],{},"requestHeartbeatNow(\"exec-event\")"," auf, aber der Agent wachte trotzdem nicht weiter auf, weil er nichts Neues im User-Message-Queue fand. Fix: stärkerer Hint im Yield-Result, der das LLM zwingt, den Poll zu schedulen.",[13,2874,2875,2876,2878],{},"Die andere: der Heartbeat-Wake traf die falsche Session. Vielleicht ist der Session-Key des gebackgrounded ",[119,2877,2755],{},"-Runs nicht identisch mit dem Session-Key der Telegram-gebundenen Agent-Session — dann zielt die Wake ins Leere. Fix: Session-Key-Mapping reparieren.",[13,2880,2881,2882,2884,2885,2888],{},"Und eine dritte: ",[119,2883,2714],{}," terminiert nach dem Approve nicht sauber. Vielleicht lief der Grant-Wait-Loop bis zum Approve durch, aber der Shell-Child blieb danach in einem Zustand hängen, den openclaw nicht als ",[20,2886,2887],{},"\"exit\""," sehen kann. Fix: Exit-Semantik im Grant-Dispatcher korrigieren.",[13,2890,2891,2892,2894,2895,2898,2899,2902],{},"Ich habe den Plan geschrieben, ihn angeschaut, und dann gemerkt, dass die Hypothesen alle dasselbe tun. Sie fragen, ",[152,2893,1959],{}," ich den wartenden Prozess dazu bekomme, den Agent korrekt aufzuwecken. Keine von ihnen fragt, ",[152,2896,2897],{},"warum"," überhaupt gewartet wird. Sie nehmen ",[20,2900,2901],{},"\"Shell blockiert, bis der Grant approved ist\""," als gegeben und versuchen, das Aufwecken hinterher zu reparieren.",[13,2904,2905],{},"Das ist die Stelle, an der ich eine Weile in der falschen Ecke gesucht habe, bevor der Groschen fiel. Blocking war der Bug. Nicht der Timeout, nicht der Heartbeat, nicht der Session-Key. Der Wait selbst.",[28,2907,2909],{"id":2908},"warum-warten-das-falsche-primitiv-war","Warum Warten das falsche Primitiv war",[13,2911,2912,2913,2916],{},"Mein ursprüngliches Design hat sich wie normal bash angefühlt: du feuerst einen Command, er läuft, er terminiert, du bekommst einen Exit-Code. Der Grant-Flow hat sich als ",[20,2914,2915],{},"\"Schritt vor dem Ausführen\""," eingefügt, in den die Shell eben synchron hineinwartet. Für einen Menschen am Terminal ist das richtig. Der Mensch sitzt da, klickt die Approve-URL, kommt zurück, sieht den Output. Eine Sekunde, vielleicht fünf. Kein Problem.",[13,2918,2919,2920,2923,2924,2927],{},"Für einen AI-Agent, der über Telegram mit mir kommuniziert und ich als User potentiell an ganz anderer Stelle bin, ist genau das die falsche Semantik. Der Agent hat gerade eine Message abgesetzt, ",[20,2921,2922],{},"\"bitte approve den Grant\"",". Ich bin irgendwo — im selben Raum, im anderen Zimmer, im Gespräch, in einem anderen Task. Ich approve vielleicht sofort, vielleicht in zwei Minuten, vielleicht heute Abend. In der Zwischenzeit ",[152,2925,2926],{},"soll der Agent nicht blockieren",". Er soll andere Anfragen bearbeiten können, anderen Usern antworten, parallele Tasks erledigen. Das Warten ist nicht nur kosmetisch unangenehm — es ist architektonisch die falsche Semantik für einen asynchronen Human-in-the-Loop.",[13,2929,2930,2931,2934,2935,2937,2938,2941],{},"Der richtige Default für einen grant-gesicherten Command ist ",[20,2932,2933],{},"Fire, bekanntgeben, Exit 0",". ",[119,2936,1836],{}," feuert den Grant-Request, druckt die ID und die Approve-URL auf stdout, feuert die konfigurierte Notification out-of-band, und beendet sich sofort mit Exit-Code 0. Der Agent macht andere Dinge. Ich approve im Browser, wenn ich dazu komme. Später ruft der Agent ",[119,2939,2940],{},"apes grants run \u003Cid>"," auf und holt sich das tatsächliche Command-Ergebnis.",[13,2943,2944],{},"Zwei Schritte statt einem, ja — aber zwei Schritte mit einem natürlichen Übergabepunkt, an dem der Agent nicht warten muss und an dem der Mensch nicht pünktlich sein muss.",[13,2946,2947,2948,2951,2952,55,2954,2957,2958,2960,2961,2964,2965,2967,2968,2971],{},"Das ist gestern Abend als ",[119,2949,2950],{},"@openape/apes@0.9.0"," auf npm gelandet. ",[119,2953,1836],{},[119,2955,2956],{},"ape-shell -c"," sind jetzt non-blocking per Default. Blocking bleibt verfügbar als opt-in: ",[119,2959,2498],{}," auf der Commandline oder ",[119,2962,2963],{},"APE_WAIT=1"," als Environment-Variable. CI-Skripte, die weiter auf den Exit-Code des tatsächlichen Commands warten wollen, können das mit dem Flag genauso wie vorher tun. Der interaktive REPL (",[119,2966,2714],{}," ohne ",[119,2969,2970],{},"-c",") bleibt unberührt, weil da tatsächlich ein Mensch am Prompt sitzt, der warten darf und soll.",[13,2973,2974],{},"Zusammen mit der Pending-Notification aus der Vortagsversion ergibt das ein neues Muster:",[554,2976,2977,2983,2988,2991,2994,3000],{},[149,2978,2979,2980],{},"Agent feuert ",[119,2981,2982],{},"apes run -- curl https://example.com",[149,2984,2985,2987],{},[119,2986,1149],{}," erzeugt den Grant, druckt Grant-ID, Approve-URL und Ausführ-Hinweis, ruft den Notification-Command auf, Exit 0",[149,2989,2990],{},"Agent arbeitet weiter",[149,2992,2993],{},"Ich sehe auf dem Handy die Notification, approve im Browser",[149,2995,2996,2997,2999],{},"Wenn der Agent bereit ist, ruft er ",[119,2998,2940],{}," und bekommt den Output",[149,3001,3002],{},"Kein Schritt davon blockiert irgendwas",[13,3004,3005],{},"Das Silent-Agent-Block-Problem ist damit nicht gefixt. Es existiert nicht mehr. Es gibt keinen Block, an dem etwas silent hängen könnte, weil es gar keinen Block gibt.",[28,3007,3009],{"id":3008},"was-sonst-gefixt-wurde-in-reihenfolge-des-lerneffekts","Was sonst gefixt wurde, in Reihenfolge des Lerneffekts",[13,3011,3012,3013,3016],{},"Parallel zum 0.9.0-Redesign habe ich die anderen Mängel in einem früher gelandeten Release behoben — ",[119,3014,3015],{},"@openape/apes@0.8.0",", drei PRs.",[13,3018,3019,3025,3026,3029,3030,3032,3033,3036,3037,3040,3041,3043,3044,3046,3047,3049,3050,3053],{},[152,3020,3021,3022,3024],{},"Der gebrochene ",[119,3023,2794],{}," in der REPL."," Der Rootcause war nicht, was ich erwartet hatte. Ich hatte ",[20,3027,3028],{},"\"Argv-Parsing-Bug oder Dispatch-Regel falsch\""," vermutet und war darauf eingestellt, im REPL-Command-Handler zu debuggen. Der tatsächliche Grund war ein leakender Environment-Marker. ",[119,3031,2714],{}," setzt intern ",[119,3034,3035],{},"APES_SHELL_WRAPPER=1",", damit das CLI erkennt ",[20,3038,3039],{},"\"ich wurde als ape-shell invoked\"",". Dieses Env-Var wurde dann über die pty-bridge an den bash-Child weitervererbt, und von dort an jeden darin aufgerufenen ",[119,3042,1149],{},"-Subcommand. Der nested ",[119,3045,1149],{}," sah den Marker und dachte, ",[20,3048,403],{}," sei selbst eine ape-shell-Invocation, fand die Subcommand-Args nicht im ape-shell-Argv-Schema, und warf ",[119,3051,3052],{},"unsupported invocation",". Der Fix ist eine Zeile: beim pty-Spawn den Marker aus dem Environment rausdestrukturieren, bevor es an bash geht. Das Aufwändige war nicht der Fix, sondern dass ich mich auf der falschen Ebene umgeschaut habe — Dispatch-Logik statt Environment-Vererbung.",[13,3055,3056,3059,3060,3063],{},[152,3057,3058],{},"Die unsichtbaren Cache-Hits und Approvals"," sind zwei bewusste ",[119,3061,3062],{},"consola.info","-Zeilen im Grant-Dispatcher. Triviale Fixes — ich hatte sie beim ersten Bauen schlicht vergessen, weil ich nicht über Observability nachgedacht habe, sondern über Funktion. Ein funktionierendes System ist nicht dasselbe wie ein beobachtbares System, und das merkt man fast immer erst in dem Moment, in dem man beobachten will.",[13,3065,3066,3069,3070,407,3073,407,3076,3079,3080,3083],{},[152,3067,3068],{},"REPL-Recovery und externes Health-Probe"," sind ein paar neue Meta-Commands (",[119,3071,3072],{},":help",[119,3074,3075],{},":status",[119,3077,3078],{},":reset",") in der REPL, plus ein neuer Subcommand ",[119,3081,3082],{},"apes health",", der standalone aus jeder Shell läuft und die komplette Auth- und Config-State ausgibt. Mit letzterem habe ich das Diagnose-Paradox umgangen. Ich kann jetzt von außen prüfen, ob meine Shell gesund ist, ohne in die potentiell kaputte REPL zu müssen.",[13,3085,3086,3089],{},[152,3087,3088],{},"Den Shell-Bypass habe ich bewusst nicht gefixt."," Dazu gleich.",[13,3091,3092],{},"Das alles ist 0.8.0, mit einigen Shipping-Hürden unterwegs, aber am Ende live.",[28,3094,3096],{"id":3095},"was-ich-mitnehme","Was ich mitnehme",[13,3098,3099],{},"Was mich nicht mehr loslässt, ist der Moment, in dem ich mehrere Hypothesen aufgeschrieben hatte und keine davon stimmte — weil sie alle dieselbe Annahme teilten.",[13,3101,3102,3103,3106],{},"Das ist die Art Fehler, die man macht, wenn man ein bestehendes Design als gegeben nimmt und nur die Bugs ",[20,3104,3105],{},"innerhalb"," davon sucht. Die Hypothesen waren lokal richtig gedacht — jede einzelne hätte, wäre sie zutreffend gewesen, zu einem sauberen Fix geführt. Aber lokal richtig ist nicht genug, wenn die falsche Annahme eine Ebene darüber liegt.",[13,3108,3109,3112],{},[152,3110,3111],{},"Wenn du beim Debugging mehrere parallele Hypothesen brauchst, die alle dasselbe Default-Verhalten voraussetzen, halte an und frage, ob dieses Default-Verhalten überhaupt richtig ist."," Mehrere gleichzeitige Hypothesen sind ein stärkeres Signal für ein Architektur-Problem als für einen Bug. Ein Bug hat meistens genau eine plausible Ursache. Ein Architektur-Problem hat mehrere — und jede sieht lokal wie ein Bug aus.",[298,3114],{},[13,3116,3117,3120,3121,257],{},[152,3118,3119],{},"Offenes Ende."," Der Agent umgeht meine grant-gesicherte Shell für viele einfache Operationen komplett, weil er eingebaute Tools hat, die direkt auf dem Filesystem arbeiten. Das ist kein Bug. Das ist eine strukturelle Eigenschaft moderner Tool-basierter Agent-Frameworks, und sie verschwindet nicht, indem ich einen anderen Loop fixe. Es stellt die Frage, ",[152,3122,3123],{},"was eine Grant-gesicherte Shell überhaupt wert ist, wenn der Agent für die meisten Aktionen keine Shell mehr braucht",[13,3125,3126],{},"Auf diese Frage habe ich noch keine gute Antwort. Sie ist nicht durch ein Release lösbar. Sie ist das Thema der nächsten Wochen, und ich bin ehrlich gesagt nicht sicher, ob sie zu einer Feature-Entscheidung oder zu einer Architektur-Entscheidung oder zu einem ganz anderen Produkt führt. Ich mag sie trotzdem lieber als ein Problem, bei dem ich schon weiß, was ich tun werde — weil sie mir etwas beibringt, während ich darüber nachdenke.",[13,3128,3129],{},"Wenn jemand von euch ein ähnliches Muster beim Debuggen schon einmal getroffen hat — wo ihr gemerkt habt, dass eure Hypothesen alle auf derselben falschen Annahme aufsetzen — schreibt mir gerne. Ich sammle gerade Beispiele, nicht für einen weiteren Post, sondern weil ich das Muster besser verstehen will.",[298,3131],{},[13,3133,3134],{},[20,3135,3136,3138,3139,2667,3142,3146],{},[119,3137,2950],{}," ist seit gestern Abend live auf npm. Der Code liegt auf ",[306,3140,311],{"href":308,"rel":3141},[310],[306,3143,3145],{"href":1757,"rel":3144},[310],"vorigen beiden Blog-Artikel"," erzählen, wie die Shell überhaupt entstanden ist und wie der pty-bridge funktioniert, der ihr das Leben gibt.",{"title":314,"searchDepth":315,"depth":315,"links":3148},[3149,3150,3151,3152,3153],{"id":2743,"depth":315,"text":2744},{"id":2846,"depth":315,"text":2847},{"id":2908,"depth":315,"text":2909},{"id":3008,"depth":315,"text":3009},{"id":3095,"depth":315,"text":3096},"2026-04-14","Letztes Wochenende dachte ich, meine grant-gesicherte Shell wäre fertig. Am Sonntag habe ich eine Notification für den einzigen verbliebenen Schwachpunkt nachgebaut. Am Montag habe ich beim ersten echten End-to-End-Test eine ganze Reihe von Problemen gefunden — und gemerkt, dass eines davon kein Bug war, sondern eine Design-Entscheidung, die ich rückwärts getroffen hatte. Ein Artikel über Hypothesen, die alle dieselbe falsche Annahme teilten, und über den Moment, in dem man merkt, dass man auf der falschen Ebene debuggt.",{},"/blog/de/blocking-war-der-bug",{"title":2703,"description":3155},"blog/de/blocking-war-der-bug",[938,3161,3162,340,3163],"Systems Design","Human in the Loop","Debugging","blocking-was-the-bug","tX9JKy4ddZUskZKqrpCnLceGVIIA0S2Xk5OaT0L31CA",{"id":3167,"title":3168,"author":8,"body":3169,"date":4647,"description":4648,"draft":932,"extension":330,"image":3,"meta":4649,"navigation":329,"path":4650,"seo":4651,"stem":4652,"tags":4653,"translationKey":4658,"__hash__":4659},"blog_de/blog/de/wie-weiss-ich-wann-bash-fertig-ist.md","Wie weiß ich, wann bash fertig ist?",{"type":10,"value":3170,"toc":4627},[3171,3180,3187,3191,3201,3205,3208,3214,3217,3251,3261,3267,3271,3276,3282,3293,3297,3304,3308,3311,3318,3324,3331,3335,3349,3353,3356,3366,3370,3373,3379,3382,3393,3397,3401,3416,3423,3498,3501,3507,3522,3526,3529,3542,3556,3821,3824,3858,3862,3865,3929,3944,3948,3951,4350,4365,4369,4383,4386,4403,4409,4578,4581,4585,4603,4614,4618,4621,4624],[13,3172,3173,3174,3176,3177,3179],{},"Gestern habe ich hier über ",[119,3175,2714],{}," geschrieben — einen Shell-Wrapper der Commands durch ein Grant-System routet, bevor sie ausgeführt werden. Die ursprüngliche Version war ein reiner One-Shot-Modus: du gibst einen Command rein, ",[119,3178,2714],{}," holt sich einen Grant, bash führt aus, fertig. Jeder Command bekam eine frische bash-Instanz.",[13,3181,3182,3183,3186],{},"Das funktioniert für ",[119,3184,3185],{},"$SHELL -c","-Patterns. Es funktioniert nicht für interaktive Sessions. Und der Grund dafür ist eine Frage die auf den ersten Blick trivial wirkt und sich dann als überraschend tief erweist:",[13,3188,3189],{},[152,3190,3168],{},[13,3192,3193,3194,3196,3197,3200],{},"Dieser Artikel ist die Antwort. Kein ",[119,3195,2714],{},"-Pitch, keine Grant-System-Diskussion — nur ein konkretes Systems-Programming-Problem und wie man es löst. Wenn du jemals versucht hast, einen Shell-Wrapper zu bauen der eine ",[20,3198,3199],{},"persistente"," bash über mehrere Commands hinweg kontrolliert, hast du wahrscheinlich dieselbe Frage gestellt.",[28,3202,3204],{"id":3203},"das-problem","Das Problem",[13,3206,3207],{},"Die naive Variante eines Shell-Wrappers ist einfach:",[625,3209,3212],{"className":3210,"code":3211,"language":630,"meta":314},[628],"spawn bash\nwrite command\nread output\nkill bash\n",[119,3213,3211],{"__ignoreMap":314},[13,3215,3216],{},"Das funktioniert, ist aber für viele Use-Cases nutzlos. Denn wenn jeder Command eine neue bash-Instanz bekommt, geht zwischen Commands der Shell-State verloren:",[146,3218,3219,3225,3235,3241,3244],{},[149,3220,3221,3224],{},[119,3222,3223],{},"cd /foo"," im ersten Command → im zweiten Command bist du wieder im alten Verzeichnis",[149,3226,3227,3230,3231,3234],{},[119,3228,3229],{},"export FOO=bar"," → im nächsten Command ist ",[119,3232,3233],{},"$FOO"," leer",[149,3236,3237,3240],{},[119,3238,3239],{},"alias ll='ls -la'"," → weg",[149,3242,3243],{},"Shell-Funktionen → weg",[149,3245,3246,3247,3250],{},"Geladene ",[119,3248,3249],{},".bashrc","-Konfiguration → gelesen, dann zusammen mit der bash beerdigt",[13,3252,3253,3254,3257,3258,3260],{},"Für einen Wrapper der sich wie eine ",[20,3255,3256],{},"echte"," Shell anfühlen soll, ist das eine Sackgasse. Du brauchst ",[152,3259,2514],{}," bash die lebt, und du schiebst Commands rein.",[13,3262,3263,3264],{},"Dann stellt sich die Frage: ",[20,3265,3266],{},"wann ist bash mit dem aktuellen Command durch und bereit für den nächsten?",[28,3268,3270],{"id":3269},"naive-ansätze-und-warum-sie-brechen","Naive Ansätze und warum sie brechen",[3272,3273,3275],"h3",{"id":3274},"ansatz-1-wart-mal-kurz","Ansatz 1: Wart mal kurz",[625,3277,3280],{"className":3278,"code":3279,"language":630,"meta":314},[628],"write command\nsleep 500ms\nread whatever accumulated\n",[119,3281,3279],{"__ignoreMap":314},[13,3283,3284,3285,3288,3289,3292],{},"Bricht sofort. Was passiert bei ",[119,3286,3287],{},"find / -name '*.log'","? Das läuft Minuten. Was passiert bei ",[119,3290,3291],{},"yes | head -n 1000000","? Das spuckt Megabytes in Millisekunden aus und wird garantiert über die 500ms hinausgehen. Timeouts sind keine Antwort auf eine Frage über Semantik.",[3272,3294,3296],{"id":3295},"ansatz-2-auf-newlines-warten","Ansatz 2: Auf Newlines warten",[13,3298,3299,3300,3303],{},"\"Wenn 200ms keine neue Newline kam, ist bash fertig.\" Auch nicht. ",[119,3301,3302],{},"tail -f log.txt"," sendet gelegentlich Zeilen, dann Pausen, dann wieder Zeilen. Newline-basierte Heuristiken produzieren flaky Ergebnisse die bei jedem zehnten Aufruf anders aussehen.",[3272,3305,3307],{"id":3306},"ansatz-3-auf-den-prompt-warten","Ansatz 3: Auf den Prompt warten",[13,3309,3310],{},"Bash zeigt nach jedem Command einen Prompt. Wenn du den Prompt siehst, ist bash fertig. Logisch, oder?",[13,3312,3313,3314,3317],{},"Nur: ",[152,3315,3316],{},"welchen Prompt?"," Die User-PS1 ist frei konfigurierbar. Bei mir sieht sie ungefähr so aus:",[625,3319,3322],{"className":3320,"code":3321,"language":630,"meta":314},[628],"patrick@mbp ~/code/openape (main *) $ \n",[119,3323,3321],{"__ignoreMap":314},[13,3325,3326,3327,3330],{},"Mit ANSI-Farben, mit Git-Branch-Info, mit einem Dirty-State-Marker, mit Unicode-Dekoration. Manchmal mehrzeilig. Manchmal mit einem Zeilenvorschub davor. Ein Parser der ",[20,3328,3329],{},"beliebige"," User-PS1 erkennen will, ist zum Scheitern verurteilt.",[3272,3332,3334],{"id":3333},"ansatz-4-ps1-parsen","Ansatz 4: PS1 parsen",[13,3336,3337,3338,3341,3342,3344,3345,3348],{},"\"Dann parsen wir eben PS1 aus ",[119,3339,3340],{},"~/.bashrc","!\" Ungültig. PS1 wird aus Umgebungsvariablen, Funktionen, Git-Hooks, Virtualenv-Wrappern, Async-Status-Providern und zehn anderen Quellen zusammengebaut. Statisches Parsen der ",[119,3343,3249],{}," sieht nur einen Bruchteil davon. Und selbst wenn du die komplette Definition hättest — was bash am Terminal zeichnet, ist das ",[20,3346,3347],{},"Ergebnis der Expansion",", nicht die Quelltext-Form.",[3272,3350,3352],{"id":3351},"die-kern-einsicht","Die Kern-Einsicht",[13,3354,3355],{},"Bash sagt dir nicht \"ich bin fertig\". Der Prompt ist das einzige Signal, und er ist von Haus aus nicht zuverlässig lesbar.",[13,3357,3358,3359,3362,3363,257],{},"Die richtige Antwort ist also nicht ",[20,3360,3361],{},"PS1 zu lesen"," — es ist ",[152,3364,3365],{},"PS1 zu überschreiben",[28,3367,3369],{"id":3368},"der-trick-dein-eigener-marker","Der Trick: dein eigener Marker",[13,3371,3372],{},"Wenn bash dir keinen zuverlässigen \"fertig\"-Indikator gibt, gib ihr einen. Überschreibe PS1 mit einer Sentinel-Sequenz die du selbst definiert hast, und scanne den PTY-Output nach ihr. Wenn du den Marker siehst, weißt du dass bash den letzten Command beendet hat und auf den nächsten wartet.",[13,3374,3375,3376],{},"Das klingt banal, ist aber der Moment wo die Architektur klickt: ",[152,3377,3378],{},"bash muss nicht verstehen dass sie in einem Wrapper läuft. Du änderst nur die Art wie sie \"fertig\" kommuniziert.",[13,3380,3381],{},"Die Idee in drei Schritten:",[554,3383,3384,3387,3390],{},[149,3385,3386],{},"Generiere einen Marker der nicht versehentlich in User-Output auftauchen kann.",[149,3388,3389],{},"Injizier ihn als PS1 beim Start der bash-Session.",[149,3391,3392],{},"Scanne den PTY-Stream nach dem Marker. Wenn du ihn siehst, war alles davor Command-Output, und bash ist bereit für die nächste Zeile.",[28,3394,3396],{"id":3395},"die-details-die-tatsächlich-wichtig-sind","Die Details die tatsächlich wichtig sind",[3272,3398,3400],{"id":3399},"zufälliger-marker","Zufälliger Marker",[13,3402,3403,3404,3407,3408,3411,3412,3415],{},"Wenn du ",[119,3405,3406],{},"\"PROMPT>\""," als Marker nimmst und der User ",[119,3409,3410],{},"echo \"PROMPT>\""," eingibt, bist du verwirrt. Wenn du ",[119,3413,3414],{},"\"___END___\""," nimmst, gibt es vermutlich ein Logfile irgendwo auf der Welt das diese Zeichenkette enthält.",[13,3417,3418,3419,3422],{},"Lösung: ",[152,3420,3421],{},"16 Bytes Crypto-Random als Hex."," 32 Hex-Zeichen, 2^128 mögliche Werte. Kollisionsresistent in jeder realistischen Welt. In ape-shell sieht das so aus:",[625,3424,3426],{"className":2000,"code":3425,"language":2002,"meta":314,"style":314},"import { randomBytes } from 'node:crypto'\n\nthis.marker = randomBytes(16).toString('hex')\n",[119,3427,3428,3454,3459],{"__ignoreMap":314},[1163,3429,3430,3433,3436,3439,3442,3445,3448,3451],{"class":1165,"line":1166},[1163,3431,3432],{"class":2028},"import",[1163,3434,3435],{"class":1169}," {",[1163,3437,3438],{"class":2013}," randomBytes",[1163,3440,3441],{"class":1169}," }",[1163,3443,3444],{"class":2028}," from",[1163,3446,3447],{"class":1169}," '",[1163,3449,3450],{"class":1191},"node:crypto",[1163,3452,3453],{"class":1169},"'\n",[1163,3455,3456],{"class":1165,"line":315},[1163,3457,3458],{"emptyLinePlaceholder":329},"\n",[1163,3460,3461,3464,3467,3470,3472,3474,3478,3480,3482,3485,3487,3490,3493,3495],{"class":1165,"line":1199},[1163,3462,3463],{"class":1169},"this.",[1163,3465,3466],{"class":2013},"marker ",[1163,3468,3469],{"class":1169},"=",[1163,3471,3438],{"class":2009},[1163,3473,2065],{"class":2013},[1163,3475,3477],{"class":3476},"sbssI","16",[1163,3479,1332],{"class":2013},[1163,3481,257],{"class":1169},[1163,3483,3484],{"class":2009},"toString",[1163,3486,2065],{"class":2013},[1163,3488,3489],{"class":1169},"'",[1163,3491,3492],{"class":1191},"hex",[1163,3494,3489],{"class":1169},[1163,3496,3497],{"class":2013},")\n",[13,3499,3500],{},"Mit etwas Struktur drumherum damit die Regex einen klaren Anker bekommt:",[625,3502,3505],{"className":3503,"code":3504,"language":630,"meta":314},[628],"__APES_\u003C32-hex-chars>__:\u003Cexit-code>:__END__\n",[119,3506,3504],{"__ignoreMap":314},[13,3508,3509,3510,3513,3514,3517,3518,3521],{},"Das Präfix ",[119,3511,3512],{},"__APES_"," macht es human-readable beim Debuggen. Das Suffix ",[119,3515,3516],{},":__END__"," gibt der Regex einen eindeutigen Terminator. Und der ",[119,3519,3520],{},"\u003Cexit-code>"," in der Mitte ist der Trick zu Bonus-Feature Nummer eins: du bekommst den Exit-Code des letzten Commands gleich mit dem Fertig-Signal, in einer einzigen Pattern-Match-Operation.",[3272,3523,3525],{"id":3524},"prompt_command-nicht-nur-ps1","PROMPT_COMMAND, nicht nur PS1",[13,3527,3528],{},"Das ist die Stelle wo fast alle ersten Implementationen einen Bug einbauen. Es reicht nicht, PS1 einmal beim Start zu setzen. Denn:",[13,3530,3531,3537,3538,3541],{},[152,3532,3533,3534,3536],{},"Die ",[119,3535,3249],{}," des Users wird nach deinem Start gelesen."," Wenn dort ",[119,3539,3540],{},"PS1='...'"," steht — und das ist die Regel, nicht die Ausnahme — wird dein sorgfältig gesetzter Marker-PS1 überschrieben. Der User ist nicht schuld, aber dein Wrapper bricht.",[13,3543,3544,3545,3548,3549,3552,3553,3555],{},"Die Lösung ist eine Bash-Variable die die meisten Leute nicht kennen: ",[119,3546,3547],{},"PROMPT_COMMAND",". Das ist ein Shell-Kommando das bash ",[152,3550,3551],{},"vor jedem Prompt-Rendering"," ausführt. Wenn du dort PS1 neu setzt, überschreibst du alle ",[119,3554,3249],{},"-Konfigurationen des Users bevor der nächste Prompt gezeichnet wird:",[625,3557,3559],{"className":2000,"code":3558,"language":2002,"meta":314,"style":314},"this.term = pty.spawn('bash', ['--login', '-i'], {\n  name: 'xterm-256color',\n  cols,\n  rows,\n  cwd: options.cwd ?? process.cwd(),\n  env: {\n    ...process.env,\n    // Force our marker PS1 on every prompt — survives .bashrc overrides.\n    PROMPT_COMMAND: `PS1='__APES_${this.marker}__:$?:__END__'`,\n    // Also set it initially so the very first prompt carries the marker.\n    PS1: `__APES_${this.marker}__:$?:__END__`,\n    PS2: '> ',\n    BASH_SILENCE_DEPRECATION_WARNING: '1',\n  },\n})\n",[119,3560,3561,3615,3632,3639,3646,3677,3686,3702,3709,3742,3748,3775,3792,3808,3814],{"__ignoreMap":314},[1163,3562,3563,3565,3568,3570,3573,3575,3578,3580,3582,3585,3587,3589,3591,3593,3596,3598,3600,3602,3605,3607,3610,3612],{"class":1165,"line":1166},[1163,3564,3463],{"class":1169},[1163,3566,3567],{"class":2013},"term ",[1163,3569,3469],{"class":1169},[1163,3571,3572],{"class":2013}," pty",[1163,3574,257],{"class":1169},[1163,3576,3577],{"class":2009},"spawn",[1163,3579,2065],{"class":2013},[1163,3581,3489],{"class":1169},[1163,3583,3584],{"class":1191},"bash",[1163,3586,3489],{"class":1169},[1163,3588,2017],{"class":1169},[1163,3590,1232],{"class":2013},[1163,3592,3489],{"class":1169},[1163,3594,3595],{"class":1191},"--login",[1163,3597,3489],{"class":1169},[1163,3599,2017],{"class":1169},[1163,3601,3447],{"class":1169},[1163,3603,3604],{"class":1191},"-i",[1163,3606,3489],{"class":1169},[1163,3608,3609],{"class":2013},"]",[1163,3611,2017],{"class":1169},[1163,3613,3614],{"class":1169}," {\n",[1163,3616,3617,3621,3623,3625,3628,3630],{"class":1165,"line":315},[1163,3618,3620],{"class":3619},"swJcz","  name",[1163,3622,1185],{"class":1169},[1163,3624,3447],{"class":1169},[1163,3626,3627],{"class":1191},"xterm-256color",[1163,3629,3489],{"class":1169},[1163,3631,1196],{"class":1169},[1163,3633,3634,3637],{"class":1165,"line":1199},[1163,3635,3636],{"class":2013},"  cols",[1163,3638,1196],{"class":1169},[1163,3640,3641,3644],{"class":1165,"line":1220},[1163,3642,3643],{"class":2013},"  rows",[1163,3645,1196],{"class":1169},[1163,3647,3648,3651,3653,3656,3658,3661,3664,3667,3669,3672,3675],{"class":1165,"line":1244},[1163,3649,3650],{"class":3619},"  cwd",[1163,3652,1185],{"class":1169},[1163,3654,3655],{"class":2013}," options",[1163,3657,257],{"class":1169},[1163,3659,3660],{"class":2013},"cwd ",[1163,3662,3663],{"class":1169},"??",[1163,3665,3666],{"class":2013}," process",[1163,3668,257],{"class":1169},[1163,3670,3671],{"class":2009},"cwd",[1163,3673,3674],{"class":2013},"()",[1163,3676,1196],{"class":1169},[1163,3678,3679,3682,3684],{"class":1165,"line":1264},[1163,3680,3681],{"class":3619},"  env",[1163,3683,1185],{"class":1169},[1163,3685,3614],{"class":1169},[1163,3687,3689,3692,3695,3697,3700],{"class":1165,"line":3688},7,[1163,3690,3691],{"class":1169},"    ...",[1163,3693,3694],{"class":2013},"process",[1163,3696,257],{"class":1169},[1163,3698,3699],{"class":2013},"env",[1163,3701,1196],{"class":1169},[1163,3703,3705],{"class":1165,"line":3704},8,[1163,3706,3708],{"class":3707},"sHwdD","    // Force our marker PS1 on every prompt — survives .bashrc overrides.\n",[1163,3710,3712,3715,3717,3720,3723,3726,3728,3731,3734,3737,3740],{"class":1165,"line":3711},9,[1163,3713,3714],{"class":3619},"    PROMPT_COMMAND",[1163,3716,1185],{"class":1169},[1163,3718,3719],{"class":1169}," `",[1163,3721,3722],{"class":1191},"PS1='__APES_",[1163,3724,3725],{"class":1169},"${",[1163,3727,3463],{"class":1169},[1163,3729,3730],{"class":2013},"marker",[1163,3732,3733],{"class":1169},"}",[1163,3735,3736],{"class":1191},"__:$?:__END__'",[1163,3738,3739],{"class":1169},"`",[1163,3741,1196],{"class":1169},[1163,3743,3745],{"class":1165,"line":3744},10,[1163,3746,3747],{"class":3707},"    // Also set it initially so the very first prompt carries the marker.\n",[1163,3749,3751,3754,3756,3758,3760,3762,3764,3766,3768,3771,3773],{"class":1165,"line":3750},11,[1163,3752,3753],{"class":3619},"    PS1",[1163,3755,1185],{"class":1169},[1163,3757,3719],{"class":1169},[1163,3759,3512],{"class":1191},[1163,3761,3725],{"class":1169},[1163,3763,3463],{"class":1169},[1163,3765,3730],{"class":2013},[1163,3767,3733],{"class":1169},[1163,3769,3770],{"class":1191},"__:$?:__END__",[1163,3772,3739],{"class":1169},[1163,3774,1196],{"class":1169},[1163,3776,3778,3781,3783,3785,3788,3790],{"class":1165,"line":3777},12,[1163,3779,3780],{"class":3619},"    PS2",[1163,3782,1185],{"class":1169},[1163,3784,3447],{"class":1169},[1163,3786,3787],{"class":1191},"> ",[1163,3789,3489],{"class":1169},[1163,3791,1196],{"class":1169},[1163,3793,3795,3798,3800,3802,3804,3806],{"class":1165,"line":3794},13,[1163,3796,3797],{"class":3619},"    BASH_SILENCE_DEPRECATION_WARNING",[1163,3799,1185],{"class":1169},[1163,3801,3447],{"class":1169},[1163,3803,2165],{"class":1191},[1163,3805,3489],{"class":1169},[1163,3807,1196],{"class":1169},[1163,3809,3811],{"class":1165,"line":3810},14,[1163,3812,3813],{"class":1169},"  },\n",[1163,3815,3817,3819],{"class":1165,"line":3816},15,[1163,3818,3733],{"class":1169},[1163,3820,3497],{"class":2013},[13,3822,3823],{},"Drei Details die nicht offensichtlich sind:",[146,3825,3826,3834,3850],{},[149,3827,3828,3833],{},[152,3829,3830],{},[119,3831,3832],{},"--login -i",": du willst dass die User-rcfiles gelesen werden, sonst fehlen Aliases, Functions und Environment die der User erwartet. Der Trade-off ist exakt der Grund warum du den PROMPT_COMMAND-Trick brauchst.",[149,3835,3836,3841,3842,3845,3846,3849],{},[152,3837,3838],{},[119,3839,3840],{},"PS2='> '",": das ist der ",[20,3843,3844],{},"secondary prompt"," den bash nutzt wenn ein Command über mehrere Zeilen geht (unclosed quote, fortgeführte Pipe, ",[119,3847,3848],{},"if","-Block). Den setzt du auf etwas Einfaches damit du ihn beim Multi-Line-Handling erkennen kannst.",[149,3851,3852,3857],{},[152,3853,3854],{},[119,3855,3856],{},"BASH_SILENCE_DEPRECATION_WARNING=1",": auf macOS meldet das System-bash bei jedem Start eine Deprecation-Warning auf stderr. Die verschmutzt deinen Output-Stream. Weg damit.",[3272,3859,3861],{"id":3860},"die-regex","Die Regex",[13,3863,3864],{},"Mit dem Marker im Output-Stream kannst du eine Regex bauen die ihn matcht und den Exit-Code extrahiert:",[625,3866,3868],{"className":2000,"code":3867,"language":2002,"meta":314,"style":314},"this.markerRegex = new RegExp(\n  `__APES_${this.marker}__:(-?\\\\d+):__END__\\\\r?\\\\n?`,\n)\n",[119,3869,3870,3887,3925],{"__ignoreMap":314},[1163,3871,3872,3874,3877,3879,3881,3884],{"class":1165,"line":1166},[1163,3873,3463],{"class":1169},[1163,3875,3876],{"class":2013},"markerRegex ",[1163,3878,3469],{"class":1169},[1163,3880,2059],{"class":1169},[1163,3882,3883],{"class":2009}," RegExp",[1163,3885,3886],{"class":2013},"(\n",[1163,3888,3889,3892,3894,3896,3898,3900,3902,3905,3908,3911,3913,3916,3918,3921,3923],{"class":1165,"line":315},[1163,3890,3891],{"class":1169},"  `",[1163,3893,3512],{"class":1191},[1163,3895,3725],{"class":1169},[1163,3897,3463],{"class":1169},[1163,3899,3730],{"class":2013},[1163,3901,3733],{"class":1169},[1163,3903,3904],{"class":1191},"__:(-?",[1163,3906,3907],{"class":2013},"\\\\",[1163,3909,3910],{"class":1191},"d+):__END__",[1163,3912,3907],{"class":2013},[1163,3914,3915],{"class":1191},"r?",[1163,3917,3907],{"class":2013},[1163,3919,3920],{"class":1191},"n?",[1163,3922,3739],{"class":1169},[1163,3924,1196],{"class":1169},[1163,3926,3927],{"class":1165,"line":1199},[1163,3928,3497],{"class":2013},[13,3930,3931,3932,3935,3936,3939,3940,3943],{},"Das ",[119,3933,3934],{},"\\\\r?\\\\n?"," am Ende ist ein subtiler aber wichtiger Punkt: je nachdem wie bash den Prompt rendert (auf einer frischen Zeile oder direkt nach dem letzten Output), kann ein Newline folgen oder auch nicht. Die Regex toleriert beide Fälle. Die Gruppe ",[119,3937,3938],{},"(-?\\\\d+)"," fängt den Exit-Code ein, inklusive negativer Werte für Signale wie ",[119,3941,3942],{},"130"," oder bei unüblichen Konventionen.",[3272,3945,3947],{"id":3946},"der-output-parser","Der Output-Parser",[13,3949,3950],{},"Jeder PTY-Chunk der reinkommt wird an einen pending-Buffer angehängt und nach dem Marker gescannt. Wenn der Marker gefunden wird, ist alles davor das Output des gerade beendeten Commands:",[625,3952,3954],{"className":2000,"code":3953,"language":2002,"meta":314,"style":314},"private handleData(chunk: string): void {\n  this.pending += chunk\n\n  for (;;) {\n    const match = this.pending.match(this.markerRegex)\n    if (!match || match.index === undefined) break\n\n    const before = this.pending.slice(0, match.index)\n    const exitCode = Number(match[1])\n\n    // Alles vor dem Marker ist Command-Output.\n    if (before.length > 0) {\n      this.currentLineBuffer += before\n      this.events.onOutput(before)\n    }\n\n    // Marker und alles davor aus dem Buffer werfen.\n    this.pending = this.pending.slice(match.index + match[0].length)\n\n    // Command ist fertig — Frame an den Consumer.\n    const frame = { output: this.currentLineBuffer, exitCode }\n    this.currentLineBuffer = ''\n    this.events.onLineDone(frame)\n  }\n\n  // Was jetzt noch in `pending` liegt, ist entweder partieller Output\n  // oder ein angefangener Marker der im nächsten Chunk weitergeht.\n}\n",[119,3955,3956,3972,3986,3990,4005,4035,4068,4072,4105,4129,4133,4138,4162,4175,4193,4198,4203,4209,4252,4257,4263,4291,4303,4322,4328,4333,4339,4345],{"__ignoreMap":314},[1163,3957,3958,3961,3964,3967,3970],{"class":1165,"line":1166},[1163,3959,3960],{"class":2013},"private ",[1163,3962,3963],{"class":2009},"handleData",[1163,3965,3966],{"class":2013},"(chunk: string): ",[1163,3968,3969],{"class":1169},"void",[1163,3971,3614],{"class":1169},[1163,3973,3974,3977,3980,3983],{"class":1165,"line":315},[1163,3975,3976],{"class":1169},"  this.",[1163,3978,3979],{"class":2013},"pending",[1163,3981,3982],{"class":1169}," +=",[1163,3984,3985],{"class":2013}," chunk\n",[1163,3987,3988],{"class":1165,"line":1199},[1163,3989,3458],{"emptyLinePlaceholder":329},[1163,3991,3992,3995,3997,4000,4003],{"class":1165,"line":1220},[1163,3993,3994],{"class":2028},"  for",[1163,3996,2185],{"class":3619},[1163,3998,3999],{"class":1169},";;",[1163,4001,4002],{"class":3619},") ",[1163,4004,1170],{"class":1169},[1163,4006,4007,4010,4013,4016,4019,4021,4023,4026,4028,4030,4033],{"class":1165,"line":1244},[1163,4008,4009],{"class":1178},"    const",[1163,4011,4012],{"class":2013}," match",[1163,4014,4015],{"class":1169}," =",[1163,4017,4018],{"class":1169}," this.",[1163,4020,3979],{"class":2013},[1163,4022,257],{"class":1169},[1163,4024,4025],{"class":2009},"match",[1163,4027,2065],{"class":3619},[1163,4029,3463],{"class":1169},[1163,4031,4032],{"class":2013},"markerRegex",[1163,4034,3497],{"class":3619},[1163,4036,4037,4040,4042,4045,4047,4050,4052,4054,4057,4060,4063,4065],{"class":1165,"line":1264},[1163,4038,4039],{"class":2028},"    if",[1163,4041,2185],{"class":3619},[1163,4043,4044],{"class":1169},"!",[1163,4046,4025],{"class":2013},[1163,4048,4049],{"class":1169}," ||",[1163,4051,4012],{"class":2013},[1163,4053,257],{"class":1169},[1163,4055,4056],{"class":2013},"index",[1163,4058,4059],{"class":1169}," ===",[1163,4061,4062],{"class":1169}," undefined",[1163,4064,4002],{"class":3619},[1163,4066,4067],{"class":2028},"break\n",[1163,4069,4070],{"class":1165,"line":3688},[1163,4071,3458],{"emptyLinePlaceholder":329},[1163,4073,4074,4076,4079,4081,4083,4085,4087,4090,4092,4095,4097,4099,4101,4103],{"class":1165,"line":3704},[1163,4075,4009],{"class":1178},[1163,4077,4078],{"class":2013}," before",[1163,4080,4015],{"class":1169},[1163,4082,4018],{"class":1169},[1163,4084,3979],{"class":2013},[1163,4086,257],{"class":1169},[1163,4088,4089],{"class":2009},"slice",[1163,4091,2065],{"class":3619},[1163,4093,4094],{"class":3476},"0",[1163,4096,2017],{"class":1169},[1163,4098,4012],{"class":2013},[1163,4100,257],{"class":1169},[1163,4102,4056],{"class":2013},[1163,4104,3497],{"class":3619},[1163,4106,4107,4109,4112,4114,4117,4119,4121,4124,4126],{"class":1165,"line":3711},[1163,4108,4009],{"class":1178},[1163,4110,4111],{"class":2013}," exitCode",[1163,4113,4015],{"class":1169},[1163,4115,4116],{"class":2009}," Number",[1163,4118,2065],{"class":3619},[1163,4120,4025],{"class":2013},[1163,4122,4123],{"class":3619},"[",[1163,4125,2165],{"class":3476},[1163,4127,4128],{"class":3619},"])\n",[1163,4130,4131],{"class":1165,"line":3744},[1163,4132,3458],{"emptyLinePlaceholder":329},[1163,4134,4135],{"class":1165,"line":3750},[1163,4136,4137],{"class":3707},"    // Alles vor dem Marker ist Command-Output.\n",[1163,4139,4140,4142,4144,4147,4149,4152,4155,4158,4160],{"class":1165,"line":3777},[1163,4141,4039],{"class":2028},[1163,4143,2185],{"class":3619},[1163,4145,4146],{"class":2013},"before",[1163,4148,257],{"class":1169},[1163,4150,4151],{"class":2013},"length",[1163,4153,4154],{"class":1169}," >",[1163,4156,4157],{"class":3476}," 0",[1163,4159,4002],{"class":3619},[1163,4161,1170],{"class":1169},[1163,4163,4164,4167,4170,4172],{"class":1165,"line":3794},[1163,4165,4166],{"class":1169},"      this.",[1163,4168,4169],{"class":2013},"currentLineBuffer",[1163,4171,3982],{"class":1169},[1163,4173,4174],{"class":2013}," before\n",[1163,4176,4177,4179,4182,4184,4187,4189,4191],{"class":1165,"line":3810},[1163,4178,4166],{"class":1169},[1163,4180,4181],{"class":2013},"events",[1163,4183,257],{"class":1169},[1163,4185,4186],{"class":2009},"onOutput",[1163,4188,2065],{"class":3619},[1163,4190,4146],{"class":2013},[1163,4192,3497],{"class":3619},[1163,4194,4195],{"class":1165,"line":3816},[1163,4196,4197],{"class":1169},"    }\n",[1163,4199,4201],{"class":1165,"line":4200},16,[1163,4202,3458],{"emptyLinePlaceholder":329},[1163,4204,4206],{"class":1165,"line":4205},17,[1163,4207,4208],{"class":3707},"    // Marker und alles davor aus dem Buffer werfen.\n",[1163,4210,4212,4215,4217,4219,4221,4223,4225,4227,4229,4231,4233,4235,4238,4240,4242,4244,4246,4248,4250],{"class":1165,"line":4211},18,[1163,4213,4214],{"class":1169},"    this.",[1163,4216,3979],{"class":2013},[1163,4218,4015],{"class":1169},[1163,4220,4018],{"class":1169},[1163,4222,3979],{"class":2013},[1163,4224,257],{"class":1169},[1163,4226,4089],{"class":2009},[1163,4228,2065],{"class":3619},[1163,4230,4025],{"class":2013},[1163,4232,257],{"class":1169},[1163,4234,4056],{"class":2013},[1163,4236,4237],{"class":1169}," +",[1163,4239,4012],{"class":2013},[1163,4241,4123],{"class":3619},[1163,4243,4094],{"class":3476},[1163,4245,3609],{"class":3619},[1163,4247,257],{"class":1169},[1163,4249,4151],{"class":2013},[1163,4251,3497],{"class":3619},[1163,4253,4255],{"class":1165,"line":4254},19,[1163,4256,3458],{"emptyLinePlaceholder":329},[1163,4258,4260],{"class":1165,"line":4259},20,[1163,4261,4262],{"class":3707},"    // Command ist fertig — Frame an den Consumer.\n",[1163,4264,4266,4268,4271,4273,4275,4278,4280,4282,4284,4286,4288],{"class":1165,"line":4265},21,[1163,4267,4009],{"class":1178},[1163,4269,4270],{"class":2013}," frame",[1163,4272,4015],{"class":1169},[1163,4274,3435],{"class":1169},[1163,4276,4277],{"class":3619}," output",[1163,4279,1185],{"class":1169},[1163,4281,4018],{"class":1169},[1163,4283,4169],{"class":2013},[1163,4285,2017],{"class":1169},[1163,4287,4111],{"class":2013},[1163,4289,4290],{"class":1169}," }\n",[1163,4292,4294,4296,4298,4300],{"class":1165,"line":4293},22,[1163,4295,4214],{"class":1169},[1163,4297,4169],{"class":2013},[1163,4299,4015],{"class":1169},[1163,4301,4302],{"class":1169}," ''\n",[1163,4304,4306,4308,4310,4312,4315,4317,4320],{"class":1165,"line":4305},23,[1163,4307,4214],{"class":1169},[1163,4309,4181],{"class":2013},[1163,4311,257],{"class":1169},[1163,4313,4314],{"class":2009},"onLineDone",[1163,4316,2065],{"class":3619},[1163,4318,4319],{"class":2013},"frame",[1163,4321,3497],{"class":3619},[1163,4323,4325],{"class":1165,"line":4324},24,[1163,4326,4327],{"class":1169},"  }\n",[1163,4329,4331],{"class":1165,"line":4330},25,[1163,4332,3458],{"emptyLinePlaceholder":329},[1163,4334,4336],{"class":1165,"line":4335},26,[1163,4337,4338],{"class":3707},"  // Was jetzt noch in `pending` liegt, ist entweder partieller Output\n",[1163,4340,4342],{"class":1165,"line":4341},27,[1163,4343,4344],{"class":3707},"  // oder ein angefangener Marker der im nächsten Chunk weitergeht.\n",[1163,4346,4348],{"class":1165,"line":4347},28,[1163,4349,1267],{"class":1169},[13,4351,4352,4353,4356,4357,4360,4361,4364],{},"Der subtile Punkt ist die Behandlung von ",[152,4354,4355],{},"partiellen Markern",". Ein PTY-Chunk kann mitten im Marker enden — bash hat den Anfang geschrieben, der Rest kommt mit dem nächsten ",[119,4358,4359],{},"data","-Event. Wenn du den pending-Buffer zwischenzeitlich weiterschiebst (etwa auf den letzten Newline trimst), zerstörst du den teilweisen Marker und die Erkennung schlägt fehl. Die Lösung: ",[152,4362,4363],{},"halte alle unmatched Bytes im pending-Buffer",", bis entweder der Marker komplett ankommt oder der Stream endet.",[3272,4366,4368],{"id":4367},"die-bootstrap-phase","Die Bootstrap-Phase",[13,4370,4371,4372,4374,4375,4378,4379,4382],{},"Ein letztes Detail das einen ersten Durchlauf ruiniert: wenn du bash startest, wird zuerst ",[119,4373,3340],{}," geladen. Das produziert oft Output — MOTDs, Shell-Init-Meldungen, ",[119,4376,4377],{},"nvm","-Status-Prints, alles Mögliche. Der erste Marker den du siehst, ist ",[152,4380,4381],{},"nicht"," das Ende eines User-Commands. Er ist das Ende des Startup-Prozesses.",[13,4384,4385],{},"Das heißt: zwei Phasen im State.",[146,4387,4388,4394],{},[149,4389,4390,4393],{},[152,4391,4392],{},"Phase 1 — Bootstrap:"," warte auf den ersten Marker. Verwirf alles was davor kam (Startup-Noise). Signalisiere dem Consumer \"bash is ready\".",[149,4395,4396,4399,4400,4402],{},[152,4397,4398],{},"Phase 2 — Normal:"," jeder weitere Marker ist das Ende eines User-Commands. Frames gehen via ",[119,4401,4314],{}," an den Consumer.",[13,4404,4405,4406,1185],{},"Das ist in ape-shell ein einzelnes Boolean namens ",[119,4407,4408],{},"readyForFirstLine",[625,4410,4412],{"className":2000,"code":4411,"language":2002,"meta":314,"style":314},"if (!this.readyForFirstLine) {\n  // Bootstrap-Prompt: Startup-Noise wegwerfen, Ready signalisieren.\n  // onLineDone feuert hier bewusst NICHT — das wäre ein Fake-Frame\n  // aus Sicht des Consumers.\n  this.readyForFirstLine = true\n  this.currentLineBuffer = ''\n  const resolve = this.awaitingInitialPrompt\n  this.awaitingInitialPrompt = null\n  if (resolve) resolve()\n  continue\n}\n\n// Echtes Command-Ende: Frame übergeben.\nconst frame = { output: this.currentLineBuffer, exitCode }\nthis.currentLineBuffer = ''\nthis.events.onLineDone(frame)\n",[119,4413,4414,4428,4433,4438,4443,4455,4465,4480,4492,4509,4514,4518,4522,4527,4554,4565],{"__ignoreMap":314},[1163,4415,4416,4418,4420,4423,4426],{"class":1165,"line":1166},[1163,4417,3848],{"class":2028},[1163,4419,2185],{"class":2013},[1163,4421,4422],{"class":1169},"!this.",[1163,4424,4425],{"class":2013},"readyForFirstLine) ",[1163,4427,1170],{"class":1169},[1163,4429,4430],{"class":1165,"line":315},[1163,4431,4432],{"class":3707},"  // Bootstrap-Prompt: Startup-Noise wegwerfen, Ready signalisieren.\n",[1163,4434,4435],{"class":1165,"line":1199},[1163,4436,4437],{"class":3707},"  // onLineDone feuert hier bewusst NICHT — das wäre ein Fake-Frame\n",[1163,4439,4440],{"class":1165,"line":1220},[1163,4441,4442],{"class":3707},"  // aus Sicht des Consumers.\n",[1163,4444,4445,4447,4449,4451],{"class":1165,"line":1244},[1163,4446,3976],{"class":1169},[1163,4448,4408],{"class":2013},[1163,4450,4015],{"class":1169},[1163,4452,4454],{"class":4453},"sfNiH"," true\n",[1163,4456,4457,4459,4461,4463],{"class":1165,"line":1264},[1163,4458,3976],{"class":1169},[1163,4460,4169],{"class":2013},[1163,4462,4015],{"class":1169},[1163,4464,4302],{"class":1169},[1163,4466,4467,4470,4473,4475,4477],{"class":1165,"line":3688},[1163,4468,4469],{"class":1178},"  const",[1163,4471,4472],{"class":2013}," resolve",[1163,4474,4015],{"class":1169},[1163,4476,4018],{"class":1169},[1163,4478,4479],{"class":2013},"awaitingInitialPrompt\n",[1163,4481,4482,4484,4487,4489],{"class":1165,"line":3704},[1163,4483,3976],{"class":1169},[1163,4485,4486],{"class":2013},"awaitingInitialPrompt",[1163,4488,4015],{"class":1169},[1163,4490,4491],{"class":1169}," null\n",[1163,4493,4494,4497,4499,4502,4504,4506],{"class":1165,"line":3711},[1163,4495,4496],{"class":2028},"  if",[1163,4498,2185],{"class":3619},[1163,4500,4501],{"class":2013},"resolve",[1163,4503,4002],{"class":3619},[1163,4505,4501],{"class":2009},[1163,4507,4508],{"class":3619},"()\n",[1163,4510,4511],{"class":1165,"line":3744},[1163,4512,4513],{"class":2028},"  continue\n",[1163,4515,4516],{"class":1165,"line":3750},[1163,4517,1267],{"class":1169},[1163,4519,4520],{"class":1165,"line":3777},[1163,4521,3458],{"emptyLinePlaceholder":329},[1163,4523,4524],{"class":1165,"line":3794},[1163,4525,4526],{"class":3707},"// Echtes Command-Ende: Frame übergeben.\n",[1163,4528,4529,4532,4535,4537,4539,4541,4543,4545,4547,4549,4552],{"class":1165,"line":3810},[1163,4530,4531],{"class":1178},"const",[1163,4533,4534],{"class":2013}," frame ",[1163,4536,3469],{"class":1169},[1163,4538,3435],{"class":1169},[1163,4540,4277],{"class":3619},[1163,4542,1185],{"class":1169},[1163,4544,4018],{"class":1169},[1163,4546,4169],{"class":2013},[1163,4548,2017],{"class":1169},[1163,4550,4551],{"class":2013}," exitCode ",[1163,4553,1267],{"class":1169},[1163,4555,4556,4558,4561,4563],{"class":1165,"line":3816},[1163,4557,3463],{"class":1169},[1163,4559,4560],{"class":2013},"currentLineBuffer ",[1163,4562,3469],{"class":1169},[1163,4564,4302],{"class":1169},[1163,4566,4567,4569,4571,4573,4575],{"class":1165,"line":4200},[1163,4568,3463],{"class":1169},[1163,4570,4181],{"class":2013},[1163,4572,257],{"class":1169},[1163,4574,4314],{"class":2009},[1163,4576,4577],{"class":2013},"(frame)\n",[13,4579,4580],{},"Ohne diese Trennung bekommt der Consumer beim Start einen kaputten Frame mit allem rcfile-Noise als \"Output\" und einem willkürlichen Exit-Code. Das ist die Art Bug die du erst beim fünften User bemerkst, und dann ist die Ursache hart zu finden.",[28,4582,4584],{"id":4583},"eine-anwendung-ape-shells-ptybridge","Eine Anwendung: ape-shell's PtyBridge",[13,4586,4587,4588,4594,4595,4602],{},"Ich nutze dieses Pattern in ",[306,4589,4592],{"href":4590,"rel":4591},"https://github.com/openape-ai/openape/tree/main/packages/apes",[310],[119,4593,2714],{},", einem grant-secured Shell-Wrapper den ich für AI-Agent-Workflows baue. Die konkrete Implementation liegt in ",[306,4596,4599],{"href":4597,"rel":4598},"https://github.com/openape-ai/openape/blob/main/packages/apes/src/shell/pty-bridge.ts",[310],[119,4600,4601],{},"packages/apes/src/shell/pty-bridge.ts"," — etwas mehr als 200 Zeilen TypeScript die den kompletten Cycle abbilden: Spawn, Bootstrap, Line-Detection, Streaming-Output, Exit-Handling.",[13,4604,4605,4606,4609,4610,4613],{},"In ape-shell sitzt zwischen User-Eingabe und bash noch ein Grant-Check. Die PtyBridge selbst weiß davon nichts — sie kümmert sich nur um die saubere Abstraktion ",[20,4607,4608],{},"\"bash ist fertig mit dieser Zeile, hier ist der Output und der Exit-Code\"",". Der Grant-Layer darüber entscheidet ob eine Zeile überhaupt bei ",[119,4611,4612],{},"writeLine"," landet. Die Trennung der Concerns ist einer der Gründe warum sich das Pattern so natürlich anfühlt: die Marker-Detection ist ein universelles Problem, die Grant-Logik ist spezifisch.",[28,4615,4617],{"id":4616},"abschluss","Abschluss",[13,4619,4620],{},"Das Pattern ist nicht neu. Terminal-Emulatoren, REPL-Orchestratoren, Shell-Testing-Frameworks — sie alle lösen irgendeine Variante davon seit Jahrzehnten. expect, pexpect, bash-it's Test-Suite, IPython-Kernel, Jupyter-Frontends: alle haben irgendwo einen Marker-Trick. Aber er wird selten explizit beschrieben. Die meisten Entwickler die einen Shell-Wrapper schreiben stolpern selbst über die Lösung, manchmal erst nach der dritten naiven Implementation mit Timeouts und Newline-Heuristiken.",[13,4622,4623],{},"Wenn du irgendwann ein Tool baust das eine persistente Shell (oder ein anderes REPL mit Prompt-basiertem \"ready\"-Signal) kontrollieren will: das ist wahrscheinlich das Pattern das du suchst. Boring Infrastructure in its best sense — unsichtbar wenn es funktioniert, kritisch wenn es fehlt.",[1762,4625,4626],{},"html pre.shiki code .s7zQu, html code.shiki .s7zQu{--shiki-light:#39ADB5;--shiki-light-font-style:italic;--shiki-default:#89DDFF;--shiki-default-font-style:italic;--shiki-dark:#89DDFF;--shiki-dark-font-style:italic}html pre.shiki code .sMK4o, html code.shiki .sMK4o{--shiki-light:#39ADB5;--shiki-default:#89DDFF;--shiki-dark:#89DDFF}html pre.shiki code .sTEyZ, html code.shiki .sTEyZ{--shiki-light:#90A4AE;--shiki-default:#EEFFFF;--shiki-dark:#BABED8}html pre.shiki code .sfazB, html code.shiki .sfazB{--shiki-light:#91B859;--shiki-default:#C3E88D;--shiki-dark:#C3E88D}html pre.shiki code .s2Zo4, html code.shiki .s2Zo4{--shiki-light:#6182B8;--shiki-default:#82AAFF;--shiki-dark:#82AAFF}html pre.shiki code .sbssI, html code.shiki .sbssI{--shiki-light:#F76D47;--shiki-default:#F78C6C;--shiki-dark:#F78C6C}html .light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html.light .shiki span {color: var(--shiki-light);background: var(--shiki-light-bg);font-style: var(--shiki-light-font-style);font-weight: var(--shiki-light-font-weight);text-decoration: var(--shiki-light-text-decoration);}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .swJcz, html code.shiki .swJcz{--shiki-light:#E53935;--shiki-default:#F07178;--shiki-dark:#F07178}html pre.shiki code .sHwdD, html code.shiki .sHwdD{--shiki-light:#90A4AE;--shiki-light-font-style:italic;--shiki-default:#546E7A;--shiki-default-font-style:italic;--shiki-dark:#676E95;--shiki-dark-font-style:italic}html pre.shiki code .spNyl, html code.shiki .spNyl{--shiki-light:#9C3EDA;--shiki-default:#C792EA;--shiki-dark:#C792EA}html pre.shiki code .sfNiH, html code.shiki .sfNiH{--shiki-light:#FF5370;--shiki-default:#FF9CAC;--shiki-dark:#FF9CAC}",{"title":314,"searchDepth":315,"depth":315,"links":4628},[4629,4630,4637,4638,4645,4646],{"id":3203,"depth":315,"text":3204},{"id":3269,"depth":315,"text":3270,"children":4631},[4632,4633,4634,4635,4636],{"id":3274,"depth":1199,"text":3275},{"id":3295,"depth":1199,"text":3296},{"id":3306,"depth":1199,"text":3307},{"id":3333,"depth":1199,"text":3334},{"id":3351,"depth":1199,"text":3352},{"id":3368,"depth":315,"text":3369},{"id":3395,"depth":315,"text":3396,"children":4639},[4640,4641,4642,4643,4644],{"id":3399,"depth":1199,"text":3400},{"id":3524,"depth":1199,"text":3525},{"id":3860,"depth":1199,"text":3861},{"id":3946,"depth":1199,"text":3947},{"id":4367,"depth":1199,"text":4368},{"id":4583,"depth":315,"text":4584},{"id":4616,"depth":315,"text":4617},"2026-04-10","Wenn du einen Shell-Wrapper baust, der eine persistente bash über mehrere Commands hinweg kontrolliert, stolperst du über eine überraschend tiefe Frage: wann ist bash mit dem aktuellen Command durch? Ein kleiner Deep-Dive über Prompt-Marker, PROMPT_COMMAND und warum die Antwort nicht ist PS1 zu lesen — sondern PS1 zu überschreiben.",{},"/blog/de/wie-weiss-ich-wann-bash-fertig-ist",{"title":3168,"description":4648},"blog/de/wie-weiss-ich-wann-bash-fertig-ist",[4654,4655,4656,4657,938],"Systems Programming","Shell","Bash","Technical Deep Dive","how-do-i-know-when-bash-is-done","zKA_O96D-Iev_ADvde1AiMzXVngL1lk4EQupMs4pzl4",{"id":4661,"title":4662,"author":8,"body":4663,"date":4969,"description":4970,"draft":932,"extension":330,"image":3,"meta":4971,"navigation":329,"path":4972,"seo":4973,"stem":4974,"tags":4975,"translationKey":4977,"__hash__":4978},"blog_de/blog/de/wie-aus-einem-login-modul-ein-protokoll-wurde.md","Wie aus einem Login-Modul ein Protokoll wurde",{"type":10,"value":4664,"toc":4959},[4665,4668,4671,4674,4678,4684,4687,4698,4709,4713,4719,4726,4736,4740,4743,4746,4753,4763,4767,4773,4780,4787,4791,4794,4801,4805,4812,4815,4833,4836,4840,4843,4912,4922,4925,4929,4932,4935,4942,4945,4947,4956],[13,4666,4667],{},"Ich wollte ein Login-Modul bauen.",[13,4669,4670],{},"Das war im November. Ein Nuxt-Modul, mit dem Web-Apps WebAuthn-Passkeys statt Passwörter unterstützen können. Klein, fokussiert, eine Aufgabe. Ein Wochenende, dann fertig.",[13,4672,4673],{},"So war zumindest der Plan.",[28,4675,4677],{"id":4676},"problem-1-wer-ist-der-identity-provider","Problem 1: Wer ist der Identity Provider?",[13,4679,4680,4681],{},"Sobald man WebAuthn implementiert, stößt man auf eine unscheinbare Frage: ",[152,4682,4683],{},"Woher weiß ein Service Provider, an welchen Identity Provider er sich wenden soll?",[13,4685,4686],{},"Bei Auth0 oder Clerk ist die Antwort einfach: Du bist beim Anbieter registriert, der Anbieter ist hardcoded. Aber sobald du dezentral denkst, fehlt die Auflösung. Ein User gibt seine E-Mail-Adresse ein - und dann?",[13,4688,4689,4690,4693,4694,4697],{},"Die Antwort liegt seit 1983 herum: ",[152,4691,4692],{},"DNS",". Jede E-Mail-Adresse hat eine Domain. Jede Domain kann TXT-Records haben. Also habe ich DNS Discovery gebaut: Ein TXT-Record an ",[119,4695,4696],{},"_ddisa.example.com"," verweist auf den zuständigen Identity Provider. Eine E-Mail-Adresse reicht, der Rest passiert über DNS.",[13,4699,4700,4701,4704,4705,4708],{},"Aus dem Login-Modul wurde ein zweites: ",[119,4702,4703],{},"@openape/core"," für die DNS-Resolution, ",[119,4706,4707],{},"@openape/auth"," für den eigentlichen WebAuthn-Flow.",[28,4710,4712],{"id":4711},"problem-2-aber-wie-authentifiziert-sich-ein-agent","Problem 2: Aber wie authentifiziert sich ein Agent?",[13,4714,4715,4716],{},"Etwa zur gleichen Zeit fingen meine eigenen Projekte an, AI Agents ernsthaft zu nutzen. Und bevor ich überhaupt über Berechtigungen nachdenken konnte, stand da eine viel grundlegendere Frage: ",[152,4717,4718],{},"Passkeys brauchen einen Finger. Agents haben keine Finger. Wie soll sich der überhaupt anmelden?",[13,4720,4721,4722,4725],{},"Also habe ich den Auth-Flow um einen zweiten Pfad erweitert: ",[152,4723,4724],{},"Ed25519 Challenge-Response, im Prinzip wie SSH-Keys."," Der Agent hat einen privaten Key, der IdP den Public Key, der IdP stellt eine Challenge, der Agent signiert. Dasselbe Pattern wie WebAuthn - nur ohne Browser und ohne Mensch in der Schleife.",[13,4727,4728,4729,4732,4733,4735],{},"Und damit passiert etwas Schönes: ",[152,4730,4731],{},"Auf Protokoll-Ebene verschwindet der Unterschied zwischen Mensch und Agent."," Beide haben eine Identität beim IdP. Beide authentifizieren sich nach demselben Schema. Beide können mit derselben CLI (",[119,4734,1149],{},") arbeiten. Der einzige Unterschied: Der Mensch hat einen Passkey am Laptop, der Agent einen Ed25519-Key im Filesystem.",[28,4737,4739],{"id":4738},"problem-3-ok-angemeldet-was-darf-er-jetzt","Problem 3: OK angemeldet - was darf er jetzt?",[13,4741,4742],{},"Der Agent kann sich anmelden. Schön. Aber darf er jetzt einfach alles? Natürlich nicht. Ich will granulare Kontrolle - diese Mail lesen ja, jene löschen nein, diesen Code Review machen ja, direkt mergen nein.",[13,4744,4745],{},"OAuth wäre die naheliegende Antwort für Autorisierung. Aber OAuth ist für Menschen designed - Authorization Code Flow, Browser-Redirect, der ganze Tanz. Für einen Background-Agent fühlt sich das falsch an.",[13,4747,4748,4749,4752],{},"Also habe ich ",[152,4750,4751],{},"Grants"," gebaut. Ein Grant ist ein vorab genehmigtes JWT, das einem Agent erlaubt, eine bestimmte Aktion auszuführen. Granular, zeitlich begrenzt, jederzeit widerrufbar. Ich genehmige einmal mit meinem Passkey - der Agent kann dann ohne weitere Interaktion handeln, aber nur innerhalb der erteilten Berechtigung.",[13,4754,4755,4756,4758,4759,4762],{},"Aus zwei Packages wurden vier: ",[119,4757,913],{}," kam dazu, dann ",[119,4760,4761],{},"@openape/proxy"," als HTTP-Gateway für Agents.",[28,4764,4766],{"id":4765},"problem-4-wie-meldet-sich-ein-agent-in-meinem-namen-an","Problem 4: Wie meldet sich ein Agent in meinem Namen an?",[13,4768,4769,4770],{},"Grants waren die Antwort auf \"der Agent darf genau diese eine Aktion\". Aber was, wenn ein Agent über längere Zeit wie ich an einem Service arbeiten soll? ",[152,4771,4772],{},"Nicht pro Call ein neues Grant, sondern eine Session - unter meiner Identität.",[13,4774,4775,4776,4779],{},"Das konnten Grants nicht. Ein Grant ist ein Ticket für einen einzelnen Call, keine Anmeldung. Also wurde daraus ein eigener Protokoll-Baustein: ",[152,4777,4778],{},"Delegation",". Ich autorisiere meinen Agent einmal, sich bei einem Service in meinem Namen anzumelden - und er läuft dann mit einer eigenen Session, als ich, aber mit einem eindeutigen Audit-Trail: \"Das war nicht Patrick selbst, das war Agent X im Auftrag von Patrick.\"",[13,4781,4782,4783,4786],{},"Technisch auf Basis von RFC 8693 (Token Exchange), mit dem ",[119,4784,4785],{},"act","-Claim aus OAuth 2.0. Standards wo möglich, eigene Erweiterungen wo nötig.",[28,4788,4790],{"id":4789},"problem-5-wo-lebt-das-schlüsselmaterial","Problem 5: Wo lebt das Schlüsselmaterial?",[13,4792,4793],{},"Bis dahin lief alles im Browser oder im Server. Aber für eine Identity-Plattform reicht das nicht. Schlüssel müssen sicher auf dem Gerät des Users liegen, nicht in irgendeinem Server-Storage. Also kam eine Desktop-App - mit Tauri v2, Vue 3 im Frontend, Rust im Backend. Plus eine Rust-CLI für Power-User und Server-Setups.",[13,4795,4796,4797,4800],{},"Die Desktop-App hat auch noch eine andere Funktion bekommen: ",[152,4798,4799],{},"Sie orchestriert AI Agents als isolierte OS-User",". Jeder Agent läuft in seinem eigenen User-Account, mit eigenen Permissions, in einer eigenen Umgebung. Das ist nicht mehr \"Identity-Modul\" - das ist Infrastruktur für die nächste Generation von AI-gestützten Workflows.",[28,4802,4804],{"id":4803},"problem-6-wie-schreibt-man-das-auf","Problem 6: Wie schreibt man das auf?",[13,4806,4807,4808,4811],{},"Irgendwann hatte ich 10 Packages, 2 Nuxt-Module, 6 Apps, eine Desktop-App, eine CLI, und keine Spezifikation. Ein Protokoll braucht aber eine Spezifikation - sonst ist es nur Code. Also habe ich angefangen, ",[152,4809,4810],{},"DDISA"," zu schreiben: DNS-Discoverable Identity & Service Authorization.",[13,4813,4814],{},"Drei Dokumente:",[146,4816,4817,4823,4828],{},[149,4818,4819,4822],{},[152,4820,4821],{},"Core",": DNS Discovery, OIDC-Erweiterungen, WebAuthn- und Ed25519-Auth-Flows, Token-Format",[149,4824,4825,4827],{},[152,4826,4751],{},": Grant-basierte Authorization REST API, AuthZ-JWT, Polling-Modell",[149,4829,4830,4832],{},[152,4831,4778],{},": Delegationsprotokoll auf Basis von RFC 8693",[13,4834,4835],{},"Plus JSON Schemas (Draft 2020-12) für alle Datenformate, plus vollständige HTTP-Beispiele für jeden Flow. Compliance-Levels für Implementierungen: Core, Core+Grants, Core+Grants+Delegation.",[28,4837,4839],{"id":4838},"wo-das-heute-steht","Wo das heute steht",[13,4841,4842],{},"Heute, am 9. April 2026, sieht OpenApe so aus:",[1432,4844,4845,4855],{},[1435,4846,4847],{},[1438,4848,4849,4852],{},[1441,4850,4851],{},"Komponente",[1441,4853,4854],{},"Beschreibung",[1450,4856,4857,4867,4877,4887,4902],{},[1438,4858,4859,4864],{},[1455,4860,4861],{},[152,4862,4863],{},"Protokoll",[1455,4865,4866],{},"3 Specs (Core, Grants, Delegation), JSON Schemas, vollständige Beispiele",[1438,4868,4869,4874],{},[1455,4870,4871],{},[152,4872,4873],{},"Monorepo",[1455,4875,4876],{},"10 npm-Packages, 2 Nuxt-Module, 6 deployte Apps",[1438,4878,4879,4884],{},[1455,4880,4881],{},[152,4882,4883],{},"Desktop App",[1455,4885,4886],{},"Tauri v2, orchestriert AI Agents als isolierte OS-User",[1438,4888,4889,4894],{},[1455,4890,4891],{},[152,4892,4893],{},"CLI",[1455,4895,4896,4898,4899,4901],{},[119,4897,1149],{}," für Grant-Management, ",[119,4900,2714],{}," als grant-secured Shell",[1438,4903,4904,4909],{},[1455,4905,4906],{},[152,4907,4908],{},"Free IdP",[1455,4910,4911],{},"Hosted Identity Provider, kostenlos nutzbar",[13,4913,4914,4915,4917,4918,4921],{},"Heute Morgen habe ich ",[119,4916,2714],{}," committed - eine Shell-Replacement, die jeden Befehl durch das Grant-System schickt. ",[119,4919,4920],{},"ape-shell -c \"git status\""," requestet einen Grant, der für die Session gilt. Folgekommandos nutzen denselben Grant. Zero-Latency-Re-Execution mit menschlicher Kontrolle.",[13,4923,4924],{},"Im November wollte ich ein Login-Modul bauen.",[28,4926,4928],{"id":4927},"was-ich-daraus-gelernt-habe","Was ich daraus gelernt habe",[13,4930,4931],{},"Die wichtigsten Projekte entstehen nicht am Whiteboard. Sie entstehen, wenn du ein konkretes Problem löst und dabei merkst, dass das Problem ein anderes Problem versteckt hat. Und das nächste. Und das übernächste.",[13,4933,4934],{},"Wenn ich im November einen \"Plan für ein dezentrales Identity-Protokoll\" gemacht hätte, hätte ich mich überfordert. Stattdessen habe ich ein Login-Modul gebaut. Das ging. Dann das nächste Stück. Auch das ging. Und so weiter, bis das Ergebnis größer war als der ursprüngliche Plan.",[13,4936,4937,4938,4941],{},"Der einzige Grund, warum es funktioniert hat: ",[152,4939,4940],{},"Jeder Schritt war für sich genommen klein und konkret."," Die Vision ist erst rückblickend entstanden. Sie war kein Startpunkt, sie war eine Folge.",[13,4943,4944],{},"Das ist vielleicht das, was an \"Building in Public\" wirklich wertvoll ist: nicht das öffentliche Zeigen, sondern das öffentliche Eingeständnis, dass du nicht von Anfang an wusstest, was du baust. Du hast es herausgefunden, indem du gebaut hast. Zumindest war es bei mir so.",[298,4946],{},[13,4948,4949,4950,4955],{},"OpenApe ist Open Source. Die Protokoll-Spezifikation, der Code, die Apps - alles auf ",[306,4951,4954],{"href":4952,"rel":4953},"https://github.com/openape-ai",[310],"github.com/openape-ai",". Wenn du an dezentraler Identität, AI-Agent-Authorization oder einfach an interessanten Protokoll-Designs interessiert bist: schau rein, mach Issues auf, schreib mir.",[13,4957,4958],{},"Was war dein letztes Projekt, das dir entwachsen ist?",{"title":314,"searchDepth":315,"depth":315,"links":4960},[4961,4962,4963,4964,4965,4966,4967,4968],{"id":4676,"depth":315,"text":4677},{"id":4711,"depth":315,"text":4712},{"id":4738,"depth":315,"text":4739},{"id":4765,"depth":315,"text":4766},{"id":4789,"depth":315,"text":4790},{"id":4803,"depth":315,"text":4804},{"id":4838,"depth":315,"text":4839},{"id":4927,"depth":315,"text":4928},"2026-04-09","Im November wollte ich ein Nuxt-Modul für WebAuthn-Passkeys bauen. Heute besteht OpenApe aus 10 npm-Packages, 2 Nuxt-Modulen, einem Protokoll und einer Desktop-App. Eine Geschichte über Probleme, die andere Probleme aufdecken.",{},"/blog/de/wie-aus-einem-login-modul-ein-protokoll-wurde",{"title":4662,"description":4970},"blog/de/wie-aus-einem-login-modul-ein-protokoll-wurde",[938,4810,4976,340],"Decentralized Identity","from-login-module-to-protocol","Hl1a3_dpwWvBB2QQmpq2n_5X1JvXbvTiZOwBzNYamyA",1776970806600]