All posts
·
9 min read

A CLI Should Do More Than Wrap Your API

  • Developer Tools
  • CLI
  • Product Engineering

If your CLI is just a repeat of your API with flags, you did not build a CLI. You built a second client for your API.

That is the line I keep coming back to while building Hookie: multiplayer webhooks for the AI-native dev workflow. One URL receives the webhook, then broadcasts it to every environment you and your agents work in.

This is fine in the beginning. When a product is young, you may not know which workflows matter yet, and a thin command for each endpoint can help internal teams test the API surface, run endpoint checks without writing scripts, and avoid building product decisions into a tool too early. The problem starts when that thin layer becomes the product.

For many devtools products, the CLI is one of the primary interfaces. Some users install it before they understand the dashboard, put it in CI, paste commands into incident channels, or ask agents to run it. If that surface only mirrors the API, users inherit the API's shape without getting the API's flexibility.

The API is a capability layer. The CLI is an outcome layer.

API parity helps, but not enough

There is nothing wrong with exposing resource-shaped commands:

bash
hookie apps hookie apps --org-id org_123 hookie sources hookie sources app_123

Those commands are useful. They let users manage apps and sources on Hookie, give operators an escape hatch, make debugging possible when the happy path breaks, and map cleanly to docs, permissions, and support conversations.

But if this is all the CLI provides, the user still has to do the product design in their head.

They need to know which app or source maps to the service they are testing, whether the current repo has Hookie config, whether the command should use an authenticated source or an anonymous channel, which relay to connect to, where to forward events, and what UI or logs should stay open while a developer or agent debugs.

At that point, the CLI has not reduced the workflow; it has translated HTTP into terminal syntax.

Start from the task

Users usually open a CLI because they want to complete a task:

  • authenticate against the current Hookie app,
  • initialize webhook config for this repo,
  • find the source that belongs to this service,
  • receive webhook events in the current environment,
  • forward those events to the handler under test,
  • keep an event UI open while a developer or agent debugs,
  • switch between an anonymous test channel and an authenticated source.

Those are not endpoints; they are sequences.

A user who wants to test webhooks locally does not want to list apps, copy a source ID, open a relay stream, start a local forwarder, and then infer whether events are reaching the right handler.

The command should know the sequence: validate local config, infer the source when possible, fail early if auth or relay settings are missing, stream useful status, show the local UI URL when the listener is ready, and return a non-zero exit code when the setup cannot work.

That is product work, not a prettier API wrapper.

Compose primitives into workflows

A workflow command encodes the path users repeat. It can still use the same API underneath, and probably should; the distinction is not transport, but responsibility.

The API should expose primitives with stable semantics:

  • list apps,
  • list sources,
  • authenticate a user,
  • open a relay stream,
  • receive webhook events.

The CLI should compose those primitives around a job:

  • listen for webhooks in this repo,
  • forward events to my local handler,
  • keep the event UI open,
  • tell me what to fix when auth or relay config is invalid,
  • produce output that scripts and humans can both use.

This layer has opinions: it chooses defaults, decides which checks run before contacting the server, and defines what to hide, what to show, and what to name. Some teams avoid that work because a one-to-one wrapper feels safer and does not require product judgment.

But avoiding judgment does not remove the workflow; it pushes the workflow onto each user who hits that path.

When the sequence leaks

The weakest CLIs make users memorize backend order:

bash
hookie apps hookie sources app_123 hookie listen -t src_456 hookie listen -t src_456 -f http://localhost:3000/api/webhooks

This is not wrong because apps and sources exist. Those commands are normal and necessary; the problem is leaving the common path manual.

The docs usually reveal the problem. If a normal task requires a page that says "first run this, then copy the ID, then run this other command, then wait, then run this third command," the CLI probably lacks a workflow command.

Some workflows need multiple steps. The issue is not that sequence exists; it is that the CLI refuses to own a sequence the product already knows.

Keep resource commands

A better command can still expose the same underlying resources:

bash
hookie listen -f http://localhost:3000/api/webhooks --ui

The difference is where the work happens.

The ideal version of the CLI can resolve the app or source from hookie.yml, load local environment variables, and decide whether the listener should use an authenticated source or an anonymous channel. It can then connect to the relay, forward events, keep a local UI open, and print the state a user needs while debugging. Over time, it can also support --json for scripts and --dry-run for validation.

The user still gets escape hatches:

bash
hookie apps hookie sources app_123 hookie listen -t src_456 --debug

That is the shape I like: workflow commands for normal use, resource commands for discovery and inspection, and a raw API path for the rare case.

Use local context

CLIs are not SDKs with different clothes. An SDK runs inside a program, an API returns data to a caller, and a CLI sits in a shell, in a repo, in CI, or inside an agent run. That context changes the design.

Terminal-native tools use that context well: they read local config, write artifacts, emit searchable logs, pass structured output to other processes, and let an agent inspect a saved file instead of keeping a large response in context.

Hookie is the example I am working through. It does not need to be finished to show the pattern: the lower-level model has apps, sources, relay streams, and forwarding targets, while a thin CLI would expose those nouns and leave the user to connect them.

listen is the workflow command that should connect them:

bash
hookie init hookie listen -f http://localhost:3000/api/webhooks --ui

The command expresses the outcome: receive webhook events, forward them to a local service, and keep a local event UI open. The CLI loads a .env file from the current directory, can read a hookie.yml file from the current directory or a parent directory, and lets flags override file defaults. That makes it a product workflow adapted to the terminal, not just a different spelling of an endpoint.

The same idea applies outside webhook tooling. Source map upload commands should understand build directories, release metadata, CI variables, and cleanup. Database type generation should understand the linked project, the target language, and the local output file. Log inspection should understand the current project, the last failed run, and the local output format.

None of that appears if the CLI only mirrors endpoint names.

Thin wrappers are fine early

There is a fair defense of thin CLIs: they are cheap, honest, and broad. A generated or endpoint-shaped CLI gives a team broad coverage before it has workflow confidence, exposes new API capabilities without waiting for a full UX pass, serves power users who know the model and want exact control, and helps support engineers reproduce customer states.

I would not block a team from shipping that, but I would block them from stopping there if the CLI is a primary product surface.

The right question is not "do we expose every API capability?" The right question is "which user outcomes are important enough that the CLI should own the sequence?"

Early on, the answer may be "we do not know yet." Later, if support tickets, docs, and usage data keep showing the same five command sequences, the answer is no longer unclear.

Workflow commands cost more

A workflow layer creates drift risk. The API can change, the CLI can encode stale assumptions, and defaults can become wrong. A command that performs five API calls has more failure modes than a command that performs one, and once users automate a workflow command in CI, changing behavior becomes harder.

That is the cost of treating the CLI as a product surface.

The cost is still worth paying when the CLI is the main way users operate the product. If users listen, forward, debug, upload, or inspect through the terminal, the CLI owns part of the product experience whether the team admits it or not.

The safer approach is to design workflow commands with explicit contracts:

  • stable defaults,
  • clear exit codes,
  • --json for scripts,
  • --dry-run for validation,
  • useful logs for humans,
  • resource commands for escape hatches,
  • raw API access for debugging.

That gives users a high-level path without trapping them inside it.

Write the terminal session first

The practical rule is simple: start with the command a user wishes existed, not the endpoint, the database model, or the admin panel route.

For each important workflow, write the terminal session first:

bash
hookie login hookie init hookie apps hookie sources hookie listen -f http://localhost:3000/api/webhooks --ui

Then map those commands back to the lower-level model: auth, apps, sources, relay streams, forwarding, and local config.

This order forces better questions: what the CLI can infer, what the user must provide, what should be persisted locally, what should fail before network I/O, what should be human-readable by default, and what should be machine-readable with --json.

If you design from the API outward, you get coverage. If you design from the job inward, you get a tool.

A simple check

Here is the test I would use for any devtools CLI that users rely on:

Can a new user complete the top three workflows without reading the API docs, copying IDs between commands, or learning the backend resource graph first?

If the answer is yes, the CLI is doing product work. If the answer is no, the CLI may still be useful, but it is probably not finished; it is an API client with a terminal interface.

That distinction matters because a CLI is not valuable because it has commands; it is valuable because it removes operational work from the person running them.

API parity is useful, but workflow ownership is the point.


  • Developer Tools
  • CLI
  • Product Engineering
Made with ❤️ in 🇨🇦 · Copyright © 2026 Valentin Prugnaud
Foxy seeing you here!
Wondering if I'd fit your role?
Logo