Command structure, error messages, and distribution built on established conventions instead of gut feeling
A CLI tool that feels intuitive almost always follows the same conventions established over decades that experienced users unconsciously expect: consistent flag names, predictable exit codes, helpful error messages instead of cryptic stack traces. Claude works well for systematically thinking through exactly these conventions before the first line of code exists, instead of bolting them on after user complaints arrive. This article shows how to deliberately think through command structure, error handling, and distribution strategy for a custom CLI tool with Claude.
Table of Contents
- 1. Why CLI design is more than parsing arguments
- 2. Thinking through command structure by POSIX conventions
- 3. Designing subcommand trees after the Cobra and Click pattern
- 4. Naming flags and their short forms consistently
- 5. Generating helpful error messages and --help text
- 6. Designing exit codes and machine readable output consistently
- 7. Weighing distribution: npm, Homebrew, or binary releases
- 8. Generating shell completion for bash, zsh, and fish
- 9. Distribution channels compared
- 10. Summary
- 11. FAQ
1. Why CLI design is more than parsing arguments
The technical part of a CLI tool, reading in flags and arguments, is usually done in a few lines with modern libraries like Cobra for Go or Click for Python. The real difficulty lies elsewhere: in designing a mental model that feels immediately familiar to experienced terminal users, because it follows the same patterns as git, docker, or kubectl, instead of inventing its own seemingly obvious shortcuts that end up needing to be looked up anyway.
Claude works well as a sparring partner during this design phase, because it knows the conventions of numerous established CLI tools and can flag it deliberately when a planned command name or flag deviates from them. It's important to ask these questions before the structure gets used publicly, because a published CLI interface can only be changed later at considerable cost and by breaking existing scripts.
2. Thinking through command structure by POSIX conventions
POSIX conventions establish, among other things, that short flags start with a single dash and a letter while long, descriptive flags use a double dash, say -v as a short form for --verbose. Many modern tools extend this scheme with subcommands following the pattern tool verb noun, say docker container ls, which scales well as a tool grows to cover more functional areas over time.
Claude can be asked deliberately to check a planned feature set against these conventions and point out inconsistencies, say when a flag is called --output in one subcommand and --format in another, even though both refer to the same concept. Such small inconsistencies are easy to miss in your own design because you know your own vocabulary, but they're one of the most common sources of frustration for new users encountering an unfamiliar tool for the first time.
# Have a planned command structure checked against established conventions
claude "I'm planning a CLI tool 'shipctl' for deployment automation with \
subcommands: deploy, rollback, status, logs. Check the planned flags \
against kubectl and docker conventions. Show all inconsistencies in \
flag names between subcommands.
3. Designing subcommand trees after the Cobra and Click pattern
Both Cobra in Go and Click in Python rely on a tree of nested commands, where every node can have its own flags, its own help, and its own subcommands, while shared flags like --verbose or --config get defined on the root and inherited by all subcommands. This pattern avoids duplicating the same flag definition in multiple places in the code, while also ensuring consistent behavior across the whole tool.
Claude can design a matching skeleton for such a command tree from an informal description of the desired functionality, including a sensible grouping of related subcommands and a decision about which flags belong at the root level versus only on a specific subcommand. Doing this design work up front saves considerable refactoring effort later, when a tool initially conceived as flat grows to cover more functional areas.
package cmd
import (
"github.com/spf13/cobra"
)
var rootCmd = &cobra.Command{
Use: "shipctl",
Short: "Deployment automation for internal services",
}
var deployCmd = &cobra.Command{
Use: "deploy [service]",
Short: "Deploys a service to the given environment",
Args: cobra.ExactArgs(1),
RunE: runDeploy,
}
func init() {
rootCmd.PersistentFlags().BoolP("verbose", "v", false, "Verbose output")
rootCmd.PersistentFlags().String("config", "", "Path to config file")
deployCmd.Flags().String("env", "staging", "Target environment")
rootCmd.AddCommand(deployCmd)
}
4. Naming flags and their short forms consistently
Short forms of flags are valuable for daily use, but quickly become a trap when the same shorthand means different things in different subcommands, say -f for --force in one place and for --file elsewhere. Users who use a tool over months build muscle memory for particular shorthands, and an inconsistent shorthand then leads to mistakes that only become apparent after execution, sometimes with destructive consequences.
Claude works well for building a complete list of every planned flag across all subcommands and systematically searching it for shorthands assigned twice with different meanings. It's also worth asking which flags trigger dangerous, potentially destructive operations and should therefore deliberately not get a short, easily mistyped form, say a forced deletion that always has to be spelled out explicitly.
5. Generating helpful error messages and --help text
An error message that just states something failed, without explaining why and what the user should do next, ends up costing considerably more support effort overall than a slightly more elaborate message written up front. Claude can be asked deliberately to turn a technical error cause, say a failed authentication against an API, into a message that names the reason and suggests a concrete next step, instead of just printing the raw error code.
--help text also benefits significantly from deliberate rework: instead of just listing flags, it should include one or two realistic usage examples showing how the most common tasks actually get solved. Claude can derive matching example invocations straight from the flag definitions and specifically highlight combinations that, according to existing support requests or GitHub issues, most often cause confusion.
# Before: cryptic error message with no actionable guidance
Error: 401
# After, written by Claude:
Error: Authentication failed (HTTP 401).
Your API token has either expired or is invalid.
Run 'shipctl auth login' to sign in again.
Details: shipctl auth status
6. Designing exit codes and machine readable output consistently
A CLI tool is rarely used exclusively interactively, it's frequently embedded in scripts and CI pipelines too, where the exit code decides success or failure. A consistent convention, where 0 always means success but different nonzero codes distinguish different error classes, say configuration errors versus network errors, lets calling scripts react deliberately to different kinds of failure instead of treating every error the same way by default.
It's also worth adding a consistent --json flag that delivers the same information as the human readable output in structured form, so the tool can reliably be plugged into other automation. Claude can help design a consistent JSON schema across all subcommands, so that, say, the field for an error text carries the same name in every subcommand, instead of differing from command to command.
# Consistent exit codes for script integration
# 0 = success, 1 = general error, 2 = config error, 3 = network error
shipctl deploy myservice --env production --json | jq -r '.status'
echo "Exit code: $?" # scriptable without any text parsing
7. Weighing distribution: npm, Homebrew, or binary releases
The choice of distribution channel depends heavily on the target audience and doesn't have a one size fits all answer. A tool used mainly by Node.js developers benefits from an npm release, because that audience is already familiar with npm install -g, while the same choice for a tool meant to be used cross platform by non Node developers as well forces an unnecessary Node.js runtime dependency that puts off many potential users.
Claude works well for describing your own target audience and technical context and deriving a reasoned recommendation from it, instead of the distribution channel just following the developer's personal favorite tool. For broadly spread audiences, a combination often works well: statically compiled binary releases via GitHub Releases as the base, complemented by Homebrew for macOS users and optionally an npm package as a thin wrapper for Node.js developers who prefer their usual install path.
8. Generating shell completion for bash, zsh, and fish
Automatic completion of commands, flags, and in some cases even dynamic values like available environment names has become standard among established CLI tools by now and is silently expected by experienced terminal users. Both Cobra and Click ship built in mechanisms to automatically generate completion scripts for common shells straight from the command definition, without maintaining them by hand.
Claude works well for checking whether the generated completion scripts actually cover every subcommand and flag correctly, and for designing the matching callback code for more complex cases, say dynamic completion of environment names pulled from a live API query, tailored to the respective library. This detail work often only gets picked up after the first release, but it improves a tool's perceived quality disproportionately relative to the implementation effort it takes.
# Cobra generates completion scripts directly from the command definition
shipctl completion bash > /etc/bash_completion.d/shipctl
shipctl completion zsh > "${fpath[1]}/_shipctl"
shipctl completion fish > ~/.config/fish/completions/shipctl.fish
9. Distribution channels compared
The following table compares common distribution channels by target platform and effort required.
| Channel | Target platform | Effort | Update mechanism |
|---|---|---|---|
| npm | Node.js developers, cross platform | Low, existing toolchain reusable | npm update -g |
| Homebrew | macOS and Linux users | Medium, maintaining your own formula | brew upgrade |
| Binary releases (GitHub) | All platforms, no runtime required | Medium, needs a cross compile pipeline | Manual or self update check |
| Cargo (crates.io) | Rust ecosystem, developers with Cargo | Low for an existing Rust project | cargo install --force |
| Docker image | CI environments, containerized workflows | Low, but extra runtime overhead | Running docker pull again |
| apt/deb package | Debian and Ubuntu servers | High, needs its own repository | apt upgrade |
Mironsoft
AI-assisted development, agent workflows, and team processes
Using Claude or other AI tools on the team, but without a clear workflow?
We set up AI-assisted development workflows for teams, from CLAUDE.md conventions to subagent strategies to code review processes that combine human oversight with AI speed.
Workflow Setup
Cleanly set up CLAUDE.md, project conventions, and tool permissions for the team.
Agent Strategy
Build subagent and automation workflows for recurring development tasks.
Team Onboarding
Train developers in productive, safe use of AI coding assistants.
10. Summary
Designing CLI Tools with Claude: The Essentials at a Glance
Core idea
A good CLI tool follows established conventions from well known tools instead of its own seemingly obvious shortcuts.
Key pattern
A command tree modeled on Cobra or Click, with inherited root flags and clearly grouped subcommands.
Biggest lever
Helpful error messages with a concrete next step instead of raw error codes.
Distribution rule
The target audience determines the channel, not the developer's personal favorite tool.