跳到正文
DEV Community· Sungwoo Lee·· 4 小時前AI 評分67

Claude Code 自動化發布閘門 10 天內阻止 23 篇有缺陷文章發布

Claude Code Automation: Publish Gates Blocked 23 Bad Posts in 10 Days

AI 導讀

作者在 Claude Code 執行的網誌發布流程中加入檢查閘門,10 天內運行 248 次,因缺陷阻止發布 37 次,涉及 23 篇文章。閘門檢查實際輸出,以三種退出碼區分通過、發現缺陷和檢查器故障,並逐次寫入 JSON 日誌;其中 11 篇文章修復後通過。

正文

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.

What I measured in 10 days

The publish gate ran 248 times, blocked 37 runs, and stopped 23 posts, of which 11 were fixed and passed

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.

Measure Value
Gate runs (247 Tistory, 1 dev.to) 248
Passed (exit 0) 206
Blocked (exit 1, defect found) 37
Checker itself broke (exit 2) 5
Distinct posts checked 57
Posts blocked at least once 23
Blocked, later fixed and passed 11

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.

Why the gate blocked posts

Blocked runs by defect: kw-demand 21, fig-embed 6, rel-date 4, kw-grade 3, fig-numbers 3, fig-count 1

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

  • 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.

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

The 4 failures that created the rules

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.

Why gates work where prompts don't

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.

Copy this setup

  1. Give every checker three exit codes. 0 is clean, 1 is a defect, 2 means the checker itself is broken (no targets found, file unreadable). Finding zero things to check is exit 2, not a pass.
  2. Append one JSON line per run with timestamp, target, exit code, and defect names. If the line count doesn't grow, the check didn't run.
  3. Make the publish step call the gate and stop on exit 1. Put it inside the publisher, not in a checklist someone has to remember.
  4. Write the ledger the moment the irreversible action happens, not at the end of the batch.
  5. Restrict what a completion report may say. In 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.
  6. Log every failure as symptom, cause, fix, then turn the fix into a check. My 169 incident entries are where most gate rules came from.

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:]))

The publish step then runs this and aborts when the exit code is not 0.

Limits worth knowing

  • A gate with bad measurement lies with confidence. The first live run of a page checker flagged 320 defects across 80 pages. Every one was an analytics beacon failing because the checker opened pages from file://. When every target fails the same way, suspect the gate first.
  • Gates see surface patterns, not quality. A title-hook detector rated a good title as having zero hooks because its word list was incomplete, and it blocked a publish. Expect false positives and keep an explicit override.
  • "Success with zero output" is ambiguous. 207 runs reported success and produced nothing. Most were legitimate, such as a comment watcher with no new comments, so you must define per job what empty means.
  • This is one operator's setup, a single blog platform over a few weeks. It shows the mechanism works here; it is not a benchmark.

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.

來源:DEV Community · dev.to