Skip to content

Guide: The No-Args Entry Point

The principle: A multi-command CLI must exit 0 and show its root help when invoked with no arguments. This is the agent's first-contact probe — every agent-compatibility feature is unreachable if the entry point fails.

Agents do not start with --help. They run the bare binary first. It is cheaper, faster, and tells them whether the tool is accessible before committing tokens to per-command discovery. A non-zero exit on that probe short-circuits the entire session. The agent logs a failure, may retry, and may give up — never reaching the --schema flag, the JSON output mode, or the structured error codes that the CLI author built.


The Discovery Gap

A CLI can invest in advanced agent features and still be unreachable. The bare-invocation probe is the first thing an agent runs — if it fails, nothing else matters:

$ poly
usage: poly [-h] [--schema] [--output FORMAT] [--json] [-v] COMMAND ...
poly: error: the following arguments are required: COMMAND
exit code: 2

poly has --schema, --output FORMAT, and --json — none of it reachable. Contrast with:

$ polymarket
Polymarket CLI

Usage: polymarket [OPTIONS] <COMMAND>

Commands:
  markets  Interact with markets
  events   Interact with events
  clob     Interact with the CLOB
  wallet   Manage wallet and authentication
  ...

Options:
  -o, --output <OUTPUT>  [default: table] [possible values: table, json]
  -h, --help

polymarket has no --schema flag. Agents can still discover and use it because the entry point works.

Discovery layers are orthogonal. Both dimensions matter. The entry point is the prerequisite.


The Two argparse Anti-Patterns

add_subparsers() defaults to required=False. On bare invocation, argparse returns silently with no output — the caller receives Namespace(command=None) and no error. This is itself a failure for agents, who expect a command list on stdout.

Anti-pattern 1 — default: returns silently with no output

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command")  # required=False by default
args = parser.parse_args()
# bare invocation: parse_args() returns Namespace(command=None), prints nothing
# calling args.func(args) here raises AttributeError — no set_defaults handler exists

Argparse prints nothing and returns to the caller. The agent receives no command list, no usage. If the dispatch code then calls args.func(args) without a registered default, the program crashes with AttributeError. Either way, discovery fails — silently or noisily, depending on the dispatch code, which is harder to diagnose than an explicit error.

Anti-pattern 2 — explicit required=True: exits 2

parser = argparse.ArgumentParser()
subparsers = parser.add_subparsers(dest="command", required=True)
# bare invocation → exit 2, "the following arguments are required: command"
# (lowercase — argparse uses the dest value in the error message)

A deliberate choice to force users to provide a subcommand. Reasonable for human callers; fatal for agents. The non-zero exit causes the agent to classify the tool as broken before discovering a single command.

Both break the entry-point contract. The fix addresses both.


The Fix for CLI Authors

argparse

Both changes are required. required=False alone silently does nothing.

import sys, argparse

parser = argparse.ArgumentParser(description="my-tool — brief description")
subparsers = parser.add_subparsers(dest="command", required=False)

# register subcommands here

def _root(args):
    parser.print_help()
    sys.exit(0)

parser.set_defaults(func=_root)

args = parser.parse_args()
args.func(args)

Click

Click's invoke_without_command=True provides this correctly:

@click.group(invoke_without_command=True)
@click.pass_context
def cli(ctx):
    if ctx.invoked_subcommand is None:
        click.echo(ctx.get_help())
        # exit 0 is implicit

clap (Rust)

clap's arg_required_else_help = true on the top-level command produces exit 0 with help when no subcommand is given:

#[derive(Parser)]
#[command(arg_required_else_help = true)]
struct Cli {
    #[command(subcommand)]
    command: Option<Commands>,
}

Cobra (Go)

Cobra calls the root command's Run or RunE when no subcommand is given. Set it to print help:

var rootCmd = &cobra.Command{
    Use:   "my-tool",
    Short: "Brief description",
    Run: func(cmd *cobra.Command, args []string) {
        cmd.Help()
    },
}

Structuring the Help Output

Exit 0 with a subcommand list is the floor, not the ceiling. The help output is the first screen a human reads — and often the last they consult before deciding whether the tool fits their workflow. Structure it to answer three questions in order: what does this do, where do I start, and where do I go next.

Group commands by role, not alphabet

Alphabetical ordering exposes implementation detail. Role-based grouping encodes priority. A CORE COMMANDS group signals "start here"; ADDITIONAL COMMANDS signals "you'll find this later"; HELP TOPICS signals "concepts, not commands." The grouping lets a reader form a mental model before reading a single description.

CORE COMMANDS
  issue:    Manage issues
  pr:       Manage pull requests
  repo:     Manage repositories

ADDITIONAL COMMANDS
  alias:    Create command shortcuts
  config:   Manage configuration

HELP TOPICS
  exit-codes:    Exit codes used by this tool
  formatting:    Formatting options for JSON output
  environment:   Environment variables that can be used with this tool

Provide curated examples, not abstract syntax

Abstract usage syntax (tool <command> [flags]) describes the grammar. Examples demonstrate the idiom. Provide 2–4 examples that span distinct domains and represent the most common real workflows — not a complete inventory.

EXAMPLES
  $ gh issue create
  $ gh repo clone owner/repo
  $ gh pr checkout 321

Three examples, three domains. A reader immediately grasps the scope of the tool without reading the full command list.

Make help topics first-class

Concepts like exit codes, output formats, and environment variables are often buried in man pages or online documentation. Listing them by name in the root help makes them discoverable in one invocation. An agent that sees exit-codes as a named topic knows to run tool help exit-codes before guessing. A human knows where to look before reaching for a browser.


Verification Checklist for CLI Authors

Three checks that must all pass before a CLI is considered agent-accessible:

Check Command Expected result
Bare invocation your-tool Exit 0, subcommand list on stdout
Clean environment HOME=/tmp/empty your-tool Exit 0, no files created, no network calls
Non-TTY pipe your-tool \| cat Help text still appears on stdout

The clean environment check is not optional — REQ-F-068 requires that the empty-invocation path follows the same purity guarantees as --help. No config directory creation, no schema migration, no credential check.


For Agent Builders

Agents cannot control whether the CLIs they use follow this spec. The defensive first-contact probe handles both the clean case and the error case:

import subprocess, re

def probe_cli(tool: str) -> dict:
    # Attempt 1 — bare invocation
    result = subprocess.run(
        [tool],
        capture_output=True, text=True,
        stdin=subprocess.DEVNULL,  # never inherit agent stdin (§50)
        timeout=10,
    )

    if result.returncode == 0 and result.stdout.strip():
        commands = re.findall(r'^\s{2,4}(\w[\w-]*)\s', result.stdout, re.MULTILINE)
        return {"ok": True, "commands": commands, "source": "bare"}

    # Attempt 2 — explicit --help flag
    fallback = subprocess.run(
        [tool, "--help"],
        capture_output=True, text=True,
        stdin=subprocess.DEVNULL,
        timeout=10,
    )
    if fallback.returncode == 0:
        commands = re.findall(r'^\s{2,4}(\w[\w-]*)\s', fallback.stdout, re.MULTILINE)
        return {"ok": True, "commands": commands, "source": "help_flag"}

    return {"ok": False, "commands": [], "source": "none"}

stdin=subprocess.DEVNULL must be set on every invocation — not just the probe. Without it, the subprocess inherits the agent's own stdin. Any tool that silently falls back to stdin (§50) will consume it, corrupting the agent's input stream.

Limitation: The --help fallback is not guaranteed. Some tools run credential or config checks before argument parsing and exit non-zero even on --help. If both probes fail, classify the tool as "unknown interface" and surface the failure to the caller rather than proceeding with guessed invocations.


For AI Agents

Use this decision table when encountering an unfamiliar CLI:

Step Command Exit 0 + output? Next step
1 <tool> Yes Parse subcommand list; try --schema
1 <tool> No Try step 2
2 <tool> --help Yes Parse subcommand list; try --schema
2 <tool> --help No Tool is not usable; stop
3 <tool> --schema --output json Yes, valid JSON Use as authoritative schema
3 <tool> --schema --output json No Fall back to per-command --help

Additional signals to watch for:

  • Exit 0 with no output on bare invocation: go directly to --help
  • A shell or repl entry in the subcommand list: REPL risk (§37) — never invoke these subcommands without a TTY
  • Non-zero exit on --help: tool runs initialization before argument parsing — treat as partially broken; try per-command <tool> <command> --help directly

Document Relationship
§21 Schema & Help Discoverability Enforces: bare-invocation exit 0 now a prerequisite to §21 scoring
§1 Exit Codes & Status Signaling Enforces: exit-code contract that bare invocation must satisfy
§52 Recursive Command Tree Discovery Cost Composes: schema export assumes entry-point works
§37 REPL / Interactive Mode Accidental Triggering Composes: shell subcommands visible in root help pose REPL risk
§50 Stdin Consumption Deadlock Provides: reason stdin=DEVNULL is required in the probe pattern
REQ-F-068 Enforces: empty-invocation purity — no side effects, exit 0
REQ-F-009 Provides: non-TTY detection that governs the empty-invocation path
research/argparse.md Sources: required-subcommand anti-pattern documented in framework research