Logger-TXT started life as a shell script over a decade ago. It does one small thing: append a timestamped line to a text file so I can remember what I did on that Tuesday two weeks ago. I rewrote it in Go recently, and the surface reason was capabilities. Date ranges, type and project filters, reverse scanning so a decade-deep log file stays fast.
That’s not really why I did it, though. The rewrite was an excuse to practise designing an AI-native command-line tool: one a coding agent can pick up and drive correctly, with no help from me, on the first try.
Every loop in the process that follows (the constraint, the testing, the rewrites of my own help text) was for the agent. Humans get a better tool out of it, and I’ll get to why that happens, but that’s the dividend rather than the motivation.
What “AI-Native” Means Here
Worth defining, since the phrase gets used for something else entirely.
An AI-native CLI isn’t a CLI with AI inside it. There’s no model in Logger-TXT,
no natural-language parsing, no logger-txt ask "what did I do Tuesday". It’s
a tool that treats a coding agent as a first-class consumer of its interface,
alongside the human at the terminal.
A human explores. They run the command, squint at the output, try a flag, read the error, and converge. They carry context between attempts, and they can go ask someone or read the source when they’re stuck.
An agent commits. It reads the interface once, picks the command that best matches its intent, and runs it. An error it will read and route around, so a command that fails outright is no problem. A plausible-looking result it simply accepts, because it has no independent sense that the number should have been 40 instead of 10.
So an agent is functionally a user with no prior context, no accumulated intuition about your tool, and no way to ask a clarifying question, who nonetheless has to get it right immediately.
The Constraint: --help Has to Be Enough
Here’s the rule I gave myself: an agent handed nothing but this binary should reach the right command on the first try.
No instruction file to load. No skill to install. No MCP server wrapping the CLI. No prose in a repo telling the agent which flag to prefer. Everything it needs to use the tool correctly ships inside the tool.
Why Not a CLAUDE.md, a Skill, or an MCP Server
Those are the normal answers to “make this tool legible to an agent,” and each one puts the knowledge in the wrong place:
- A
CLAUDE.mdsection lives in your repo, not mine. It drifts out of date the moment I ship a flag, and it only helps agents that happen to read that file, in that repo. - An MCP server is a second artifact to build, version, release, and keep in sync with the binary. Now there are two things that can disagree about how the tool behaves. It does buy something help text can’t, in fairness: a typed schema constrains arguments structurally, where help text only asks nicely. I went the other way regardless, for the reason below.
- A skill has to be installed before it can help, so the tool doesn’t work until someone has already done setup on the agent’s side.
Help text ships with the binary, versions with the binary, and is already the
first place both consumers look. It’s the only documentation surface that can’t
drift from the thing it documents. That matters most in the case the tool
actually gets used in: someone runs brew install grantlucas/tap/logger-txt and
there is no repo, no README, and no source to fall back on. There’s a binary and
its help output, and nothing else.
Though the better reason I picked it is that it’s a forcing function. If
--help is the only channel I’m allowed to use, every ambiguity in the tool has
to get sorted out in the help text or in the design itself. I can’t paper over a
confusing interface with a paragraph somewhere else.
The Design Loop
The constraint makes the workflow concrete, and it ends up looking like usability testing with an agent as the test subject:
- Hand an agent the binary and nothing but
--help - Watch for the spot where it picks a plausible-but-wrong command
- Fix the help text, or the design, until a cold read leads somewhere better
Step 2 is the whole game, because plausible-but-wrong is the failure mode agents produce and humans mostly don’t. An agent that picks a command which errors out is fine, since it reads the error, adjusts, and gets there. An agent that picks a command which succeeds and returns believable-but-wrong results has no signal that anything went wrong, and neither do I until I notice the numbers look off weeks later.
The Trap I Built Myself
The best example came from a change that looked purely additive.
I added -t (type) and -p (project) filters to both show and search. Nice
and consistent: same flags, both commands. Except that quietly created a trap.
These now look interchangeable, but search matches the term anywhere in the
full log line, including the message body, so an entry like
31/01/26 13:30 -0600 - PERSONAL - Followed up on WORK email shows up in the
first command and not the second.
Asked to find my work entries, the agent reasonably picked search and got
plausible results back. Nothing in the help text would have told it otherwise. A
human who wrote the tool would never make this mistake, which is exactly why I
couldn’t see it.
The fix went into the help string, not into a document telling the agent what to do:
It’s deliberately blunt, and it names the wrong choice explicitly, because a disambiguation that only describes the correct option is useless to a reader who already believes they’ve found it.
What Designing for the Agent Produced
A handful of patterns fell out of running that loop. Each one is here because of a specific way an agent breaks without it.
Help Text That Teaches Rather Than Lists
Without this, an agent works out the data format by running commands and inferring from whatever comes back, which burns turns and produces a wrong inference when the sample isn’t representative.
So the root --help embeds the log entry format and the full file-resolution
order. One read establishes where the data lives and what it looks like:
|
|
Subcommand examples show their expected output inline:
That # => line costs nothing and removes a round trip. It also makes the
uppercasing visible, since -t dev comes back as DEV and -p acme as
(ACME), instead of leaving it as a surprise to be discovered later.
Documented Defaults, Not Implied Ones
An undocumented default gets guessed at, and a guess that happens to return rows is indistinguishable from a correct answer. So the ones that could bite are spelled out:
- A date-only
--startbegins at00:00 - A date-only
--endends at23:59 - An active date range overrides the default count of 10
That last one especially. show --start 01/03/26 --end 14/03/26 returning
everything in the range rather than the last 10 is the behaviour I want, and it
flatly contradicts the count default documented a few lines above it in the same
help output. That contradiction is exactly why it has to be stated: left
implicit, an agent summarising two weeks of entries would confidently report on
ten of them.
No Quoting Ceremony
Quoting is one of the most reliable ways to make a generated command fail: nested quotes, shell escaping, an apostrophe in the message. So there’s nothing to quote.
|
|
Arguments are joined into the message, which removes the whole class of failure and is pretty nice to type besides.
A Non-Interactive Path for Everything
A blocking prompt is a hung agent. It sits there waiting on stdin that will
never come, and the turn dies on a timeout with no useful error. So delete
asks for confirmation and delete -y skips it. This is the oldest advice on the
list and the easiest to forget when you write the interactive path first.
Composable, Greppable Output
One entry per line on stdout, plain text on disk:
The property I care about here is the escape hatch. When my CLI is the wrong
tool for the job, the agent can ignore it entirely and read the log file
directly. That was always the point of the plain-text format, though it pays off
differently with a consumer that’s perfectly happy to grep a file when that’s
the shorter path.
Loud Failure on Ambiguity
Passing --start without --end is an error with a clear message, not a
guessed range. The temptation is to be helpful: assume “until now,” assume “for
one day.” But a guessed range produces results that look correct, which puts us
straight back into plausible-but-wrong.
Erroring out converts a silent wrong answer into an immediate, self-correcting retry. For an agent, a good error message beats a permissive default, since the error is information and the default is a coin flip.
Humans Get It for Free
Here’s the part I didn’t expect: I never once had to trade one consumer against the other. Every change on that list made the tool better for me at the terminal too.
I think the reason is structural, and it’s the most useful thing I’ve taken from the exercise. An agent is the worst-case human. No context, no accumulated intuition about the tool, no memory of the last time it hit this flag, no way to ask. Every affordance a human silently supplies from experience, the agent needs supplied by the interface.
Which means designing for the agent isn’t a parallel track to designing for humans. It’s designing for the strictest possible human, the one on their first day, at 2am, with no colleague to ask. Satisfy that reader and the experienced one is covered by construction.
So there’s no “agent version” of the interface to maintain, and I’d push back on
the idea that this is extra work. It’s one interface, held to a standard that
was always the right one, now with something that reliably tells me when I’ve
fallen short of it. I knew search WORK and show -t WORK differed, since I wrote
both. My tool’s ambiguities were invisible to me precisely because I held all
the context that resolved them. The agent holds none of it, and it doesn’t have
the good grace to quietly work around my gaps. It takes exactly the wrong turn
my help text permits, immediately and repeatably, where I can watch it happen.
Why This Is Worth Practising Now
Logger-TXT is a personal tool. The stakes are that I mis-tally my hours one week, which makes it a pretty good place to practise something that gets expensive elsewhere.
The ratio is shifting, though. More and more of the tools I build, at work and otherwise, are going to spend most of their invocations being driven by something that can’t ask a clarifying question in Slack, can’t absorb the tribal knowledge in someone’s head, and can’t infer intent from a hallway conversation. The interface is all it gets, and if the interface is ambiguous then the tool is wrong in a way that looks right, at whatever scale the agent is operating at.
That’s a design discipline, and it’s worth practising deliberately before it matters. Which flags are confusable. Which defaults are guessable. Which prompts hang. Which errors teach. None of it is hard, it’s just invisible until you put something in front of the tool that can’t paper over your omissions with intuition.
If you want to poke at the result, it’s on
GitHub, or
brew install grantlucas/tap/logger-txt and run --help. If you try it with an
agent and it takes a wrong turn I haven’t caught yet, let me know. Those are the
gaps I can’t find on my own, since I already know what my own tool means.