ai newsSo., 25. Okt.
EilmeldungGive Claude a read-only straw to your SQLite DB: a tiny MCP server in Python

Publish-Gate stoppte in zehn Tagen 23 fehlerhafte Posts von Claude Code

Wer Claude Code Veröffentlichungen oder Löschungen überlässt, sollte sich nicht auf das Prompt-Signal 'sei vorsichtig' verlassen. Ein kleines Zwischenskript prüft die tatsächliche Ausgabe, bevor irreversible Schritte passieren. Innerhalb von zehn Tagen griff ein solches Gate 248 Mal.

dev.toSonntag 06:00 · 25.10.DE6 Min. Lesezeit
Bild: dev.to

Veröffentlicht am 25. Okt. • Zuerst erschienen auf my-blog.org

Wenn Claude Code irgendetwas ausfuehrt, das fuer dich veroeffentlicht, postet oder loescht, solltest du dich nicht auf einen Prompt verlassen, der sagt: 'sei vorsichtig'. Schiebe ein kleines Skript zwischen das Modell und den irreversiblen Schritt, und lasse dieses Skript die tatsaechliche Ausgabe lesen. In 10 Tagen (28.09.2026 bis 07.10.2026) lief eine solche Sperre 248 Mal in meiner von Claude Code betriebenen Blog-Pipeline und blockierte 37 dieser Laeufe. Das verhinderte 23 verschiedene Beitraege, bevor sie live gingen.

Keine dieser Zahlen stammt aus einer 'fertig!'-Nachricht eines Modells. Sie stammen aus append-only JSON-Ledgern, die jedes Skript bei jedem Lauf schreibt. Nachfolgend die Zahlen, die vier Fehler, die jede Regel erzwungen haben, und die Schritte zum Nachbauen. Die ausfuehrlichere Darstellung mit den vollstaendigen Tabellen befindet sich im Originalbeitrag.

Claude Code schreibt und bearbeitet die Python-Skripte, ein Windows-Scheduler fuehrt sie aus, und jedes Skript haengt pro Lauf eine JSON-Zeile unter data/ops/ an. Ich habe die Ledgers am 07.10.2026 Zeile fuer Zeile gezaehlt.

Ueber ein laengeres Zeitfenster von 49 Tagen mit Laeufen hinweg protokollierten die geplanten Jobs 1.243 unbeaufsichtigte Laeufe ueber 44 Jobs. 997 meldeten Erfolg mit Ausgabe, 207 meldeten Erfolg mit null Ausgabe, und 39 meldeten Fehlschlag.

Ein einzelner Lauf kann zwei Defekte tragen, daher ergeben sich 38 Gruende ueber die 37 blockierten Laeufe:

Die meisten Blockaden sind unspektakulaer. Genau das ist der Punkt: Es sind die guenstigen, mechanischen Fehler, die ein Modell macht, waehrend es meldet, dass alles in Ordnung ist.

Jede Sperre existiert, weil etwas schiefging, waehrend jede Pruefung alles als in Ordnung meldete.

1. 132 Beitraege erschienen ohne Bilder. Die Pipeline hatte 132 Beitraege ohne Bilder im Textkoerper veroeffentlicht, und jeder Verifikationslauf war bestanden. Die Pruefung betrachtete das Social-Preview-Bild und nahm jeden Beitrag davon aus, der eines hatte. Die Loesung war eine gemeinsame Sperre, die die tatsaechlich in den Koerper eingebetteten Diagramme zaehlt, woraus fig-embed und fig-count stammen.

2. Ein Beitrag ging live, dann stuerzte der Job ab, bevor er sein Ledger schrieb. Der taegliche Job veroeffentlichte um 05:01:20 und starb um 05:03 mit ValueError: I/O operation on closed file. Ein spaeterer Import ersetzte sys.stdout durch einen neuen Wrapper, der alte Wrapper wurde vom Garbage Collector eingesammelt und schloss den gemeinsamen Puffer. Kein Ledger-Eintrag bedeutete, dass der Lauf am naechsten Morgen denselben Beitrag erneut veroeffentlicht haette. Die Loesung: stattdessen sys.stdout.reconfigure verwenden und die Ledger-Zeile unmittelbar nach jeder Veroeffentlichung schreiben statt am Ende des Batches.

3. Ein --dry-Lauf ueberschrieb eine Eingabedatei. Ein Probelauf eines Social-Follow-Skripts ueberschrieb seine Zielliste mit 30 Kandidaten, die der Besitzer bereits abgelehnt hatte. Der naechste geplante Lauf haette ihnen gefolgt, und dev.to hat keine Unfollow-API. Die Regel: Dry bedeutet keine Statusaenderungen, nicht 'eine andere Ausgabe'. Wenn ein Dry-Pfad auch nur eine Datei schreibt, ist er nicht dry.

4. Durch einen Shell-Heredoc eingefuegter Code brach den Publisher zweimal. Ein \n innerhalb eines f-Strings wurde zu einem echten Zeilenumbruch, was ein SyntaxError war. Schlimmer noch: \b in einem Regex wurde zum Steuerzeichen 0x08, sodass der Bildzaehler immer 0 zurueckgab. Dieser bestand die Syntaxpruefung und zeigte sich nur als '2 Diagramme eingefuegt, 0 Bilder gezaehlt'. Jetzt geht jeder Code mit Backslashes durch das File-Edit-Tool, und anschliessend laeuft ein Scan auf Steuerzeichen.

Ein Prompt wie 'pruefe deine Arbeit' fordert das Modell auf, sich selbst zu benoten. Claude Code ist gut darin, einen Pruefer zu schreiben, aber es ist kein zuverlaessiger Zeuge seines eigenen Laufs, weil das Log des Laufs und das Ergebnis des Laufs auseinanderfallen koennen. Fehler 2 ist genau das: der Beitrag existierte, und der Eintrag dazu nicht.

Eine Sperre trennt beides. Sie oeffnet die Ausgabe (das HTML, die Diagrammdateien, das Ledger) und liefert einen Exit-Code, und der Publisher weigert sich, bei einem Defekt fortzufahren. Claude Code hat dann einen konkreten, maschinenlesbaren Grund, etwas zu korrigieren. Von den 23 gestoppten Beitraegen wurden 11 korrigiert und bestanden den Test erneut; die anderen 12 bestanden in diesem Zeitfenster nie, also gingen sie nie mit dem Defekt durch den Publisher.

Ein minimales Grundgeruest mit den drei Exit-Codes und der Ledger-Zeile:

import json, sys, time from pathlib import Path def check ( html_path: Path ) -> list [ str ]: html = html_path. read_text ( encoding = " utf-8 " ) defects = [] if html. count ( " <img " ) < 2: defects. append ( " fig-count " ) return defects def main ( paths: list [ str ]) -> int: targets = [ Path ( p ) for p in paths if Path ( p ). is_file ()] if not targets: return 2 # nothing to check means the checker is broken worst = 0 with open ( " gate.jsonl ", " a ", encoding = " utf-8 " ) as ledger: for t in targets: defects = check ( t ) code = 1 if defects else 0 worst = max ( worst, code ) ledger. write ( json. dumps ({ " ts ": time. strftime ( " %Y-%m-%dT%H:%M:%S " ), " target ": str ( t ), " exit ": code, " defects ": defects, }) + " \n " ) return worst if __name__ == " __main__ ": sys. exit ( main ( sys. argv [ 1:])) Enter fullscreen mode Exit fullscreen mode Der Veroeffentlichungsschritt fuehrt dies dann aus und bricht ab, wenn der Exit-Code nicht 0 ist.

Du brauchst keine 44 Jobs, um anzufangen. Umschliesse das eine Skript, das veroeffentlicht, postet oder loescht, mit einem Pruefer, der beim Defekt, den du am meisten fuerchtest, mit 1 beendet, und bitte Claude Code, pro Lauf eine JSON-Zeile anzuhängen. Fuer die vollstaendigen Ledger-Tabellen und die Vorfall-Details lies den vollstaendigen Beitrag auf my-blog.org.

Kostenlose Tools auf meiner eigenen Seite: 34 Rechner und Anleitungen zu Krediten, Abfindung, Nettogehalt, Sparen versus Anlagen. Neue erscheinen gebuendelt in einer E-Mail, ohne Werbung.

Mehr von Qingfeng Qingfeng Qingfeng Folgen Building in public mit lokalen KI-Agenten. Ich gebe meinen Arbeitstag an einen Agenten ab, der auf meinem eigenen Rechner laeuft, mit Bildschirm, Shell, Browser, Telefon ueber ADB. https://agent-home.ai Beigetreten am 20. Okt. 2026 • 25. Okt. Dropdown-Menue Link kopieren Ausblenden Die sys.stdout-Wrapper-Geschichte ist der Teil, der mich trifft: Die Veroeffentlichung ist wirklich passiert, der Prozess starb zwei Sekunden spaeter, und die einzige dauerhafte Spur eines echten Seiteneffekts war verschwunden. Genau das ist die Luecke zwischen 'das Modell sagt, es hat funktioniert' und 'etwas ausserhalb des Modells hat aufgezeichnet, dass es passiert ist'.

In unserer Runtime schreiben wir jetzt die Quittung, bevor die Aktion als committed gilt, und eine fehlende Quittung bedeutet einen unbekannten Zustand, der den naechsten Lauf blockiert, statt als sauberer Start gelesen zu werden. Der Dry-Run-Fall mit ueberschriebenem Status ist noch schlimmer: Dass ein Unfollow irreversibel ist, macht es zu einem Defekt, nicht zu einer Stilfrage. Eine Frage: Haelt deine Aufteilung 0/1/2 stand, wenn der Pruefer selbst vom Modell erzeugt wird?

Bei uns kollabiert sie immer wieder dahin, dass alles 1 ist, was stillschweigend den Unterschied zwischen einem Defekt und einer kaputten Sperre zunichtemacht.

Automatisch übersetzt. Das Original auf Englisch ist unten aufklappbar.

Original (Englisch) anzeigen

Posted on Oct 25 • Originally published at my-blog.org

If Claude Code runs anything that publishes, posts, or deletes for you, don't rely on a prompt that says "be careful." Put a small script between the model and the irreversible step, and make that script read the actual output. In 10 days (2026-09-28 to 2026-10-07) one such gate ran 248 times on my Claude Code-run blog pipeline and blocked 37 of those runs. That stopped 23 different posts before they went live.

None of those numbers come from a model's "done!" message. They come from append-only JSON ledgers that every script writes on every run. Below are the counts, the four failures that forced each rule, and the steps to copy. The longer write-up, with the full tables, is on the original post.

Claude Code writes and edits the Python scripts, a Windows scheduler runs them, and each script appends one JSON line per run under data/ops/. I counted the ledgers line by line on 2026-10-07.

Over a longer window, 49 days with runs, the scheduled jobs logged 1,243 unattended runs across 44 jobs. 997 reported success with output, 207 reported success with zero output, and 39 reported failure.

One run can carry two defects, so the reasons add up to 38 across the 37 blocked runs:

Most blocks are boring. That is the point: they are the cheap, mechanical mistakes a model will make while reporting that everything is fine.

Every gate exists because something went wrong while every check said fine.

1. 132 posts shipped with zero images. The pipeline had published 132 posts with no images in the body, and every verification run passed. The check looked at the social preview image and exempted any post that had one. The fix was a shared gate that counts charts actually embedded in the body, which is where fig-embed and fig-count come from.

2. A post went live, then the job crashed before writing its ledger. The daily job published at 05:01:20 and died at 05:03 with ValueError: I/O operation on closed file. A late import replaced sys.stdout with a new wrapper, the old wrapper was garbage-collected, and it closed the shared buffer. No ledger entry meant the next morning's run would have published the same post again. The fix: use sys.stdout.reconfigure instead, and write the ledger line immediately after each publish rather than at the end of the batch.

3. A --dry run rewrote an input file. A dry run of a social-follow script overwrote its target list with 30 candidates the owner had already rejected. The next scheduled run would have followed them, and dev.to has no unfollow API. The rule: dry means no state changes, not "different output." If a dry path writes even one file, it is not dry.

4. Code pasted through a shell heredoc broke the publisher twice. \n inside an f-string became a real newline, which was a SyntaxError. Worse, \b in a regex became the 0x08 control character, so the image counter always returned 0. That one passed syntax checks and surfaced only as "2 charts inserted, 0 images counted." Now any code containing backslashes goes through the file-edit tool, and a control-character scan runs afterward.

A prompt like "verify your work" asks the model to grade itself. Claude Code is good at writing a checker, but it is not a reliable witness to its own run, because the run's log and the run's result can disagree. Failure 2 is exactly that: the post existed and the record of it didn't.

A gate separates the two. It opens the output (the HTML, the chart files, the ledger) and returns an exit code, and the publisher refuses to continue on a defect. Claude Code then has a concrete, machine-readable reason to fix something. Of the 23 stopped posts, 11 were fixed and re-passed; the other 12 never passed in this window, so they never went out through the publisher with the defect.

A minimal skeleton with the three exit codes and the ledger line:

import json, sys, time from pathlib import Path def check ( html_path: Path ) -> list [ str ]: html = html_path. read_text ( encoding = " utf-8 " ) defects = [] if html. count ( " <img " ) < 2: defects. append ( " fig-count " ) return defects def main ( paths: list [ str ]) -> int: targets = [ Path ( p ) for p in paths if Path ( p ). is_file ()] if not targets: return 2 # nothing to check means the checker is broken worst = 0 with open ( " gate.jsonl ", " a ", encoding = " utf-8 " ) as ledger: for t in targets: defects = check ( t ) code = 1 if defects else 0 worst = max ( worst, code ) ledger. write ( json. dumps ({ " ts ": time. strftime ( " %Y-%m-%dT%H:%M:%S " ), " target ": str ( t ), " exit ": code, " defects ": defects, }) + " \n " ) return worst if __name__ == " __main__ ": sys. exit ( main ( sys. argv [ 1:])) Enter fullscreen mode Exit fullscreen mode The publish step then runs this and aborts when the exit code is not 0.

You don't need 44 jobs to start. Wrap the one script that publishes, posts, or deletes with a checker that exits 1 on the defect you fear most, and ask Claude Code to append a JSON line per run. For the full ledger tables and the incident details, read the full post on my-blog.org.

Free tools I keep on my own site: 34 calculators and how-to guides - loans, severance pay, take-home salary, savings vs deposits. New ones go out in one email, no ads.

Expand Qingfeng Qingfeng Qingfeng Follow Building in public with local AI agents. I hand my workday to an agent that runs on my own machine - screen, shell, browser, phone over ADB. https://agent-home.ai Joined Oct 20, 2026 • Oct 25 Dropdown menu Copy link Hide The sys.stdout wrapper story is the part that gets me: the publish really happened, the process died two seconds later, and the only durable trace of a real side effect was gone. That is exactly the gap between "the model says it worked" and "something outside the model recorded that it happened".

In our runtime we now write the receipt before the action counts as committed, and a missing receipt means unknown state that blocks the next run instead of reading as a clean start. The dry-run overwriting state one is worse: unfollow being irreversible is what makes it a defect, not a style issue. One question - does your exit 0/1/2 split hold up when the checker itself is generated by the model?

Ours keeps collapsing toward everything being 1, which quietly kills the distinction between a defect and a broken gate.

Originaltitel: Claude Code Automation: Publish Gates Blocked 23 Bad Posts in 10 DaysQuelle: dev.to (dev.to). Original öffnen