Skip to content

[Bug]: openclaw <unknown-command> --help silently shows generic top-level help (exit 0); same command without --help correctly errors with Unknown command (exit 1) #81077

Description

@YB0y

Bug type

Behavior bug (incorrect output/state without crash)


Beta release blocker

No


Summary

openclaw <anything-that-is-not-a-real-subcommand> correctly exits 1 with a clear Unknown command: openclaw <name>. No built-in command or plugin CLI metadata owns "<name>" error. But the same invocation with --help appended silently falls through to the generic top-level help and exits 0, never surfacing that the user's chosen command name doesn't exist. Reproduces on v2026.5.10-beta.1 (today's npm beta) and the same code path exists on v2026.5.7 (today's stable).

The asymmetry is in src/cli/run-main.ts:369-377 — the unknown-primary detection short-circuits whenever invocation.hasHelpOrVersion is true, deferring to the standard help handler instead of erroring like the no---help case does.


Steps to reproduce

  1. On v2026.5.10-beta.1 (9c7e67b0f8) or later. Same code path on v2026.5.7 per git show v2026.5.7:src/cli/run-main.ts.

  2. Run any non-existent subcommand:

    pnpm openclaw heartbeat; echo "exit=$?"
    pnpm openclaw heartbeat --help; echo "exit=$?"
  3. Observe the asymmetry:

    • First form: prints Unknown command: openclaw heartbeat. …, exits 1. Correct.
    • Second form: prints generic top-level help (Usage: openclaw [options] [command] etc.), exits 0. Incorrect.

Same result for any unknown name (zzz-not-a-real-command, definitelynotacommand, etc.).


Expected behavior

Either:

  1. Reject consistently. openclaw <unknown> --help should print the same Unknown command: openclaw <unknown>. No built-in command or plugin CLI metadata owns "<unknown>". error and exit 1, regardless of whether --help is present.
  2. Reject + steer. Print the Unknown command error AND then optionally show the top-level help below it as guidance. Still exit 1 so scripts can detect the failure.

Either is consistent with the no---help behavior. The current state silently misleads users who typed a guess to discover whether a command exists.


Actual behavior

Verbatim capture on v2026.5.10-beta.1 (152ea9af34):

$ pnpm openclaw heartbeat
[openclaw] Could not start the CLI.
[openclaw] Reason: Unknown command: openclaw heartbeat. No built-in command or plugin CLI metadata owns "heartbeat".
[openclaw] Debug: set OPENCLAW_DEBUG=1 to include the stack trace.
[openclaw] Try: openclaw doctor
[openclaw] Help: openclaw --help
[ELIFECYCLE] Command failed with exit code 1.
exit=1                                  ← correct

$ pnpm openclaw heartbeat --help
🦞 OpenClaw 2026.5.10-beta.1 (152ea9a) — All your chats, one OpenClaw.

Usage: openclaw [options] [command]
Options:
  --container <name>   …
  -h, --help           Display help for command
  …
exit=0                                  ← buggy: no "Unknown command" error, looks like heartbeat is a real thing

Source trace on v2026.5.10-beta.1:

  • src/cli/run-main.ts:369-377 contains the unknown-primary check that emits the "Unknown command" message:

    if (
      invocation.hasHelpOrVersion ||   // ← bails out when --help or --version is present
      !primary ||
      primary === "help" ||
      isReservedNonPluginCommandRoot(primary) ||
      isKnownBuiltInCommandRoot(primary)
    ) {
      return null;
    }

    Returning null here lets the standard help/version handler take over, which prints generic top-level help and exits 0.

  • src/cli/run-main.ts:385-400 (resolveUnownedCliPrimaryMessage) is the function that produces the "Unknown command: openclaw " message that the no---help case correctly emits. It's only reached when the gate at line 369 doesn't bail out.

So the defect is intentional in the sense that --help was given priority — but the resulting UX is wrong: users typo a command, append --help to learn how it works, and OpenClaw silently shows the global help without acknowledging that the typo'd name doesn't exist.


OpenClaw version

Reproduces on:

  • v2026.5.10-beta.1 (today's npm beta; commit 9c7e67b0f8). Verified live above.
  • v2026.5.7 (today's npm stable). Source identical: git show v2026.5.7:src/cli/run-main.ts contains the same invocation.hasHelpOrVersion short-circuit at the same logical position.

Operating system

Ubuntu 24.04 (Linux 6.8.0-110-generic). OS-agnostic — pure CLI argv-parsing defect.


Install method

pnpm openclaw … from source checkout. Reproduces on any install path (npm install -g openclaw, openclaw update, etc.).


Model

Not applicable — pure CLI dispatch defect; no model dispatch.


Provider / routing chain

Not applicable.


Additional provider/model setup details

Not relevant. Any default-shape config.


Logs, screenshots, and evidence

Full evidence log saved at qa-reports/11-unknown-command-help-silent-fallthrough/v2026.5.10-beta.1-evidence.log:

openclaw heartbeat --help                 exit=0   (buggy)
openclaw heartbeat                        exit=1   (correct)
openclaw zzz-not-a-real-command --help    exit=0   (buggy)
openclaw zzz-not-a-real-command           exit=1   (correct)

Verifiable in two commands:

$ openclaw heartbeat --help; echo "exit=$?"
…generic help…
exit=0

$ openclaw heartbeat; echo "exit=$?"
[openclaw] Could not start the CLI.
[openclaw] Reason: Unknown command: openclaw heartbeat. …
exit=1

Impact and severity

  • Affected: anyone who types openclaw <name> --help to discover whether a command exists. Particularly painful when users have heard of a feature (like "heartbeat" — which is a real openclaw config concept but not a CLI subcommand) and try to find the CLI surface for it.
  • Severity: Low–Medium. UX / discoverability defect, not blocking. But mis-direction is the worst kind of help-system bug — users who get back the generic help conclude the command exists and they just missed the right docs page.
  • Frequency: 100% on any unknown name + --help.
  • Concrete consequences:
    • Users typing openclaw heartbeat --help (because heartbeat config exists and they reasonably guess a CLI exists too) get top-level help, conclude heartbeat is a hidden / undocumented command, and waste time digging.
    • Shell-completion / discovery scripts that probe openclaw <name> --help to enumerate valid commands get a false positive on every unknown name.
    • Tab-completion users who fat-finger a command and tack on --help to recover get no useful signal that they mis-typed.

Why this isn't already fixed and isn't a duplicate

  • Source-gate is explicit. The invocation.hasHelpOrVersion short-circuit at src/cli/run-main.ts:369 is intentional code; it just produces the wrong UX. Not a missing-check or oversight.
  • Distinct from #73077 (CLOSED: "parent CLI commands exit 1 when invoked without a subcommand (memory, channels, plugins, approvals, devices, cron, mcp)"). That issue was about real parent commands without a subcommand. Mine is about fake commands with --help.
  • Distinct from #62356 (CLOSED: "CLI enters CPU 100% infinite loop on unknown/unregistered subcommands (v2026.4.5)"). That was a CPU loop on unknown commands; my case is silent fall-through.
  • Distinct from #70349 (CLOSED: "TUI entry point UX: openclaw chat silently does nothing, …"). That was about a real but mis-behaving subcommand; mine is about completely-unknown names.
  • Dedup search (open + closed): openclaw unknown command --help exit code, unknown subcommand --help falls through, cli unknown command help silent, openclaw "Unknown command" help, openclaw cli help unknown command. No existing issue matches the asymmetric --help vs no---help behavior on unknown commands.

Suggested fix sketch

Two reasonable shapes:

  1. Smallest: remove the --help/--version early-exit from the unknown-primary check. In src/cli/run-main.ts:369-377, drop invocation.hasHelpOrVersion || from the bail-out condition so the unknown-primary detection runs regardless of --help. Then in the dispatching code, when invocation.hasHelpOrVersion is true AND the primary is unknown, emit the Unknown command error and exit 1 (same as the no---help path does today).

  2. Medium: print the error AND show top-level help below it. Even better UX — operator sees both "your command doesn't exist" and "here are the real ones." Exit 1 so scripts still detect the failure.

Either fix is a 5-10 line change. Option 2 is the most user-friendly but option 1 is the minimum.

Regression test:

it("openclaw <unknown> --help errors instead of silently falling through to top-level help", async () => {
  const { stdout, stderr, exitCode } = await runCli(["heartbeat", "--help"]);
  expect(exitCode).toBe(1);
  expect(stderr + stdout).toMatch(/Unknown command: openclaw heartbeat/);
});
it("openclaw <unknown> still errors (regression)", async () => {
  const { exitCode } = await runCli(["heartbeat"]);
  expect(exitCode).toBe(1);
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions