{"slug": "claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days", "title": "Claude Code Automation: Publish Gates Blocked 23 Bad Posts in 10 Days", "summary": "A developer built a script-based publish gate between Claude Code and irreversible publishing steps, logging every run to append-only JSON ledgers. Over 10 days the gate ran 248 times, blocked 37 runs, and stopped 23 distinct posts from going live, with keyword-demand staleness (21) and unembedded charts (6) the most common defects. The gates were prompted by four incidents, including 132 posts shipping without images and a dry run that overwrote a social-follow target list.", "body_md": "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.\n\nNone 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](https://my-blog.org/tangents/post/claude-code-automation-publish-gates?utm_source=devto&utm_medium=referral&utm_campaign=claude-code-automation-publish-gates).\n\nClaude 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.\n\n| Measure | Value | \n|---|---|\n| Gate runs (247 Tistory, 1 dev.to) | 248 | \n| Passed (exit 0) | 206 | \n| Blocked (exit 1, defect found) | 37 | \n| Checker itself broke (exit 2) | 5 | \n| Distinct posts checked | 57 | \n| Posts blocked at least once | 23 | \n| Blocked, later fixed and passed | 11 | \n\nOver 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.\n\nOne run can carry two defects, so the reasons add up to 38 across the 37 blocked runs:\n\n`kw-demand` (21): the keyword-demand research file was missing or older than 45 days.`fig-embed` (6): charts were rendered but never inserted into the post body.`rel-date` (4): relative dates like \"this weekend\" that turn false if publishing slips a day.`kw-grade` (3): the target keyword was below the 200 monthly-search bar.`fig-numbers` (3): a number in a chart appeared nowhere in the post text.`fig-count` (1): fewer than 2 charts in the post.\nMost blocks are boring. That is the point: they are the cheap, mechanical mistakes a model will make while reporting that everything is fine.\n\nEvery gate exists because something went wrong while every check said fine.\n\n**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.\n\n**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.\n\n**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.\n\n**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.\n\nA 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.\n\nA 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.\n\n`CLAUDE.md`, a report can cite only the checker's name and exit code, what a human actually opened, and what the gate cannot see.\nA minimal skeleton with the three exit codes and the ledger line:\n\n``` php\nimport json, sys, time\nfrom pathlib import Path\n\ndef check(html_path: Path) -> list[str]:\n    html = html_path.read_text(encoding=\"utf-8\")\n    defects = []\n    if html.count(\"<img\") < 2:\n        defects.append(\"fig-count\")\n    return defects\n\ndef main(paths: list[str]) -> int:\n    targets = [Path(p) for p in paths if Path(p).is_file()]\n    if not targets:\n        return 2  # nothing to check means the checker is broken\n    worst = 0\n    with open(\"gate.jsonl\", \"a\", encoding=\"utf-8\") as ledger:\n        for t in targets:\n            defects = check(t)\n            code = 1 if defects else 0\n            worst = max(worst, code)\n            ledger.write(json.dumps({\n                \"ts\": time.strftime(\"%Y-%m-%dT%H:%M:%S\"),\n                \"target\": str(t), \"exit\": code, \"defects\": defects,\n            }) + \"\\n\")\n    return worst\n\nif __name__ == \"__main__\":\n    sys.exit(main(sys.argv[1:]))\n```\n\nThe publish step then runs this and aborts when the exit code is not 0.\n\n`file://`. When every target fails the same way, suspect the gate first.\nYou 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](https://my-blog.org/tangents/post/claude-code-automation-publish-gates?utm_source=devto&utm_medium=referral&utm_campaign=claude-code-automation-publish-gates).\n\n*Free tools I keep on my own site: [34 calculators and how-to guides](https://my-blog.org/tools?utm_source=devto&utm_medium=referral&utm_campaign=claude-code-automation-publish-gates) - loans, severance pay, take-home salary, savings vs deposits. New ones go out in one email, no ads.*", "url": "https://wpnews.pro/news/claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days", "canonical_source": "https://dev.to/sungwoo_lee_e0f26be4a29fd/claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days-5fh3", "published_at": "2026-10-25 05:00:58+00:00", "updated_at": "2026-10-25 05:14:33.447817+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "mlops", "ai-safety"], "entities": ["Claude Code", "Tistory", "dev.to"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days", "markdown": "https://wpnews.pro/news/claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days.md", "text": "https://wpnews.pro/news/claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days.txt", "jsonld": "https://wpnews.pro/news/claude-code-automation-publish-gates-blocked-23-bad-posts-in-10-days.jsonld"}}