Design and evaluate command-line tools for human users: naming and grammar, interactive prompts, colour and progress output, error messages, and a 0-21 usability rubric
72
91%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
When and how to prompt users interactively, how to design bypass flags for automation, and how to handle stdin detection. Interactive prompts make CLIs safer and more approachable — but only when they help, not when they block.
$ mycli database drop users-prod
⚠ This will permanently delete the 'users-prod' database (2.3 GB, 1.2M rows).
This action cannot be undone.
Type the database name to confirm: █$ mycli deploy
? Which environment? (Use arrow keys)
❯ staging
production
developmentmycli list should just list, not ask "which resource?"The simplest prompt. Use for binary decisions, especially destructive ones.
? Delete 3 pods in namespace 'production'? (y/N) █N signals the default.(Y/n) for non-destructive actions.y, yes, Y, YES, n, no, N, NO. Case-insensitive.--yes or --force or -yUse when the user must choose from a known list. Show the list, highlight the current selection.
? Select deployment target:
development
❯ staging
production (requires approval)--target staging (explicit flag matching the prompt's purpose)? Select services to restart: (Press <space> to select, <a> to toggle all)
◉ api-gateway
◯ web-frontend
◉ worker
◯ scheduler--services api-gateway,worker (comma-separated)? Enter new cluster name: █
(3-40 chars, lowercase alphanumeric and hyphens only)--name my-cluster? Enter API token: ████████--token-file, MYCLI_TOKEN env var, or OS keychain.ps.Every interactive prompt must have a corresponding flag or environment variable that provides the answer non-interactively.
| Prompt type | Flag bypass | Env var bypass |
|---|---|---|
| Confirm (destructive) | --force or --yes | MYCLI_FORCE=1 |
| Select environment | --env <name> | MYCLI_ENV=staging |
| Text input (name) | --name <value> | — |
| Password | --token-file <path> | MYCLI_TOKEN=xxx |
| Multi-select | --services a,b,c | — |
--yes / --force distinction--yes (-y): Skip confirmation prompts, accept defaults for all selections. Equivalent to pressing Enter on every prompt.--force: Skip confirmation AND override safety checks. More aggressive than --yes. Example: --force deletes even when dependent resources exist.Document the distinction. Don't use them interchangeably.
When stdin is not a TTY (piped, redirected, or running in CI), the CLI must never block waiting for user input.
import sys
if not sys.stdin.isatty():
# Non-interactive mode
passimport { isatty } from 'node:tty';
if (!isatty(0)) { // fd 0 = stdin
// Non-interactive mode
}Error: --env is required in non-interactive mode.
Usage: mycli deploy --env production --yes--yes or --force is passed:
Error: destructive operation requires --force in non-interactive mode.
Usage: mycli database drop users-prod --forceError: --target is required when stdin is not a terminal.
Available targets: development, staging, productionNever silently default in non-interactive mode. Explicit is safer than implicit when there's no human watching.
--dry-run shows what a command would do without executing it. This serves both humans (preview before committing) and scripts (validate before piping to the next command).
$ mycli deploy --env production --dry-run
Dry run — no changes will be made:
→ Update deployment 'api-gateway'
Image: app:v1.2.3 → app:v1.3.0
Replicas: 3 (unchanged)
Region: us-east-1
→ Create deployment 'new-worker'
Image: worker:v1.0.0
Replicas: 2
Region: us-east-1
2 changes planned. Run without --dry-run to apply.| Framework | Library | Features |
|---|---|---|
| Node.js (Commander.js) | @inquirer/prompts | Confirm, select, input, password, checkbox, search |
| Node.js (oclif) | @inquirer/prompts or @oclif/prompts | Same + oclif integration |
| Python (Click) | click.prompt(), click.confirm() | Built-in, basic prompts |
| Python (Typer) | typer.prompt(), typer.confirm() | Built-in via Click internals |
| Python (rich) | rich.prompt.Prompt, Confirm | Rich formatting in prompts |
| Go (Cobra) | AlecAivazis/survey/v2 or charmbracelet/huh | Confirm, select, input, multi-select |
| Rust (clap) | dialoguer | Confirm, select, input, password, multi-select, fuzzy-select |
| Rust (clap) | inquire | Alternative to dialoguer with similar API |
# Python (Click) — with non-interactive fallback
import click, sys
@click.command()
@click.option('--env', help='Target environment')
@click.option('--yes', '-y', is_flag=True, help='Skip confirmation')
def deploy(env, yes):
if not env:
if sys.stdin.isatty():
env = click.prompt('Target environment', type=click.Choice(['dev', 'staging', 'prod']))
else:
click.echo('Error: --env required in non-interactive mode', err=True)
raise SystemExit(2)
if env == 'prod' and not yes:
if sys.stdin.isatty():
click.confirm(f'Deploy to {env}?', abort=True)
else:
click.echo('Error: --yes required for production in non-interactive mode', err=True)
raise SystemExit(2)
# ... proceed with deploy