Building a CLI Around Your Agent Harness
A useful agent nobody could run became one everyone uses — the fix was a real agent harness cli with stdin, clean stdout, and meaningful exit codes.
def main():
prompt = os.environ.get("PROMPT", "")
result = agent.run(prompt)
print(result.text)
if __name__ == "__main__":
main()Picture this: you're a developer on a team that built a genuinely useful agent, but every time someone wants to run it, they open a Python file, edit some variables at the top, and run the script — copying output out of the terminal by hand. It works, technically. It's also why nobody outside the original two engineers ever uses it. The thing standing between a useful agent and an adopted one is often just a good agent harness cli — a proper command-line interface that makes the agent feel like a real tool. This is the story of a team that built one and what they learned about the details that matter.
A CLI seems like a thin wrapper, and a bad one is. A good one is the difference between "run this script the way I showed you" and "it's on everyone's path and it composes with their other tools." The gap between those is entirely in the details this case study is about.
The Problem This Team Faced
Their agent was strong and its ergonomics were terrible. Running it meant editing source, there was no way to pass input except by changing code, output was an undifferentiated blob printed at the end, and there was no way to tell success from failure except by reading the text. It couldn't be scripted, couldn't be piped, couldn't be scheduled — it could only be babysat by someone who knew the code.
The result was predictable: adoption stalled at the people who wrote it. Every potential user bounced off the friction, and the agent's real capability went unused because the interface to it was a source file.
⚠️ Common mistake: Treating the CLI as an afterthought wrapper around a
main()The Wrong Approach
Their first pass at "adding a CLI" was to read a couple of environment variables and print the result. It removed the source-editing but little else.
[object Object], ,[object Object],():
prompt = os.environ.get(,[object Object],, ,[object Object],)
result = agent.run(prompt)
,[object Object],(result.text)
,[object Object], __name__ == ,[object Object],:
main()What this does: Pulls the prompt from an environment variable and prints the final text. It's marginally better than editing source, but it has no real argument parsing, no streaming (the user still stares at nothing until the end), no way to signal success or failure to a calling script, and no help text. It's a script wearing a CLI costume.
The deeper issue was that it composed with nothing. A real command-line tool participates in the shell ecosystem — it reads from stdin, writes to stdout, sets an exit code, and behaves differently when piped than when run interactively. This version did none of that, so it couldn't be dropped into a pipeline or a cron job or another script.
The Correct Approach
The rebuilt agent harness cli was designed as a first-class command-line citizen: real argument parsing, streaming output to a terminal, plain output when piped, and meaningful exit codes.
[object Object], argparse, sys
,[object Object], ,[object Object],():
p = argparse.ArgumentParser(description=,[object Object],)
p.add_argument(,[object Object],, nargs=,[object Object],, ,[object Object],=,[object Object],)
p.add_argument(,[object Object],, action=,[object Object],, ,[object Object],=,[object Object],)
p.add_argument(,[object Object],, action=,[object Object],, ,[object Object],=,[object Object],)
args = p.parse_args()
prompt = args.prompt ,[object Object], sys.stdin.read() ,[object Object],
,[object Object],:
run = agent.run(prompt, dry_run=args.dry_run,
stream=sys.stdout.isatty()) ,[object Object],
emit(run, as_json=args.json)
sys.exit(,[object Object], ,[object Object], run.succeeded ,[object Object], ,[object Object],) ,[object Object],
,[object Object], HarnessError ,[object Object], e:
,[object Object],(,[object Object],, file=sys.stderr)
sys.exit(exit_code_for(e))What this does: Parses real arguments including a positional prompt (or reads it from stdin), supports
--dry-run--jsongrepcurlResults and What Changed
Adoption moved. Once the agent read stdin, wrote clean stdout, and set exit codes, people started composing it into their own workflows — piping data in, capturing output, wiring it into scripts and scheduled jobs. The capability that had been trapped behind a source file became something teammates reached for daily, because it now fit the tools they already used.
The exit codes turned out to matter more than expected. Because a failed run exited non-zero and different failure classes had different codes, people could script around the agent reliably — retry on a transient failure code, alert on a hard one, branch on success. An agent that only ever printed text couldn't support any of that; one that spoke the shell's language of exit codes slotted into automation naturally. The team's takeaway was that the boring plumbing — stdin, stdout, stderr, exit codes — did more for adoption than any feature they could have added to the agent itself.
⚡ Pro tip: Detect whether you're attached to a terminal with
sys.stdout.isatty()How to Apply This to Your Situation
Start with the shell contract, not the flags. A good agent harness cli reads input from arguments or stdin, writes results to stdout and errors to stderr, and sets an exit code that reflects the outcome. Get those three right and the tool composes with everything else in the shell; get them wrong and no amount of flags will make it feel native.
Then add the flags that map to your harness features you already built:
--dry-run--json--replay--verboseFinally, invest in help text and errors.
--helpThere's a subtle sequencing lesson here too: the team got adoption not by making the agent smarter but by making it reachable. The underlying capability hadn't changed between the version nobody used and the version everyone used — only the interface had. It's worth remembering whenever adoption of a good agent stalls, because the instinct is usually to improve the model or the prompt when the actual barrier is that people can't easily run the thing.
⚡ Pro tip: Ship a single-line install and a single-line first run in your README, and test that exact pair on a fresh machine. The distance between "I heard about this agent" and "I ran it once" is where most potential users are lost, and every extra setup step in that gap sheds a fraction of them. A tool someone can try in two commands gets tried; one that needs a page of setup gets bookmarked and forgotten.
Beyond the Basics: Config, Interactivity, and Errors
Once the shell contract is solid, three refinements turn a functional agent harness cli into one people genuinely enjoy using.
Configuration layering matters more than it sounds. Users want to set defaults once and override them per invocation, so support a config file, environment variables, and flags — with flags winning over env vars, which win over the config file. This lets someone set their common options in a file and override just one for a particular run, instead of retyping everything every time.
[object Object], ,[object Object],(,[object Object],):
,[object Object], flag_val ,[object Object], ,[object Object], ,[object Object],: ,[object Object],
,[object Object], flag_val
,[object Object], env_key ,[object Object], os.environ: ,[object Object],
,[object Object], os.environ[env_key]
,[object Object], config.get(env_key, default) ,[object Object],What this does: Resolves each setting by precedence — an explicit flag beats an environment variable beats the config file beats the built-in default. Users get the convenience of persistent defaults and the flexibility of per-run overrides from the same setting, which is the behavior every well-loved CLI tool shares. Predictable precedence is what makes configuration feel obvious instead of surprising.
An interactive mode is worth adding for agents people converse with. When run with no piped input and attached to a terminal, drop into a REPL-style loop where the user can send follow-up messages and watch the agent respond, rather than exiting after one turn. The same binary then serves both one-shot scripting and interactive exploration.
⚡ Pro tip: Write errors to stderr and results to stdout, always, and never mix them. A user piping your agent's output into another program wants only the result on stdout; if error messages and progress chatter leak into stdout, they corrupt the piped data. Keeping the two streams clean is what lets
your-agent "..." | jqNext Steps
A CLI that sets meaningful exit codes depends on the harness having a meaningful classification of failures underneath it — you can't map errors to exit codes if all errors look the same. That error taxonomy is the natural next piece to build, and it makes the CLI's exit codes trustworthy — a distinct code per failure class is only possible when the harness already knows how to tell those classes apart.
Keep your CLI's argument conventions and exit-code map versioned alongside your prompts in a library like PromptABCD, so every agent you ship presents the same familiar interface — and someone who's used one of your agents already knows how to drive the next.
Continue Reading
Save the prompts from this post
PromptABCD is a free prompt manager. Paste, organize, and reuse your best AI prompts — no more hunting through chat history.
