cli-patterns

CLI Patterns for Agentic Workflows

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "cli-patterns" with this command: npx skills add 0xdarkmatter/claude-mods/0xdarkmatter-claude-mods-cli-patterns

CLI Patterns for Agentic Workflows

Patterns for building CLI tools that AI assistants and power users can chain, parse, and rely on.

Philosophy

Build CLIs for agentic workflows - AI assistants and power users who chain commands, parse output programmatically, and expect predictable behavior.

Core Principles

Principle Meaning Why It Matters

Self-documenting --help is comprehensive and always current LLMs discover capabilities without external docs

Predictable Same patterns across all commands Learn once, use everywhere

Composable Unix philosophy - do one thing well Tools chain together naturally

Parseable --json always available, always valid Machine consumption without parsing hacks

Quiet by default Data only, no decoration unless requested Scripts don't break on unexpected output

Fail fast Invalid input = immediate error No silent failures or partial results

Design Axioms

  • stdout is sacred - Only data. Never progress, never logging, never decoration.

  • stderr is for humans - Progress bars, colors, tables, warnings live here.

  • Exit codes have meaning - Scripts can branch on failure mode.

  • Help includes examples - The fastest path to understanding.

  • JSON shape is predictable - Same structure across all commands.

Command Architecture

Structural Pattern

<tool> [global-options] <resource> <action> [options] [arguments]

Every CLI follows this hierarchy:

<tool> ├── --version, --help # Global flags ├── auth # Authentication (if required) │ ├── login │ ├── status │ └── logout └── <resource> # Domain resources (plural nouns) ├── list # Get many ├── get <id> # Get one by ID ├── create # Make new (if supported) ├── update <id> # Modify existing (if supported) ├── delete <id> # Remove (if supported) └── <custom-action> # Domain-specific verbs

Naming Conventions

Element Convention Valid Examples Invalid Examples

Tool name lowercase, 2-12 chars mytool , datactl

MyTool , my-tool-cli

Resource plural noun, lowercase invoices , users

Invoice , user

Action verb, lowercase list , get , sync

listing , getter

Long flags kebab-case --dry-run , --output-format

--dryRun , --output_format

Short flags single letter -n , -q , -v

-num , -quiet

Standard Resource Actions

Action HTTP Equiv Returns Idempotent

list

GET /resources Array Yes

get <id>

GET /resources/:id Object Yes

create

POST /resources Created object No

update <id>

PATCH /resources/:id Updated object Yes

delete <id>

DELETE /resources/:id Confirmation Yes

search

GET /resources?q= Array Yes

Flags & Options

Mandatory Flags

Every command MUST support:

Flag Short Behavior Output

--help

-h

Show help with examples Help text to stdout, exit 0

--json

Machine-readable output JSON to stdout

Root command MUST additionally support:

Flag Short Behavior Output

--version

-V

Show version <tool> <version> to stdout, exit 0

Recommended Flags

Flag Short Type Purpose Default

--quiet

-q

bool Suppress non-essential stderr false

--verbose

-v

bool Increase detail level false

--dry-run

bool Preview without executing false

--limit

-n

int Max results to return 20

--output

-o

path Write output to file stdout

--format

-f

enum Output format varies

Flag Behavior Rules

  • Boolean flags take no value: --json not --json=true

  • Short flags can combine: -vq equals -v -q

  • Unknown flags are errors: Never silently ignore

  • Repeated flags: Last value wins (or error if inappropriate)

Output Specification

Stream Separation

This is the most critical rule:

Stream Content When

stdout Data only Always

stderr Everything else Interactive mode

stdout receives:

  • JSON when --json is set

  • Minimal text output when interactive

  • Nothing else. Ever.

stderr receives:

  • Progress indicators (spinners, bars)

  • Status messages ("Fetching...", "Done")

  • Warnings

  • Rich formatted tables

  • Colors and decoration

  • Debug information (--verbose )

Interactive Detection

import sys

def is_interactive() -> bool: """True if connected to a terminal, not piped.""" return sys.stdout.isatty() and sys.stderr.isatty()

Context stdout.isatty() Behavior

Terminal True Rich output to stderr, summary to stdout

Piped (| jq ) False Minimal/JSON to stdout

Redirected (> file ) False Minimal to stdout

--json flag Any JSON to stdout, suppress stderr noise

JSON Output Schema

See references/json-schemas.md for complete JSON response patterns.

Key conventions:

  • List responses: {"data": [...], "meta": {...}}

  • Single item: {"data": {...}}

  • Errors: {"error": {"code": "...", "message": "..."}}

  • ISO 8601 dates, decimal money, string IDs

Exit Codes

Semantic exit codes that scripts can rely on:

Code Name Meaning When

0 SUCCESS Operation completed Everything worked

1 ERROR General/unknown error Unexpected failures

2 AUTH_REQUIRED Not authenticated No token, token expired

3 NOT_FOUND Resource missing ID doesn't exist

4 VALIDATION Invalid input Bad arguments, failed validation

5 FORBIDDEN Permission denied Authenticated but not authorized

6 RATE_LIMITED Too many requests API throttling

7 CONFLICT State conflict Concurrent modification, duplicate

Usage

Script can branch on exit code

mytool items get item-001 --json case $? in 0) echo "Success" ;; 2) echo "Need to authenticate" && mytool auth login ;; 3) echo "Item not found" ;; *) echo "Error occurred" ;; esac

Implementation

Constants

EXIT_SUCCESS = 0 EXIT_ERROR = 1 EXIT_AUTH_REQUIRED = 2 EXIT_NOT_FOUND = 3 EXIT_VALIDATION = 4 EXIT_FORBIDDEN = 5 EXIT_RATE_LIMITED = 6 EXIT_CONFLICT = 7

Usage

raise typer.Exit(EXIT_NOT_FOUND)

Error Handling

Error Output Format

With --json , errors output structured JSON to stdout AND a message to stderr:

stderr:

Error: Item not found

stdout:

{ "error": { "code": "NOT_FOUND", "message": "Item not found", "details": { "item_id": "bad-id" } } }

Error Codes

Code Exit Meaning

AUTH_REQUIRED

2 Must authenticate first

TOKEN_EXPIRED

2 Token needs refresh

FORBIDDEN

5 Insufficient permissions

NOT_FOUND

3 Resource doesn't exist

VALIDATION_ERROR

4 Invalid input

INVALID_ARGUMENT

4 Bad argument value

MISSING_ARGUMENT

4 Required argument missing

RATE_LIMITED

6 Too many requests

CONFLICT

7 State conflict

ALREADY_EXISTS

7 Duplicate resource

INTERNAL_ERROR

1 Unexpected error

API_ERROR

1 Upstream API failed

NETWORK_ERROR

1 Connection failed

Implementation Pattern

def _error( message: str, code: str = "ERROR", exit_code: int = EXIT_ERROR, details: dict = None, as_json: bool = False, ): """Output error and exit.""" error_obj = {"error": {"code": code, "message": message}} if details: error_obj["error"]["details"] = details

if as_json:
    print(json.dumps(error_obj, indent=2))

# Always print human message to stderr
console.print(f"[red]Error:[/red] {message}")
raise typer.Exit(exit_code)

Help System

Help Requirements

Every --help output MUST include:

  • Brief description (one line)

  • Usage syntax

  • Options with descriptions

  • Examples (critical for discovery)

Help Format Template

<one-line description>

Usage: <tool> <resource> <action> [OPTIONS] [ARGS]

Arguments: <arg> Description of positional argument

Options: -s, --status TEXT Filter by status -n, --limit INTEGER Max results [default: 20] --json Output as JSON -h, --help Show this help

Examples: <tool> <resource> <action> <tool> <resource> <action> --status active <tool> <resource> <action> --json | jq '.[0]'

Examples Are Critical

Examples should show:

  • Basic usage - Simplest invocation

  • Common filters - Most-used options

  • JSON piping - How to chain with jq

  • Real-world scenarios - Actual use cases

Authentication

Auth Commands

Tools requiring authentication MUST implement:

<tool> auth login # Interactive authentication <tool> auth status # Check current state <tool> auth logout # Clear credentials

Credential Storage Priority

Recommended: OS keyring with fallbacks for maximum security

Environment variable (CI/CD, testing)

  • MYTOOL_API_TOKEN or similar

  • Highest priority, overrides all other sources

OS Keyring (primary storage - secure)

  • Windows: Credential Manager

  • macOS: Keychain

  • Linux: Secret Service (GNOME Keyring, KWallet)

  • Encrypted at rest, per-user isolation

.env file (development fallback)

  • Plain text in current directory

  • Convenient for local development

  • Must be in .gitignore

Dependencies:

dependencies = [ "keyring>=24.0.0", # OS keyring access "python-dotenv>=1.0.0", # .env file support ]

Simple alternative: Just config file in ~/.config/<tool>/

  • Good for tools without sensitive credentials

  • Or when OS keyring adds too much complexity

See references/implementation.md for complete credential storage implementations.

Unauthenticated Behavior

When auth is required but missing:

$ mytool items list Error: Not authenticated. Run: mytool auth login

exit code: 2

$ mytool items list --json

stderr: Error: Not authenticated. Run: mytool auth login

{"error": {"code": "AUTH_REQUIRED", "message": "Not authenticated. Run: mytool auth login"}}

exit code: 2

Data Conventions

Date Handling

Input (Flexible): Accept multiple formats for user convenience

Format Example Interpretation

ISO date 2025-01-15

Exact date

ISO datetime 2025-01-15T10:30:00Z

Exact datetime

Relative today , yesterday , tomorrow

Current/previous/next day

Relative last , this (with context) Previous/current period

Output (Strict): Always output ISO 8601

{ "created_at": "2025-01-15T10:30:00Z", "due_date": "2025-02-15", "month": "2025-01" }

Money

  • Store as decimal number, not cents

  • Include currency when ambiguous

  • Never format (no "$" or "," in JSON)

{ "total": 1250.50, "currency": "USD" }

IDs

  • Always strings (even if numeric)

  • Preserve exact format from source

{ "id": "abc_123", "legacy_id": "12345" }

Enums

  • UPPER_SNAKE_CASE in JSON

  • Case-insensitive input

All equivalent

--status DRAFT --status draft --status Draft

{"status": "IN_PROGRESS"}

Filtering & Pagination

Common Filter Patterns

By status

--status DRAFT --status active,pending # Multiple values

By date range

--from 2025-01-01 --to 2025-01-31 --month 2025-01 --month last

By related entity

--user "Alice" --project "Project X"

Text search

--search "keyword" -q "keyword"

Boolean filters

--archived --no-archived --include-deleted

Pagination

Limit results

--limit 50 -n 50

Offset-based

--page 2 --offset 20

Cursor-based

--cursor "eyJpZCI6MTIzfQ==" --after "item_123"

Implementation

See references/implementation.md for complete Python implementation templates including:

  • CLI skeleton with Typer

  • Client pattern with httpx

  • Error handling

  • Authentication flows

  • Testing patterns

Anti-Patterns

❌ Output Pollution

BAD: Progress to stdout

$ bad-tool items list --json Fetching items... [{"id": "1"}] Done!

GOOD: Only JSON to stdout

$ good-tool items list --json [{"id": "1"}]

❌ Interactive Prompts

BAD: Prompts in non-interactive context

$ bad-tool items create Enter name: _

GOOD: Fail fast with required flags

$ good-tool items create Error: --name is required

❌ Inconsistent Flags

BAD: Different flags for same concept

$ tool1 list -j $ tool2 list --format=json

GOOD: Same flags everywhere

$ tool1 list --json $ tool2 list --json

❌ Silent Failures

BAD: Success exit code on failure

$ bad-tool items delete bad-id Item not found $ echo $? 0

GOOD: Semantic exit code

$ good-tool items delete bad-id Error: Item not found: bad-id $ echo $? 3

Quick Reference

Must-Have Checklist

  • <tool> --version

  • <tool> --help with examples

  • <tool> <resource> list [--json]

  • <tool> <resource> get <id> [--json]

  • Semantic exit codes (0, 1, 2, 3, 4, 5, 6, 7)

  • Errors to stderr, data to stdout

  • Valid JSON on --json

  • Stream separation (stdout = data, stderr = UI)

Recommended Additions

  • Authentication commands (auth login , auth status , auth logout )

  • Create/Update/Delete operations

  • --quiet and --verbose modes

  • --dry-run for mutations

  • Pagination (--limit , --page )

  • Filtering (status, date range, search)

  • Automated tests

Framework Choice

Typer (preferred for new tools):

  • Type hints provide automatic validation

  • Built-in help generation

  • Rich integration for beautiful output

  • Less boilerplate than Click

Click (acceptable for existing tools):

  • Typer is built on Click (100% compatible)

  • Well-structured Click code doesn't need migration

  • Both must follow same output conventions

Typer (preferred)

import typer from rich.console import Console

app = typer.Typer() console = Console(stderr=True) # UI to stderr

Click (acceptable)

import click from rich.console import Console

console = Console(stderr=True) # Same pattern

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Coding

python-pytest-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-async-patterns

No summary provided by upstream source.

Repository SourceNeeds Review
Coding

python-env

No summary provided by upstream source.

Repository SourceNeeds Review