Table of Contents
🌏 中文版
The Design Problem: Muscle Memory Is the Biggest Existing Asset
Build a new coding agent and no matter how capable it is, you hit the same wall: users must first learn your interface. Every invented flag, every "huh, that's not what I thought it meant" moment loses people. Conversely, if a brand-new tool's first impression is "this works like what I use every day," learning cost drops to nearly zero — your fingers just know.
That's the core of CLI ergonomics: don't invent vocabulary, inherit it. Among Nielsen's usability heuristics, "consistency and standards" ranks fourth, and its point is exactly this: platform conventions belong to users, not to you. clig.dev likewise lists "following existing command-line conventions" as a first principle.
But what are the conventions? Talking in the abstract proves nothing, so I read the source code of five mature coding agents and compared their command surfaces directly.
How the Five Do It
Claude Code: positional prompt plus four key flags
Claude Code's commander definition states its default behavior outright: "starts an interactive session by default, use -p/--print for non-interactive output":
- The root command takes a
[prompt]positional argument (the program definition inclaude-code-source/src/main.tsx) -p, --print: "Print response and exit (useful for pipes)"-c, --continue: continue the most recent conversation in the current directory-r, --resume [value]: resume by session ID, or open an interactive picker
Interactive is the default; headless is opt-in — and headless stacks a whole group of options that only apply there (--output-format, --max-turns).
Codex: the usage string is the manifesto
Codex uses clap, and its root usage reads codex [OPTIONS] [PROMPT] alongside codex [OPTIONS] <COMMAND> [ARGS] (codex/codex-rs/cli/src/main.rs, MultitoolCli). In the subcommand table:
Exec(ExecCli)carries visible alias"e"— "Run Codex non-interactively"Resume(ResumeCommand)andFork(ForkCommand)are first-class commandsCliincodex-rs/exec/src/cli.rsaccepts a[PROMPT]positional, falling back to stdin when absent- Directory switching uses
-C/--cd(SharedCliOptionsincodex-rs/utils/cli/src/shared_options.rs) — direct descent fromgit -Candtar -C
Pi: hand-written parser, identical vocabulary
Pi skips frameworks and parses args itself, but the conventions align fully: --print/-p, --continue/-c, --resume/-r all live in parseArgs at pi-mono/packages/coding-agent/src/cli/args.ts; everything after -- goes into the messages positional. Modes split into modes/interactive (TUI) and modes/print-mode.ts (non-interactive) — interactive and headless are two modes of one tree, not two programs.
OMP: a fork inherits ergonomics too
OMP is a fork of Pi, and the parse logic in oh-my-pi/packages/coding-agent/src/cli/args.ts keeps --print/-p. That fact is itself part of the argument: even when forking someone else's project, the CLI surface is the last thing you need to touch — because it was already right.
OpenCode: bare command opens the TUI; run is non-interactive
In OpenCode's yargs definition, the default command ($0) is $0 [project] from cmd/tui.ts — typing opencode drops you into the TUI. The non-interactive path is RunCommand in cmd/run.ts: "run [message..]", where message is an array-typed positional and models take -m provider/model. Notably, within the run subcommand -p is given to --password (for attaching to a remote server) — subcommands may own local vocabulary, but the root-level convention stays clear.
The Converged Shared Vocabulary
| Convention | Claude Code | Codex | Pi | OMP | OpenCode |
|---|---|---|---|---|---|
| positional prompt | ✓ | ✓ | ✓ | ✓ | ✓ |
| interactive by default | ✓ | ✓ | ✓ | ✓ | ✓ |
-p = print/headless | ✓ | (exec) | ✓ | ✓ | unused at root |
| continue/resume | ✓ | ✓ | ✓ | ✓ | -c/-s |
-C/--cd for directory | (cwd is location) | ✓ | — | — | --dir |
Five projects evolving independently converged on the same answers — not coincidence, but forty years of command-line sedimentation.
rivumi's Choice and Its Differences
rivumi is my own Python coding agent. The M7 stage goal fits in one sentence: make it feel like a daily-driver coding CLI without replacing the loop or weakening safety boundaries (stage doc: rivumi/docs/stages/m7-familiar-cli-ergonomics.md).
The implementation has three layers.
Layer one: default routing. Typer's command groups demand a subcommand by default, but rivumi wants rivumi fix this bug to just run. So there's a custom DefaultCommandGroup: if the first argument isn't a known subcommand, it silently inserts a hidden chat command (DefaultCommandGroup.parse_args in src/rivumi/cli.py). This keeps rivumi resume and rivumi auth dispatching normally while arbitrary text becomes the initial prompt — the same shape as Codex's usage declaration, implemented in Python.
Layer two: vocabulary alignment. chat() carries three aliases --cd/-C/--repo (-C follows Codex/git; --repo preserves legacy automation), and --print/-p means non-interactive JSON output (the chat function in src/rivumi/cli.py). exec aliases run; both are the headless path. resume is a first-class command defaulting to "last", matching Claude Code's -c mental model. Historically -p once meant provider; M7 deliberately changed it and left a migration error message — because Claude Code and Pi users expect -p to mean print.
Layer three: config holds no secrets. rivumi config stores only three non-secret fields: provider, model, and api_url (CliConfig in src/rivumi/cli_config.py), with an extra="forbid" schema, atomic writes, mode 0600; API keys always come from environment variables or credential stores. Convenience must not trade away safety: what gets saved is typing, not approval.
Just as worth recording is what rivumi deliberately does not do: no full-screen TUI, no slash commands, no fuzzy spelling correction — mistype a subcommand and that word becomes a prompt, following Codex's logic. Each omission is a conscious trade-off, not missing work.
Engineering / UX Evidence
- Consistency and standards: Nielsen Norman Group lists "follow platform conventions" as the fourth of ten usability heuristics — user time belongs to the task, not your interface (Consistency and Standards).
- The CLI conventions compendium: clig.dev (Command Line Interface Guidelines) systematically covers exit codes, stdin/stdout, flag naming; all five projects' behavior maps onto it.
- The POSIX lineage of directory switching: from
git -Candtar -Cto Codex's-C/--cd, the same letter serving the same mental model for three decades. - The projects' own docs corroborate: Claude Code's CLI reference documents the semantics of
-p,--continue, and--resume; the Codex CLI docs and OpenCode docs likewise positionexec/runas the non-interactive paths.
Worth emphasizing: these conventions aren't aesthetic preferences. For agentic CLIs, whether -p means print directly determines whether a one-line CI script runs — the interface is the API.
Improvement Roadmap
rivumi already scores on "looks familiar." Three directions come next:
- Add continue: today there's only
resume [session]; a no-argument "continue most recent session" path would align with Claude Code's-c. - Session picker: both Claude Code's and Codex's resume ship an interactive picker by default; rivumi only accepts an ID or
last. - Fork semantics: Codex's
Forkand Claude Code's--fork-sessionshow that branching a new thread off an old session is now an expected capability, but rivumi's stage doc deliberately defers it — mutating durable tasks needs a protocol decision, not parser sugar.
The most honest test of CLI ergonomics: find someone who uses Claude Code or Codex daily and have them type at your tool with zero documentation. Their fingers will tell you the answer.
References
- Nielsen Norman Group — Consistency and Standards
- Command Line Interface Guidelines (clig.dev)
- Claude Code CLI reference
- OpenAI Codex CLI
- OpenCode documentation
- badlogic/pi-mono (source evidence: packages/coding-agent/src/cli/args.ts)
- openai/codex (source evidence: codex-rs/cli/src/main.rs, codex-rs/exec/src/cli.rs)
- sst/opencode (source evidence: packages/opencode/src/index.ts, src/cli/cmd/run.ts)
Loading...