Skip to main content
Define a custom CLI when an agent needs service operations with a stable command grammar. Use a typed CLI for parsed commands and generated help, or a script CLI when the program should parse its own arguments. Shell builtins and CLIs keep separate catalogs and dispatch. Both use CommandSpec for argument parsing.

Define a typed command

Each handler receives one CLIInvocation. The example below shows the registration and parsing contract; the runnable pager example adds a mutable incident service and shared cross-language coverage.
The same CLI can be installed for multiple accounts. Service IDs use textual operands; file operands use type="path". Read flags through FlagView(inv.flags, inv.spec) so a read of an undeclared option fails rather than looking like an omitted flag.

Choose the command shape

A typed node is a leaf or a group. Names and aliases are single words; child names and aliases share one namespace. Options cannot collide across ancestors and descendants. Construction validates the tree and compiles the grammar; do not repeat parser validation in every handler. CommandSpec.arguments holds options and positional arguments: a dashed name such as --by declares an option; a bare name declares a positional. CLI.handlers maps canonical paths below the root to CLIHandler bindings, such as "ack", "incident ack", or "" for a single-command program. Declare config_model on the account CLI. Registration validates each installation, and inv.config holds that account’s settings. Without a config model, a typed CLI accepts no config and receives None. Mark mutating CLIHandler bindings write=True. This classifies the command for policy and cache invalidation; it does not grant permission or perform the mutation. Descriptions, options and operands supply --help and man at every level.

Register the program

Installing a CLI directly is the shortest path:
The installed name is the dispatch key and does not have to equal the spec’s root name. This is what makes one tree usable for several accounts. To resolve a bare name from workspace YAML, register the CLI before loading the workspace:
cli: may instead point directly at module:ATTR or ./file.py:ATTR. Relative files resolve next to the YAML file. A packaged Python CLI can publish the same CLI without import side effects:
Registered names and builtins win over direct references and entry points. Registering a name already held by another explicit registration or builtin fails; an explicit registration may intentionally shadow an entry point. Registration and installation are host-side lifecycle operations. There is no shell install or uninstall command, so an agent can use a CLI it was given but cannot remove it. A deployment that must pin the head word should enforce that with policy; shell functions can otherwise shadow it like they do in bash.

Use workspace files

Build the service client from inv.config. A CLI should not find a matching VFS to obtain credentials or a backend accessor. Reuse core service functions between CLI and VFS implementations where their behavior is the same. For file arguments such as uploads or attachments, use inv.view: Declare file options and operands with type="path"; preserve the resulting PathSpec values when dispatching. Use PathSpec.from_str_path(value, cwd=inv.cwd) for a path string from config, and derive paths with join and parent. Do not convert them to plain strings before filesystem access. Direct handler callers may omit the view; a handler requiring files must refuse when the needed entry point is absent. Account CLI writes, including failed writes that may have reached the service, expire mounted caches. Config-free filesystem CLIs rely on the dispatcher’s per-path invalidation. A script marked as a write also expires caches because its config and service access are opaque.

Snapshots and credentials

A named typed CLI is saved by spec name. Fields declared as Pydantic secrets in config_model are redacted. Loading that snapshot must provide fresh install config through clis=; Mirage refuses to restore redacted credentials as if they were usable values. The loading process must also make the spec name resolvable, or pass (cli, config) as that install’s clis= override. A tree installed directly in live code survives Workspace.copy, which carries the live CLI and revealed config together. A script CLI is different: the script source is embedded in the snapshot and its opaque config is serialized verbatim because there is no config model to identify secrets. Keep credentials in the environment, or provide fresh script config through clis= when loading.

Script CLIs

Use script: when the program should own its grammar or should have no Mirage imports:
The file is embedded at load. Words after the head arrive as argv, piped input arrives on stdin, and config arrives as JSON in MIRAGE_CLI_CONFIG. The program’s stdout, stderr, and exit status become the shell result. A script CLI cannot declare config_model, subcommands, or function handlers; it parses and documents its own options, including --help. Slot 0 is the installed name on monty and quickjs, so two installs can identify themselves and user-facing errors can name the invoked program. Wasi and local run CPython with -c, which owns sys.argv[0]; arguments still begin at index
  1. A YAML script declares no Mirage grammar, so every option reaches the program unchanged. A script CLI constructed in code may supply a CommandSpec to opt into Mirage parsing and generated help.
Runtime argument spellings differ: The sandboxed runtimes provide files plus compute, not arbitrary networking or third-party packages. runtime: local selects the host interpreter when that broader authority is intentional.

Authoring reference

The public constructors document exact types and defaults: CLI and CLIHandler, and CommandSpec and Argument.

CLI and CLIHandler

CommandSpec

Argument

This follows argparse’s naming conventions and supports a subset of its features. Positional defaults and variadic options are unsupported; append collects single option values, while extend flattens fixed groups.

CLIInvocation

Verify a custom CLI

Check help, successful execution, a refusal and isolation between two account installations. Exercise VFS access with path and mount policy, and verify snapshot restoration with credentials supplied again. Keep service request construction and internal edge cases in unit tests; put observable shell results in shared integration cases when both hosts implement the program. The shared grammar also serves shell commands. Prefer its existing argparse and POSIX concepts; keep a program-specific rule in its handler, or use a script when the whole program needs to own parsing.