Build internal CLI tools that developers actually want to use. Covers CLI architecture, argument parsing, progressive disclosure, output formatting, plugin systems, and the UX principles that make developer tools delightful.
Internal tooling and CLI design matter when a platform team needs to expose operational primitives to developers or operators without leaking internal complexity. The command line is often the primary boundary between a platform’s capabilities and its consumers; when that boundary is poorly designed, every interaction becomes friction, scripts break silently, and adoption fragments across the organization. This page addresses the design of internal command-line interfaces—tools meant to be invoked by other scripts, CI systems, or human operators who expect consistent, predictable behavior. It matters whenever you are deciding whether to build a custom CLI, how to structure flags and output, and how to prevent the common failure modes that turn a useful tool into a maintenance burden.
The first touchpoint for any CLI is discovery. A well-designed tool should make it obvious what it can do before the user consults documentation. The standard pattern across mature CLIs involves a help subcommand or global flag that outputs a structured description of available subcommands and their arguments.
kubectl get --help
In practice, kubectl prints a hierarchical list: the top-level resource verbs (get, describe, apply) followed by resource types and their specific flags. The output is text-based but follows a predictable structure, allowing a user to scan for the relevant subcommand quickly.
For automation, tab completion is a non-negotiable feature. Installing completion scripts for common shells ensures that tab completion works for subcommands and flags, reducing the cognitive load and preventing typos in critical commands. If a CLI lacks completion support, the onus is on the consumer to maintain their own tab-completion configuration, which quickly becomes stale.
Invoking a command typically follows a verb-noun-adjective pattern. kubectl apply -f manifest.yaml demonstrates this: apply is the verb, the file path is the noun/adjective, and flags modify the behavior. The order of arguments matters. Most CLIs enforce that subcommands precede global flags, or global flags precede subcommands, depending on the design. Mixing these orders often results in the CLI interpreting a flag as an argument to the subcommand, leading to confusing error messages.
Flags are the mechanism by which a user configures a command’s behavior without altering the core operation. A sharp edge in CLI design is the distinction between global flags and subcommand flags. Global flags typically apply to the entire invocation, such as setting the API server address or enabling verbose output. Subcommand flags apply only to that specific operation, such as specifying a namespace for a get request.
kubectl --namespace production get pods
Here, --namespace is a subcommand flag. Confusing a global flag for a subcommand flag, or vice versa, is a frequent source of errors. For instance, passing --context as a subcommand flag to kubectl get pods may be ignored or misinterpreted, depending on the version, whereas the same flag placed globally switches the entire context for that session.
Boolean flags often have two forms: a positive form and a negated form. kubectl run my-app --restart=Never versus kubectl run my-app --no-restart. The negated form must be explicitly supported in the CLI definition; otherwise, the parser may reject the flag or treat the no- prefix as a literal string argument. Another common pattern is the "dry-run" flag, which allows the user to validate the request without executing it.
kubectl apply -f manifest.yaml --dry-run=client -o yaml
The --dry-run flag often accepts sub-values, such as client (simulates locally) or server (checks with the API server). The interaction between --dry-run and --validate can be subtle; in some implementations, --dry-run=client does not perform schema validation, relying on the user to provide correct manifests.
By default, many CLIs produce human-readable text output designed for terminal consumption. For scripting and integration, this is frequently insufficient. The standard solution is an output format flag.
kubectl get pods -o json
kubectl get pods -o yaml
kubectl get pods -o name
The -o json and -o yaml flags output the raw resource definition, which can be piped directly into other tools. The -o name flag strips all but the resource name, a minimal output useful for passing identifiers to subsequent commands.
Piping this output into processing tools is where brittleness often enters. Parsing human-readable text with grep or awk is fragile; a change in the CLI’s output version or a localized string update can break the script. The robust pattern is to output JSON or YAML and pipe it into a dedicated processor, such as jq for JSON or yq for YAML.
kubectl get pods -o json | jq '.items[] | select(.status.phase == "Running")'
This approach insulates the script from changes in formatting and provides a structured data shape that can be reliably queried.
When CLIs are intended for scripting, stability becomes paramount. A command that produces different output based on terminal width, locale, or update version is a liability. The primary defense is the --request-timeout flag, which prevents a script from hanging indefinitely if the target API server is unresponsive.
kubectl get pods --request-timeout=10s
Another critical pattern is the use of --dry-run before applying changes. In a CI pipeline, running a dry-run first allows the system to validate the intent and exit with a non-zero status if the change would be invalid, preventing partial or broken states from being committed.
if ! kubectl apply -f manifest.yaml --dry-run=client -o yaml > /dev/null; then
echo "Dry-run failed; aborting apply"
exit 1
fi
kubectl apply -f manifest.yaml
Idempotency is also a design goal. A well-designed CLI command should be safe to re-run. kubectl apply is idempotent by nature—if the manifest hasn't changed, the API server confirms no action is taken. However, kubectl replace is not; re-running it may overwrite existing settings unexpectedly. Scripts should favor apply over replace when the goal is to ensure a desired state without unintended side effects.
Several failure modes are specific to poorly designed CLI interfaces and can silently undermine operations.
--confirm might mean "skip confirmation prompt" at the global level but "confirm the resource kind" at the subcommand level. When a user places the flag in the wrong position, the CLI may silently apply the global behavior, leading to unintended actions such as deleting resources without a prompt.KUBECONFIG) that are not passed as flags. A script running in a clean environment may fail not because the command is wrong, but because the necessary context is missing. This creates a failure mode where the same command works in a local shell but fails in a CI job, and the error message offers no hint that context loading is the issue.if command; then may proceed down the wrong path. Understanding the specific exit code semantics of the tool in use is essential; relying on the generic "non-zero means fail" pattern can lead to logic errors.This page was rewritten on 18 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy