Skip to content

Epic: make the CLI safe and predictable for agents and automation #340

Description

@leggetter

Why

hookdeck gateway mcp is deliberately read-only (#228): the bet is that agents use skills + the CLI for anything write-shaped or listen-shaped, and MCP only for investigation. That bet only pays off if the CLI itself is safe to drive without a human.

v2.5.0 fixed the acute cases — #332, #333, #334, #335, #337, #338, #339. This epic tracks the rest, found in a full sweep of every interactive, TTY-dependent and output-related surface. Roughly two thirds of the failure modes live outside pkg/listen, and most are not about TTY detection at all.

The CLI already gets this right in two places — pkg/listen/source.go:68 and pkg/ansi/ansi.go:190-197 — it just is not applied systematically.

Design rule

A prompt must never be the only path. On a non-terminal: reads pick the safe default, writes fail loudly. Never silently succeed differently. Rendering degrades rather than dies. Data to stdout, diagnostics to stderr. Exit codes tell the truth.

Inventory

Output contract

  • Errors go to stdout for every command except gateway mcp. pkg/cmd/root.go:97,103,121,131,136 are bare fmt.Println(err). 2>/dev/null hides nothing, and errors interleave with --output json. The stderr contract exists today only as an MCP special case.
  • --output is never validated. --output yaml, --output JSON silently fall through to human text. 40 hand-rolled output == "json" branches, no shared helper (see #220 §2).
  • No mutation command emits JSON — delete, pause, unpause, retry, cancel, mute, project use, whoami. Automation must parse English.
  • Inconsistent JSON coverage: transformation count has --output json; source count, destination count, issue count do not.
  • --output name collision: a rendering mode on listen (default interactive), a serialization format everywhere else (default "").
  • renderer_simple.go mixes streams: event lines, ERROR: and warnings all go to stdout while its spinners go to stderr, so listen --output compact | jq gets diagnostics in the data stream.

Related: #279 is the spike for the io.Writer boundary this needs.

Interactive surfaces without a non-interactive path

  • project usesurvey.Select at pkg/cmd/project_use.go:102-110 with no TTY guard and no flag escape. Errors out with a survey error rather than saying what to pass.
  • listen source selection — of the three survey paths in pkg/listen/source.go, only the first (line 68) is guarded. selectSources (166-176) and createSource (204-212) are not. Also os.Exit(0) at lines 80 and 87 — declining a prompt reports success.
  • loginPress Enter + fmt.Fscanln (pkg/login/client_login.go:108, :253) blocks forever on a held-open pipe. login -i calls term.ReadPassword on os.Stdin unconditionally (pkg/login/interactive_login.go:129-138) and cannot fall back to the bufio path it already has.
  • CanOpenBrowser (pkg/open/open.go:34-50) returns true unconditionally on darwin and windows, so headless macOS and Docker-for-Mac are told they can open a browser.

Configuration inputs

  • Exactly one user-facing env var (HOOKDECK_API_KEY), and until v2.5.0 it was honoured by exactly one command. No env var for project, profile, output format, or non-interactive mode.
  • HOOKDECK_CONFIG_FILE is undocumented — its comment (pkg/config/config.go:439) calls it an acceptance-test affordance, yet it is currently the cleanest way to run the CLI without touching shared state.
  • A global --non-interactive / --quiet, and NO_COLOR support (only CLICOLOR/CLICOLOR_FORCE are honoured today).
  • InitConfig uses log.Fatalf x10 (pkg/config/config.go), emitting timestamped, prefixed logrus lines instead of plain error messages.

Documentation

Prior art in this repo

  • #279 — structured output spike (UserOutput with Out/Err io.Writer, TTY vs NonInteractive modes). Closest existing design work; still a spike.
  • #228 — MCP RFC, where the CLI-for-building / MCP-for-investigating split is stated.
  • #220 — gateway command UX and the --output json duplication.
  • #129 — key type consistency.
  • #88 — local vs global config precedence, agreed in 2024 and never implemented; it is the mechanism automation relies on for isolation.
  • #86 — a 2024 report of the silent guest fallback in Docker, left unresolved. listen ignores HOOKDECK_API_KEY and silently creates a guest account #334 was the same bug.

Suggested sequencing

  1. Output contract (stdout/stderr split, --output validation) — unblocks everything else and is what #279 already scopes.
  2. Remaining interactive surfaces (project use, listen source selection, login).
  3. Env var and global flag coverage.
  4. JSON for mutations.
  5. User-facing automation guide.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions