Build1 publisher3 min readPublished
Exit code zero, output zero bytes: why a dated marker file is the only honest success signal
A developer's nightly content pipeline kept reporting success while shipping 186-byte error messages. The fix was to stop reading logs and start checking whether a dated done-marker exists.
The Engineer · Build desk
Drafted by a language model from the sources cited here and checked against its claim ledger before publication. How we use AISend a correction
What happened
- The author's content generation scripts always reported success, while the articles they produced were sometimes 186 bytes of an error message.
- The stated design principle is to prove completion by the existence of a file: not what the script wrote to the log, but only whether ~/.claude/logs/.article-daily-done-20260710 exists, is treated as truth. This is described as the done-marker pattern.
- launchd fires the script every morning, but it starts before the network is up and exits silently.
- The API times out with no response, yet a log file still exists.
- When Claude's token budget runs dry, the error message flows straight into the output file, leaving the body at zero bytes.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
A developer writing on dev.to published the shell scaffolding behind a five-lane content generation pipeline, along with the failure that prompted it: the scripts always reported success, and the articles were sometimes 186 bytes of an error message [1]. The useful part is not the watchdog script. It is the concession that an exit code carried no information, and that the only thing now treated as truth is whether a dated file exists [2].
The author lists three ways the job died while looking alive. launchd fires the script every morning, but it starts before the network is up and exits silently [3]. The API times out with no response, and a log file still exists anyway [4]. The Claude token budget runs dry, the error text flows straight into the output file, and the body ends up at zero bytes [5]. All of them leave behind nothing but the fact that the script ran [6]. That is the whole problem in one line: the evidence of running survives, the evidence of working does not.
So completion gets its own artefact. Only the existence of ~/.claude/logs/.article-daily-done-20260710 counts, not anything the script wrote to the log [2]. content-watchdog.sh is invoked several times a day, checks the done-marker for every lane, and restarts only the lanes whose marker is missing [7]. It takes a mkdir-based contention lock at ~/.claude/locks/content-watchdog.lockd/ so overlapping slots do not double-fire [8], and re-runs an incomplete lane under run_capped 1800 [9], which is a 30-minute ceiling [10]. When every lane is done it sends one Discord heartbeat and writes a dated heartbeat file [11].
The per-lane checks are where the pattern gets honest about its own edges. article and series each use a single hidden dated file [12]. note and maker glob for $TODAY-*.done, a shape that tolerates several files per day [13]. ameba has no marker at all, so the watchdog inspects the artefacts directly, grepping .md files under ~/Desktop/Article/ameba for created: metadata matching today, then falling back to file mtime [14]. The author calls that a fallback and a realistic compromise for wiring an existing script that lives outside the design into the watchdog [15]. Five lanes, three different definitions of done [16].
The placement of the touch matters more than the marker itself. In article-daily-stock.sh the marker path is fixed at the top, and the file is created after generation, validation, stock placement and secret scanning, immediately before the git push at line 453 [17]. The in-code comment explains why: the push is best-effort, so judging done by push success would leave the marker unset on a git failure and produce duplicate generation in the next slot [18]. That is the transferable rule. The marker goes at the last point where the thing you actually care about is finished, and not one step later.
What to watch is the gap the pattern does not close. A done-marker proves the script reached line 453; it does not prove the output is any good, and a 186-byte error article would sail past a marker placed before content validation [1][17]. Here the checks sit upstream of the touch, which is what gives the file its meaning. The ameba lane shows the weaker version: when you did not write the lane, you are reduced to reading mtimes, which tells you a file changed today, not that the run succeeded [14][15]. The author also credits this verification-cost reduction with letting him run 10 iOS apps in parallel at 1.2M yen a month in revenue [19]; that part is single-sourced and unverifiable, and the marker logic stands without it. The framing that does hold: automation dies for operational reasons more often than technical ones, on the morning the Wi-Fi is not connected, at midnight with the battery at 3 percent, at the end of the month when the budget runs out [20].