# How to collect feedback for a CLI tool

> A command-line tool collects feedback with an explicit feedback subcommand that posts the typed message to the Escuta Produto REST API. Send the CLI version and operating system as metadata, use exit codes that scripts can read, and never send files, environment variables or command history.

Source: https://escutaproduto.com/resources/collect-feedback-cli-tool
Last updated: 2026-10-11

## Why a CLI should send feedback only on request

A command-line tool is used in a terminal, often inside scripts and continuous integration jobs. A feedback prompt that appears during a build breaks the build, and a tool that sends data without asking breaks trust. The safe pattern is an explicit subcommand, such as `mycli feedback`, that sends exactly what the user typed and nothing else.

The subcommand posts a JSON body to the REST API with `fetch`. The [REST API reference](/docs/api) lists the fields, and a saved report returns `201` with the new item `id`. Because the user chose to run the command, the report is a deliberate act, and the tool can keep the rest of its behavior unchanged.

## Add a feedback subcommand that takes the message

Most tools already parse their arguments. Add one branch for the feedback command, and let the message come from the remaining words. A short message on the command line is the fastest path, and an interactive prompt covers the case where the user types the command alone:

```js
#!/usr/bin/env node
const readline = require("node:readline/promises");

async function promptForMessage() {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  const answer = await rl.question("What should the maintainers know? ");
  rl.close();
  return answer.trim();
}

async function feedback(args) {
  let message = args.join(" ").trim();

  if (message.length === 0 && process.stdin.isTTY) {
    message = await promptForMessage();
  }

  if (message.length < 2) {
    console.error("Usage: mycli feedback <message>");
    console.error("For a longer note, use https://escutaproduto.com/f/your-product-slug?lang=en");
    return 2;
  }

  return send(message);
}

const [command, ...rest] = process.argv.slice(2);
if (command === "feedback") {
  feedback(rest).then((code) => {
    process.exitCode = code;
  });
}
```

The check on `process.stdin.isTTY` matters. When the command runs in a script with no terminal attached, it returns the usage error instead of waiting for input that will never arrive.

## Post the report with version and system details

The `send` function builds the body and reports the outcome as an exit code. Read the version from the tool's own package file, so it always matches the release the user installed:

```js
const os = require("node:os");
const pkg = require("../package.json");

const API_URL = "https://escutaproduto.com/api/v1/feedback";
const PUBLIC_KEY = "pk_your_product_key";

async function send(message) {
  let response;
  try {
    response = await fetch(API_URL, {
      method: "POST",
      headers: { "content-type": "application/json" },
      body: JSON.stringify({
        key: PUBLIC_KEY,
        kind: "other",
        message: message,
        metadata: {
          cliVersion: pkg.version,
          platform: process.platform,
          osRelease: os.release(),
          arch: process.arch,
          nodeVersion: process.version,
        },
      }),
    });
  } catch (error) {
    console.error("Could not reach the feedback service. Check your connection and try again.");
    return 1;
  }

  if (response.status === 201) {
    console.log("Thanks, your feedback was sent.");
    return 0;
  }
  if (response.status === 429) {
    console.error("Too many reports from this network. Try again in a minute.");
    return 1;
  }
  console.error("Feedback was not sent (status " + response.status + ").");
  return 1;
}
```

Each user's requests leave from their own IP address, so the limit of 10 requests per minute per IP and product rarely matters for one person. An office network or a shared lab can add up, which is why the 429 message tells the user to wait rather than retry in a loop.

## Keep feedback opt-in and separate from telemetry

Explicit feedback is one thing. Automatic reporting, such as crash data or usage counts, is another, and it needs its own consent. Keep them apart in the code and in the documentation. Ask once, with a default of no, and store the answer in the tool's config file. The feedback subcommand does not depend on that answer, because the user already decided to send a message by typing the command.

A short first-run question makes the choice visible, for example "Send anonymous crash reports? (y/N)". Users who say no should never be asked again, and the README should say what each option sends. Tools that people trust tend to explain their data handling in one paragraph.

## What should a CLI never send with feedback?

Terminals hold a lot of sensitive material. A feedback report should contain the message and a few facts about the tool and the machine. The table below sets the line:

| Send | Never send |
| --- | --- |
| The message the user typed | Shell history or previous commands |
| CLI version, platform and OS release | Environment variables, tokens or keys |
| Architecture and Node version | File paths, file contents or project names |

If a bug depends on a file, the user can describe the file in the message. For a CI pipeline, ask the team to write the message by hand instead of having the tool read logs and attach them. Logs often include secrets that were never meant to leave the build.

## What do scripts and users see when sending fails?

Exit codes make the command usable in scripts. Use 0 for a saved report, 2 for a usage error such as an empty message, and 1 for any other failure. A script can check the code and decide whether to retry later, and a person sees a short line that explains what happened.

Do not loop on failure. A tool that keeps retrying while a user waits feels stuck. A single attempt with a clear message, plus the hosted form link for a longer note, respects the user's time. The form works in a browser on any machine, so a user with a flaky connection can finish the message later.

## Turning on the feedback command in Escuta Produto

Create a product for the tool and copy the public key into the code. Run the command from a development build and send one message. Check that the CLI version and platform appear in the inbox, then run the command with no arguments in a pipe to confirm it exits with code 2 instead of waiting. Connect the Slack or Discord webhook for alerts, so the maintainers see new reports without checking the dashboard.

The [notifications guide](/docs/notifications) covers the webhook. For how the team should sort what arrives, read the [triage guide](/resources/how-to-triage-customer-feedback). Open-source maintainers will find the [open-source article](/resources/collect-feedback-open-source) useful for pointing users toward the right channel, and the [developer tools article](/resources/collect-feedback-developer-tools) covers reports that come with error codes.

## Frequently asked questions

### Should a CLI tool ask for feedback automatically after a command runs?

No. A prompt in the middle of a command interrupts the user and breaks scripts and CI jobs. Offer an explicit feedback subcommand instead, and keep it separate from any automatic reporting that the user has agreed to.

### Which exit code should the feedback command return on failure?

Return 0 when the report is saved, 2 for a usage error such as an empty message, and 1 for any other failure. Scripts can then check the result without parsing the text.

### Does feedback from a CLI tool need to include the command history?

No. Command history can contain secrets and private paths. Send only the message the user typed, along with the tool version, platform and architecture.
