/writing/cron-stdout-stderr

The Silent Killer of Cron Jobs: stdout vs stderr

I set up a small scheduled job in my home lab to keep an eye on outdated container images and post a summary somewhere I’d actually see it. The scanning tool runs, produces a report, and a second small container reads that report and does something useful with it. Two containers, one shared scratch volume between them, one scheduled run a week.

The scan step reported success every single time. Exit code 0, clean logs, nothing suspicious. The second container — the one supposed to read the report and act on it — crashed every single time with a file-not-found error. The report file the scan step was supposedly writing simply wasn’t there.

I checked the tool’s documentation. It has a flag that’s very clearly named for exactly this purpose — an “output file” option that, per its own docs, writes the report to the path you give it. I was passing that flag. The path was correct. The directory existed and was writable. And the file never showed up.

The flag that does nothing

The actual answer, once I went looking at what the tool does rather than what its docs say it does: that flag is a no-op for this output format. The tool prints its JSON report straight to stdout regardless of the flag, and only sends its own progress/log noise to stderr. The “exited 0, no errors” signal from the first container was completely true and completely useless — the process itself was healthy the entire time. It just never wrote the file it was told to write, and never complained about not writing it either.

The fix, once I understood that, was almost insultingly simple: don’t trust the flag, capture the stream. Redirect the tool’s actual stdout to the expected file path myself, in the container’s own entrypoint, instead of relying on the tool to do it internally.

Why “it exited cleanly” is not the same as “it worked”

This is the part worth generalizing, because it’s not really a story about one CLI tool’s undocumented behavior. It’s about a much more common trap: a process’s own reported success (exit code, absence of error logs) tells you the process didn’t crash. It tells you nothing about whether the process did the specific thing you actually needed. Those are two completely different claims, and it’s very easy to write monitoring, alerting, or a “did this job succeed” check that only verifies the first one.

The related, sharper version of this trap: if you ever redirect a process’s stderr into the same file you intend to parse downstream (a common instinct when you just want “all the output” in one place), you’ve created a way for warnings and progress noise to silently corrupt a data file with no error at the point of the mistake — the corruption only surfaces later, when whatever parses that file chokes on a stray log line that isn’t valid JSON or CSV or whatever you expected. The fix there is the mirror image of the fix above: keep stdout and stderr in genuinely separate places when one of them is meant to be machine-read, and never assume a tool’s documented flag actually routes things the way its name implies.

The generalizable rule

Verify what a tool actually writes and where, by testing it directly, before trusting its own documentation about an output flag — especially one you’re about to build automation on top of. And treat “the process exited without error” as the floor for a health check, never the whole check. If a job’s real job is “produce this specific file with this specific content,” the only thing that actually confirms success is checking for that file and that content — not the process’s own opinion of how it went.