Skip to content

Shell Completions

Shell completions let users press Tab to auto-complete subcommands, flags, and arguments. For CLI tools with dozens of flags and nested subcommands, good completions dramatically reduce friction. All three reference implementations generate completion scripts at runtime rather than shipping static files, but the mechanisms vary: Python’s shtab, Rust’s clap_complete, and Node’s yargs built-in completion.

Reference: references/aider/ at commit b9050e1d

Aider uses shtab, a Python library that generates shell completion scripts from argparse/configargparse parser definitions. The integration is minimal — shtab introspects the parser’s argument definitions and produces a completion script for the requested shell.

The --shell-completions flag is defined in aider/args.py:853-862:

supported_shells_list = sorted(list(shtab.SUPPORTED_SHELLS))
group.add_argument(
"--shell-completions",
metavar="SHELL",
choices=supported_shells_list,
help=(
"Print shell completion script for the specified SHELL and exit. Supported shells:"
f" {', '.join(supported_shells_list)}. Example: aider --shell-completions bash"
),
)

The handler in aider/main.py:506-509 catches the flag early and exits:

if args.shell_completions:
parser.prog = "aider"
print(shtab.complete(parser, shell=args.shell_completions))
sys.exit(0)

bash, zsh, and tcsh (the set defined by shtab.SUPPORTED_SHELLS). Fish is not supported by shtab.

Aider annotates 15 arguments that accept file paths with shtab.FILE markers. For example, from aider/args.py:58:

parser.add_argument(
"--model-settings-file",
...
).complete = shtab.FILE

This tells shtab to generate file path completion logic for these specific arguments. Other arguments (model names, color codes, format strings) have no special completion — they fall back to the shell’s default behavior (no suggestions).

The generated script is entirely static. It encodes the argument names and their types (file, choice list, etc.) at generation time. If Aider adds new flags in a newer version, the user must regenerate the completion script. There is no mechanism for dynamic model name completion or lazy fetching of available options.

Manual. The user runs aider --shell-completions bash >> ~/.bashrc (or the equivalent for their shell) and sources the updated config. Aider’s installer and pip package do not install completions automatically.


Reference: references/codex/ at commit 4ab44e2c

Codex uses clap_complete, the standard completion generation crate for Rust’s clap argument parser. The dependency is declared in codex-rs/cli/Cargo.toml:

clap = { workspace = true, features = ["derive"] }
clap_complete = { workspace = true }

Codex exposes a dedicated completion subcommand rather than a flag. The subcommand is defined in codex-rs/cli/src/main.rs:109-110:

/// Generate shell completion scripts.
Completion(CompletionCommand),

The CompletionCommand struct (main.rs:148-153):

#[derive(Debug, Parser)]
struct CompletionCommand {
/// Shell to generate completions for
#[clap(value_enum, default_value_t = Shell::Bash)]
shell: Shell,
}

The handler (main.rs:1027-1031):

fn print_completion(cmd: CompletionCommand) {
let mut app = MultitoolCli::command();
let name = "codex";
generate(cmd.shell, &mut app, name, &mut std::io::stdout());
}

bash, zsh, fish, PowerShell, and elvish (the full clap_complete::Shell enum). The default is bash if no shell is specified.

clap_complete generates completions for all subcommands, flags, and value_enum arguments automatically from the clap derive macros. This includes:

  • Subcommands: codex completion, codex sandbox, codex debug, codex resume, codex fork, codex apply, codex review
  • Flags: --model, --approval-policy, --sandbox, -c, --full-auto, --quiet, etc.
  • Enum values: Shell names for completion, sandbox modes, approval policies — any argument defined with #[clap(value_enum)] gets its valid values embedded in the completion script

File path completion is handled automatically by clap for arguments that accept paths.

The codex-cli npm package (codex-cli/bin/codex.js) is a thin wrapper that spawns the Rust binary. It does not add its own shell completions — users must use the Rust binary’s codex completion command directly.

Manual, like Aider. Typical installation patterns:

Terminal window
# Bash
codex completion bash > /usr/share/bash-completion/completions/codex
# Zsh
codex completion zsh > ~/.zfunc/_codex
# Fish
codex completion fish > ~/.config/fish/completions/codex.fish

Or dynamic eval for development:

Terminal window
eval "$(codex completion zsh)"

Reference: references/opencode/ at commit 7ed44997

OpenCode uses yargs, a Node.js CLI framework with built-in completion support. The completion subcommand is registered in packages/opencode/src/index.ts:121:

const cli = yargs(hideBin(process.argv))
.scriptName("opencode")
// ... options and commands ...
.completion("completion", "generate shell completion script")

Yargs’ .completion() method generates a bash/zsh completion script that, when sourced, calls opencode --get-yargs-completions at Tab-press time. This is fundamentally different from Aider and Codex: yargs completions are dynamic. When the user presses Tab, the shell calls back into the running opencode binary, which introspects its own command structure and returns matching options.

bash and zsh (yargs’ standard support). Fish is not directly supported by yargs’ built-in completion.

Because yargs introspects the command structure at runtime, completions automatically stay in sync with the codebase. When a new command is added via .command(), it appears in completions without any manual registration. This is a significant advantage over shtab and clap_complete, which produce static scripts that must be regenerated.

Dynamic completions require the CLI binary to be invoked on every Tab press. For a Bun-based TypeScript tool, this means a cold-start latency on each completion request. In practice, yargs completions finish in under 100ms on most systems, but it’s noticeably slower than sourcing a pre-generated static script.

Terminal window
eval "$(opencode completion)"

Or pipe to a file for persistent installation. The opencode package does not install completions during npm install.


Static completions (shtab, clap_complete) are fast (no binary invocation at Tab time) but go stale when the CLI changes. Dynamic completions (yargs) are always current but add latency. For a Rust binary with sub-millisecond startup, dynamic completions could be viable, but the completion protocol itself (shell calls binary, binary prints options, shell displays) adds overhead regardless.

None of the three reference implementations install completions automatically. Users must manually run the generation command and source the output. This is a deliberate choice — modifying shell config files without consent is invasive — but it means most users never set up completions at all.

Aider (shtab) doesn’t support fish. Codex (clap_complete) does. OpenCode (yargs) doesn’t natively. Fish is increasingly popular among developer tools users, so lacking support is a meaningful gap. The clap_complete crate handles fish out of the box, which is one advantage of the Rust ecosystem.

Model Name Completion Is Missing Everywhere

Section titled “Model Name Completion Is Missing Everywhere”

None of the three tools complete model names dynamically. When a user types --model gpt-4 and presses Tab, nothing happens. This is a missed opportunity — the list of valid models is finite and could be fetched from cached provider metadata. Codex’s ModelsManager with ETag-cached model lists would be the natural data source for this, but no completion hook exists.

Aider uses a flag (--shell-completions bash) while Codex uses a subcommand (codex completion bash). The subcommand approach is cleaner: it’s discoverable via codex --help as a first-class entry, and it avoids polluting the main flag namespace. The flag approach requires the parser to handle --shell-completions before any other argument processing, which is fragile.

When a user upgrades their CLI tool, the old completion script may reference flags or subcommands that no longer exist, or miss new ones. None of the tools include a version check in the completion script. A simple # Generated by codex v1.2.3 comment would let users detect staleness, but nobody does this.


Use clap_complete as the primary generation engine. It covers bash, zsh, fish, PowerShell, and elvish with zero custom code. Completion scripts are generated from the same clap derive macros that define the CLI structure, so they stay synchronized with the implementation by construction.

Expose completions as a subcommand, following Codex’s pattern:

openoxide completion [SHELL]

Default to the user’s current shell (detected from $SHELL) if no argument is given. This avoids the common user error of running completion bash when they’re actually using zsh.

fn default_shell() -> Shell {
std::env::var("SHELL")
.ok()
.and_then(|s| s.rsplit('/').next().map(String::from))
.and_then(|name| name.parse::<Shell>().ok())
.unwrap_or(Shell::Bash)
}

Consider adding a clap_complete::DynComplete integration that calls into the cached model registry to complete --model arguments at Tab time. This would require the binary to perform a fast lookup against the cached provider metadata (no network call). The ModelsManager cache (see Provider Abstraction) would be the data source.

Provide an openoxide completion install subcommand that:

  1. Detects the user’s shell
  2. Generates the completion script
  3. Prints the exact command needed to install it (e.g., source <(openoxide completion zsh))
  4. Optionally appends to the shell config file with --write flag, after explicit user confirmation

This lowers the friction without being invasive.

  • clap_complete — Completion script generation for 5 shells
  • clap with derive feature — CLI definition that doubles as the completion schema