Your CLI Tool Is Lying to You (And It’s Costing You Hours)

You’ve been there. It’s 3 AM. Your CI/CD pipeline just failed for the fifth time. You’re staring at a wall of logs, and the only thing you know for sure is that your script said ‘success’… but everything is broken. Hours of debugging later, you find the culprit: a command that crashed silently, spat out error messages to stderr, but walked away with a perfect exit code of 0.

That’s not a bug. That’s a lie.

Your CLI tool is lying to you. And it’s lying to every system that depends on it. The worst part? You probably wrote the code that told it to lie.

A tool that crashes but returns a 0 is not a tool—it’s a traitor.

Let me explain why this is the single most underrated failure pattern in modern automation. Every time you chain a command into a pipeline, the operating system listens for one thing: the exit code. Not the pretty error message. Not the logging. The exit code. It’s the machine’s way of asking, ‘Did it work?’ And if you answer 0—success—when the truth is 1, 2, or 127, you’ve just injected a silent time bomb into your infrastructure.

I’ve seen it happen at every scale. A junior developer writes a Python script that catches all exceptions with a blanket except: pass and returns 0. A production deployment runs smoothly—until the day a critical step fails, the pipeline reports success, and the next team spends three days tracking down a data corruption that should never have happened. The exit code is the nervous system of automation. Cut it, and the body goes numb.

Here’s the uncomfortable truth: most developers spend 90% of their error-handling energy on crafting human-readable error messages. They write beautiful, poetic stderr output. Meanwhile, the exit code is an afterthought—often left to the default behavior of the runtime, which is frequently wrong. You’re writing a love letter to the human user and a death threat to the automated consumer.

This is the mind-body problem of CLI tools. The error message is the voice. The exit code is the soul. A voice can scream, but a soul that whispers ‘0’ while the body burns is a ghost. Your automation doesn’t read stderr. It reads exit codes. Period.

So what do you do? First, commit to a religion: every failure path in your tool must end with a non-zero exit code. Not just the ones you think of. The ones you don’t. Network timeouts, file not found, permission denied—each one deserves its own code. Second, test your exits. Write unit tests that assert the exit code, not just the output. Third, embrace the convention: 0 = success, anything else = failure. No exceptions. No ‘well, it partially worked.’

Your code’s exit code is its last word. Make it honest.

But the real problem isn’t technical—it’s cultural. We treat exit codes as an afterthought because we don’t respect the machine as a consumer of our output. We think automation is something we ‘add’ later, not something we design for from the start. That’s why the pipeline at 3 AM is broken. That’s why you’re reading this at your desk, not sleeping.

I want you to try something. Next time you write a script, any script, imagine a faceless CI runner waiting for your answer. It doesn’t care about your log messages. It doesn’t care about your clever error handling. It only cares about one number. If you lie to that number, you’re not just breaking your pipeline—you’re breaking trust. And once trust is broken, automation becomes manual. Manual becomes slow. Slow becomes dead.

So stop lying. Your exit code is your promise. Keep it.

FAQ

Q: Isn't it enough to just log errors? Why does the exit code matter so much?

A: Logs are for humans. Pipelines read exit codes. A log can say 'error' but if the exit code is 0, the pipeline assumes success and moves on. That's how silent failures slip into production. The machine doesn't read stderr—it reads the exit code first.

Q: What's the practical fix for a tool that already has sloppy exit codes?

A: Start by auditing every failure path. Add explicit `exit(1)` or `sys.exit(1)` in your code. Wrap your entire script in a try/finally that catches exceptions and returns non-zero. Test with `$?` in shell scripts. Then enforce it in CI: if a tool misbehaves, fail the build.

Q: Isn't this overblown? Most tools work fine without obsessing over exit codes.

A: Most tools 'work fine' until they break a pipeline at 3 AM. The difference between a hobby script and a production tool is whether it degrades gracefully when automated. Exit codes are the cheapest insurance against silent failures. Ignoring them is technical debt with compound interest.

📎 Source: View Source