skills/cli-ops/SKILL.md
Patterns for building production-quality CLI tools with predictable behavior, parseable output, and agentic workflows. Triggers: cli tool, command line tool, build cli, cli patterns, agentic cli, cli design, typer cli, click cli.
npx skillsauth add 0xDarkMatter/claude-mods cli-opsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Patterns for building CLI tools that AI assistants and power users can chain, parse, and rely on.
Build CLIs for agentic workflows - AI assistants and power users who chain commands, parse output programmatically, and expect predictable behavior.
| 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 |
<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
| 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 |
| 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 |
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 |
| 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 |
--json not --json=true-vq equals -v -qThis is the most critical rule:
| Stream | Content | When | |--------|---------|------| | stdout | Data only | Always | | stderr | Everything else | Interactive mode |
stdout receives:
--json is setstderr receives:
--verbose)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 |
See references/json-schemas.md for complete JSON response patterns.
Key conventions:
{"data": [...], "meta": {...}}{"data": {...}}{"error": {"code": "...", "message": "..."}}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 |
# 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
# 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)
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"
}
}
}
| 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 |
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)
Every --help output MUST include:
<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 should show:
jqTools requiring authentication MUST implement:
<tool> auth login # Interactive authentication
<tool> auth status # Check current state
<tool> auth logout # Clear credentials
Recommended: OS keyring with fallbacks for maximum security
Environment variable (CI/CD, testing)
MYTOOL_API_TOKEN or similarOS Keyring (primary storage - secure)
.env file (development fallback)
.gitignoreDependencies:
dependencies = [
"keyring>=24.0.0", # OS keyring access
"python-dotenv>=1.0.0", # .env file support
]
Simple alternative: Just config file in ~/.config/<tool>/
See references/implementation.md for complete credential storage implementations.
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
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"
}
{
"total": 1250.50,
"currency": "USD"
}
{
"id": "abc_123",
"legacy_id": "12345"
}
# All equivalent
--status DRAFT
--status draft
--status Draft
{"status": "IN_PROGRESS"}
# 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
# Limit results
--limit 50
-n 50
# Offset-based
--page 2
--offset 20
# Cursor-based
--cursor "eyJpZCI6MTIzfQ=="
--after "item_123"
See references/implementation.md for complete Python implementation templates including:
# 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"}]
# 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
# BAD: Different flags for same concept
$ tool1 list -j
$ tool2 list --format=json
# GOOD: Same flags everywhere
$ tool1 list --json
$ tool2 list --json
# 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
<tool> --version<tool> --help with examples<tool> <resource> list [--json]<tool> <resource> get <id> [--json]--jsonauth login, auth status, auth logout)--quiet and --verbose modes--dry-run for mutations--limit, --page)Typer (preferred for new tools):
Click (acceptable for existing tools):
# 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
testing
Audit any repo against the agentic-quality doctrine — score entry docs, structure, and enforcement gates, then map each finding to its fix. Triggers on: repo doctor, repo audit, agentic quality, is this repo agent-friendly, doc drift, stale AGENTS.md, monorepo structure, nested CLAUDE.md.
data-ai
Router for parallel or recurring agent work across six skills. Covers: parallel agents, fan out work, delegate to workers, run overnight, scheduled loop, land branches, mixed-model fleet, orchestrate workers, background agents at scale. Triggers on: which skill for parallel work, fan out agents, spawn workers, run this overnight, schedule a loop, land my branches, heterogeneous fleet, delegate to cheaper model, autonomous loop.
tools
Heterogeneous cross-provider fleet - GLM (z.ai), Codex (OpenAI), Grok (xAI), Anthropic Sonnet/Opus/Haiku - from one session, porting the native Workflow tool's patterns (adversarial verify, judge panels, journal resume) to OS-process workers. Triggers: fleetflow, heterogeneous/mixed-model fleet, codex worker, grok worker, cross-provider fan-out, cross-model verify.
development
Application/game-scale three.js: ES modules, GLTF pipeline (DRACO/KTX2/meshopt), AnimationMixer, physics (rapier/cannon-es), react-three-fiber, and performance at scale (InstancedMesh, LOD, draw calls). Triggers on: three.js, GLTFLoader, r3f, game loop, WebGL memory leak, boids.