CLI user experience patterns: error messages that guide (not just report), help text design, autocomplete setup, config file hierarchy (~/.config/<tool>), environment variable conventions, --json/--quiet/--verbose flags, and progress indication. The difference between a tool people tolerate and one they recommend.
From clarcnpx claudepluginhub marvinrichter/clarc --plugin clarcThis skill uses the workspace's default tool permissions.
Designs and optimizes AI agent action spaces, tool definitions, observation formats, error recovery, and context for higher task completion rates.
Enables AI agents to execute x402 payments with per-task budgets, spending controls, and non-custodial wallets via MCP tools. Use when agents pay for APIs, services, or other agents.
Compares coding agents like Claude Code and Aider on custom YAML-defined codebase tasks using git worktrees, measuring pass rate, cost, time, and consistency.
--help output or man pages--json and --quiet output modes so a CLI can be scripted and piped without breaking human-readable outputGood error messages tell users what went wrong and what to do next.
error: <what went wrong>
hint: <what the user should do>
See: https://docs.example.com/errors/<code>
# Bad — reports the internal exception
Error: ENOENT: no such file or directory, open '/etc/config.json'
# Good — explains context and gives next step
error: config file not found at /etc/config.json
hint: run `tool init` to create a default config, or pass --config <path>
# Bad — cryptic status code
Error: 401
# Good — actionable
error: authentication failed (401)
hint: your API token may be expired. Run `tool auth login` to re-authenticate.
error: (lowercase) for the problem statementhint: on the next line with the recommended actionEvery command's --help must follow this structure:
Usage: tool <command> [options]
One-sentence description of what this command does.
Options:
--output, -o <file> Output file path [default: stdout]
--format <fmt> Output format: json|table|csv [default: table]
--verbose, -v Enable verbose logging
--quiet, -q Suppress all output except errors
--json Output as machine-readable JSON
--help, -h Show this help message
Examples:
tool export --output report.json --format json
tool export --quiet | jq '.users[]'
[default: x] for options with defaultsApply settings in this priority order (highest first):
1. CLI flags --output ./report.json
2. Environment variables MY_TOOL_OUTPUT=./report.json
3. Project config ./.mytool.json or ./.mytool/config.json
4. User config ~/.config/mytool/config.json ($XDG_CONFIG_HOME/mytool/)
5. System config /etc/mytool/config.json
6. Built-in defaults
import { readFileSync, existsSync } from 'fs';
import { join } from 'path';
import os from 'os';
interface Config {
output: string;
format: 'json' | 'table' | 'csv';
token?: string;
}
const DEFAULT_CONFIG: Config = { output: 'stdout', format: 'table' };
function loadConfig(cliFlags: Partial<Config>): Config {
const userConfigPath = join(
process.env.XDG_CONFIG_HOME ?? join(os.homedir(), '.config'),
'mytool',
'config.json',
);
const projectConfigPath = join(process.cwd(), '.mytool.json');
const userConfig = existsSync(userConfigPath)
? JSON.parse(readFileSync(userConfigPath, 'utf-8'))
: {};
const projectConfig = existsSync(projectConfigPath)
? JSON.parse(readFileSync(projectConfigPath, 'utf-8'))
: {};
const envConfig = {
...(process.env.MY_TOOL_OUTPUT && { output: process.env.MY_TOOL_OUTPUT }),
...(process.env.MY_TOOL_FORMAT && { format: process.env.MY_TOOL_FORMAT }),
...(process.env.MY_TOOL_TOKEN && { token: process.env.MY_TOOL_TOKEN }),
};
// Merge in priority order: defaults < user < project < env < CLI flags
return { ...DEFAULT_CONFIG, ...userConfig, ...projectConfig, ...envConfig, ...cliFlags };
}
| Naming rule | Example |
|---|---|
| All uppercase | MY_TOOL_TOKEN |
| Prefix with tool name | MY_TOOL_ (not TOKEN) |
| Underscore-separated | MY_TOOL_OUTPUT_FORMAT |
| No abbreviations | MY_TOOL_TIMEOUT (not MY_TOOL_TO) |
MY_TOOL_TOKEN # Auth token (avoids --token flag in shell history)
MY_TOOL_CONFIG # Override config file path
MY_TOOL_LOG_LEVEL # debug|info|warn|error
MY_TOOL_NO_COLOR # Disable colored output (honour this if set to any value)
NO_COLOR # Standard cross-tool convention (https://no-color.org)
--helpEnvironment Variables:
MY_TOOL_TOKEN API token (alternative to --token)
MY_TOOL_CONFIG Config file path (alternative to --config)
NO_COLOR Disable colored output
Shell autocomplete dramatically improves discoverability.
// yargs generates completions automatically
yargs(hideBin(process.argv))
.completion('completion', 'Generate shell completion script')
.argv;
// Install: tool completion >> ~/.zshrc
# click generates completions for bash/zsh/fish
_MY_TOOL_COMPLETE=bash_source my-tool >> ~/.bashrc
_MY_TOOL_COMPLETE=zsh_source my-tool >> ~/.zshrc
_MY_TOOL_COMPLETE=fish_source my-tool > ~/.config/fish/completions/my-tool.fish
// cobra has a built-in completion command
rootCmd.AddCommand(completionCmd)
var completionCmd = &cobra.Command{
Use: "completion [bash|zsh|fish|powershell]",
Short: "Generate shell completion script",
RunE: func(cmd *cobra.Command, args []string) error {
switch args[0] {
case "bash": return rootCmd.GenBashCompletion(os.Stdout)
case "zsh": return rootCmd.GenZshCompletion(os.Stdout)
case "fish": return rootCmd.GenFishCompletion(os.Stdout, true)
default: return fmt.Errorf("unsupported shell: %s", args[0])
}
},
}
use clap_complete::{generate, Shell};
// Add completion subcommand
fn generate_completion(shell: Shell) {
let mut cmd = Cli::command();
generate(shell, &mut cmd, "my-tool", &mut io::stdout());
}
## Shell Completion
```bash
# bash
tool completion bash >> ~/.bashrc
# zsh
tool completion zsh >> ~/.zshrc
# fish
tool completion fish > ~/.config/fish/completions/tool.fish
---
## Progress Indication
### Spinner — unknown duration
```typescript
// Node.js — ora
import ora from 'ora';
const spinner = ora('Fetching data…').start();
try {
const data = await fetchData();
spinner.succeed('Data fetched');
} catch (error) {
spinner.fail(`Failed: ${error.message}`);
process.exit(1);
}
# Python — rich
from rich.console import Console
console = Console()
with console.status("Fetching data..."):
data = fetch_data()
console.print("[green]Done![/green]")
// Node.js — cli-progress
import { SingleBar, Presets } from 'cli-progress';
const bar = new SingleBar({}, Presets.shades_classic);
bar.start(totalFiles, 0);
for (const file of files) {
await processFile(file);
bar.increment();
}
bar.stop();
--no-progress flagAlways provide a way to disable progress output (important for CI and piping):
if (!argv.noProgress && process.stdout.isTTY) {
const spinner = ora('Processing…').start();
// ...
}
hint: line--help examples are mandatory — copy-pasteable, at the endMY_TOOL_TOKEN, not TOKEN--no-progress for CI — spinners in CI logs are noiseNO_COLOR is honoured — use the standard, do not invent your ownerror: + hint: lines--help has Usage, Description, Options with defaults, and Examples sections--helpNO_COLOR respectedcompletion bash|zsh|fish)--no-progress flag available for CI environments