---
title: CLI Feedback Submission (file bugs / feature requests from external AI)
---

# CLI Feedback Submission (file bugs / feature requests from external AI)

## What it is

Omniscio has a **bug-icon toolbar button** ("Send Feedback") that opens a small dialog where you pick a category — **Bug**, **Feature**, **Feedback**, or **Question** — type a description, optionally drop in screenshots, and optionally leave a contact email — leave it blank and Omniscio fills it in from the account you are signed in with, so a report always carries an address the developer can reply to. Hitting "Send Bug Report" / "Send Feature Request" / etc. runs the payload through the shared **report-delivery chain** ([report-delivery.ts](../../src/main/services/bug/report-delivery.ts)), which sends to **Resend** (an email to the Omniscio developer) and/or **Firestore** (the developer's bug-report database) according to the `bugReportTransport` setting (default `both`). Both stop when the user turns off **Email bug reports & feedback** (RT-F024): nothing leaves the computer, and the report is kept locally and re-sent automatically once the switch is back on (or once the failing leg recovers) — no resend needed from you. Device / version diagnostics (app version, Electron version, OS, arch, Node version, current active-session count, status breakdown, recent error log lines, a **recent-crashes summary from the last 48h** — crash-signature log lines + crash-evidence files, scrubbed and bounded, shown only when the app has actually crashed, see [recent-crashes-contract.md](/.claude/memory/contracts/recent-crashes-contract.md), process uptime) are auto-collected and attached so the report is actionable without follow-up. The report counts as delivered if **any** leg succeeds — email or Firestore alone; only if **every** leg fails is it kept in the `<userData>/bug-reports/` outbox, so it is never silently lost, and the hourly outbox drain retries it later automatically.

That same flow is now reachable over the `127.0.0.1:19519` localhost HTTP control server — `POST /feedback` — so an external AI agent (Claude Code in another project, ChatGPT with the [omniscio-control skill](/.claude/skills/omniscio-control/SKILL.md), a `curl` script, the in-app sandboxed Claude session) holding the Omniscio bearer token can file a bug report on the user's behalf without piping the form through the renderer.

## Where to find it

The way in is a **bug icon in the app's toolbar** — the **Send Feedback** button — which opens a
small dialog where you choose a category, write the description, and optionally attach screenshots.
There is no sidebar entry and no settings page for this; the button is the whole surface. The rest
of this page describes how to reach the same flow without touching the interface at all, which is
only useful if you are an AI agent or writing a script.

## How it behaves

Sending feedback is a single one-shot action: pick a category, write the description, attach
anything useful, and press send. Nothing is saved as a draft and nothing is queued for later on
your end — the report goes out on the spot. If every leg rejects it, Omniscio keeps it in a local
outbox rather than silently losing it, and an hourly background drain retries it automatically —
you do not need to send it again. Because the destination is the developer's inbox rather than
your own data, the send is not put behind an approval prompt the way a destructive action would be.

## For agents

### How to use it

| Method | Path                   | Approval-gated? | Status   | Purpose                                                                                                                                          |
| ------ | ---------------------- | --------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| `POST` | `/feedback`            | **No**          | `200 OK` | Submit a bug / feature / feedback / question — runs the shared report-delivery chain (email, Firestore) immediately. Mirrors the bug-icon UI. |
| `POST` | `/feedback/with-video` | **No**          | `200 OK` | The same report with ONE **full-length video** attached by share link — see below.                                                               |

**Gating model.** Apply-immediately (NOT approval-gated). Trade-off the user explicitly chose on 2026-05-11: the "file this bug for me" flow needs to be fast, and the receiving inbox is the developer's, not a destructive operation on the user's data. The shared **10-mutations-per-minute** rate-limit bucket caps blast radius if a session goes off the rails.

**Auth.** Standard CLI bearer token in `Authorization: Bearer <token>` (or `?token=<token>` query param). The dispatcher returns `401 Unauthorized` on missing/invalid token before the route handler runs — no body validation happens for unauthenticated requests. **A spawned agent's scoped `agent-session` token is also admitted here** (both routes opt into it), so an agent can file a report with the `$AMC_CLI_TOKEN` it already holds rather than reading the full-trust token. That is a deliberate widening — the receiving inbox is the developer's, and recipients are chosen server-side, so a leaked agent token can at most mail the developer.

**Body size.** `POST /feedback` accepts a request body up to ~8.5 MB, so the schema's own **8 MB combined attachment budget** actually fits through the transport. (The cap is derived from that schema budget and kept under the cloud relay's 10 MB ceiling; it used to inherit the server's 1 MB default, ~8× tighter than the schema it carried.) `POST /feedback/with-video` sends only a path or an id, so it stays under the 1 MB default.

**Body schema** — mirrors the in-app `feedbackSendSchema` exactly so a fixture from the IPC transport works on the CLI transport:

```json
{
  "type": "bug" | "feature" | "feedback" | "question",
  "message": "string, 1–10000 chars, required",
  "email": "user@example.com (optional — omit it and the signed-in account email is used; valid RFC email or empty string)",
  "screenshots": [ /* optional, max 5 attachments */ ],
  "diagnostics": { /* optional — server auto-collects if omitted */ }
}
```

**Diagnostics auto-collection.** If the caller omits the `diagnostics` field, the server collects them itself (`collectDiagnostics()` — the same call the in-app dialog makes when you click the bug icon) and attaches them to the outgoing email. The receiving inbox always has device/version context regardless of whether the agent took the trouble to gather it. Caller-supplied diagnostics win — if the field is present, no server-side collection happens, so an agent that wants to override (e.g. include a custom `recentErrors` filter) can.

**Response shape.**

- `200 OK` → `{ "ok": true, "correlationId": "<uuid>", "delivered": ["email"] }` — `delivered` names the ONE leg that confirmed delivery (`"email"` or `"firestore"`). With both email and Firestore on, the email goes out in the background and is not named here.
- `400 Bad Request` → `{ "ok": false, "error": "<field>: <reason>; …" }` — validation failed (missing/empty message, unknown type, malformed email, message over 10000 chars, etc.).
- `401 Unauthorized` → `{ "ok": false, "error": "unauthorized" }` — missing or wrong bearer token (dispatcher-level gate).
- `429 Too Many Requests` → `{ "ok": false, "error": "Rate limit exceeded (10 mutations/minute)" }` — rolling 60-second mutation cap hit.
- `202 Accepted` → `{ "ok": false, "queued": true, "correlationId": "<uuid>", "error": "<message>" }` — every leg failed, or sending is turned off, but the report was safely kept on this computer. The hourly outbox drain retries it automatically, so the agent should NOT resend — the `error` text says sending is off when that is why it was kept, otherwise it is the generic "saved locally and will retry" copy. A repeated `X-Client-Request-Id` replays this exact 202 rather than queuing a second copy.
- `502 Bad Gateway` → `{ "ok": false, "queued": false, "error": "Feedback could not be delivered or saved. Please retry." }` — the report could neither be sent NOR saved (every leg failed AND the local write also failed) — genuinely lost. Not cacheable, so a retried request re-attempts for real.

### Examples

```bash
# Read the bearer token from Omniscio's auto-delivered file
TOKEN=$(<~/.amc/cli-token)

# 1. Minimal bug report — server auto-attaches diagnostics
curl -X POST http://127.0.0.1:19519/feedback \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bug",
    "message": "After archiving a session via middle-click, the sidebar list does not refresh until I press F5."
  }'
# Returns: { "ok": true }

# 2. Feature request with a contact email
curl -X POST http://127.0.0.1:19519/feedback \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "feature",
    "message": "It would be useful to filter the inbox by tag.",
    "email": "user@example.com"
  }'

# 3. Question (no contact email, agent-collected diagnostics)
curl -X POST http://127.0.0.1:19519/feedback \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "question",
    "message": "How do I rebind Ctrl+T to skip blank-session reuse?"
  }'
```

### Attaching a full-length video

`POST /feedback/with-video` files the same report with one video attached. The video does **not**
travel inside the request — Omniscio publishes it as a never-expiring video share and appends the
link to the report body, so both delivery legs carry it and a multi-minute recording attaches fine
even though it is far larger than any attachment limit. The upload runs inside the request and gets
about four minutes; a video too large to upload in that time is refused with `502` (the error says
it ran out of time), and nothing is published or filed.

Name the video **exactly one** of two ways. Sending both, or neither, is a `400`:

| Field         | Use it for                                                                                                                                                                                        |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `recordingId` | A recording already in the Omniscio library (e.g. one the user just made). Resolved server-side from the library row, so no caller-supplied path at all.                                          |
| `videoPath`   | A video file on this machine. A scoped token may only read inside the share allow-list — an active session workdir, `~/Claude`, or `<userData>/published-pastes` (the refusal names the exact roots). |

`videoTitle` is optional and labels the share. The recording must be **type `video`** and finished
baking — a screenshot snip, or a recording still transcoding, is refused rather than published as a
broken link.

```bash
TOKEN=$(<~/.amc/cli-token)

curl -X POST http://127.0.0.1:19519/feedback/with-video \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "type": "bug", "message": "<what the user said>", "recordingId": "<library id>" }'
```

**Read the response.** On success it carries `videoShareUrl` (now in the report) and
`videoShareToken` — the **only** handle for taking the share down, since it never expires. Tell the
user they can revoke it with that token.

**A refused publish files NOTHING.** The whole request fails so the user is never told they sent
evidence they did not. Fix the cause and send again rather than silently falling back to the
text-only route.

**With sending turned off, nothing is published.** If the user has turned off **Email bug reports &
feedback**, the route answers `403` before publishing: the recording is never made public and
nothing is filed. A public share is one more way a report leaves the computer, so the same switch
governs it.

Route-specific outcomes beyond the shared ones: `400` when the recording is not a finished video,
the path is outside the allow-list (`outside-allowlist`, with `roots`), or both/neither source is
given; `422` when the app is signed out (the caller authenticated fine — the app's state is wrong);
`500` when something failed *after* the share was published, in which case the token is still
returned so the orphan stays revocable.

### Guidance for agents

- **Use the bug-icon flow only when the user has explicitly said "file a bug" / "send feedback" / "report this to the developer."** Agents should not file unsolicited bug reports — every send is a real email to the developer's inbox.
- **Quote the user's own description in `message` verbatim** when possible. Resist paraphrasing or "improving" what the user said — the developer needs the raw words to diagnose.
- **Default to `type: "bug"`** for problem reports, `"feature"` for "it would be nice if…", `"feedback"` for general opinions about the app, `"question"` for "how do I…".
- **Do NOT include the bearer token, the user's API keys, or credentials in the `message` field.** Diagnostics are scrubbed by `collectDiagnostics()` — the message field is not.
- **The `screenshots` array maxes at 5 attachments**, together at most about 6 MB of real file (the schema caps their base64 at 8 MB combined); the in-app dialog refuses a file that would go over that before you press Send. If you're submitting from a script, encode as base64 dataURL per the in-app pattern; if you don't have screenshots, omit the field entirely.

## Related

The two neighbours most worth knowing are where Omniscio's own logs live, which is usually the
first thing to consult before filing anything, and the startup trace you can paste into a
slow-startup report. Both are named in the list below. The page also sits within the wider CLI
surface, so if what you want is to drive Omniscio over HTTP generally rather than just file a
report, start from [CLI control](cli-control.md).

### Boundary with bug-report-intake

This page covers the **outbound** "send a bug to the Omniscio developer" path — exposed at `POST /feedback`, dispatched via the shared report-delivery chain (email, Firestore), no per-project routing.

The **inbound** "tester emails a bug to MY project's intake address" path (subject `[BUG: <slug>] <title>`, parsed and routed to the matching project's Claude session) is a separate feature documented at [bug-report-intake.md](bug-report-intake.md). The two share no code and do not interact.

### See also

- [logs-and-debugging.md](logs-and-debugging.md) — where Omniscio's local logs live; useful to consult before filing a bug
- [startup-trace.md](startup-trace.md) — the self-contained `startup.log` you can paste into the bug body for slow-startup reports
- [`.claude/memory/cli-server-gating.md`](../../.claude/memory/cli-server-gating.md) — full CLI endpoint gating reference
- [`tests/unit/cli-server-feedback.test.ts`](../../tests/unit/cli-server-feedback.test.ts) — 43 cases across `/feedback` and `/feedback/bug-report` covering auth gating, validation, happy path, diagnostics auto-attach + caller-supplied preservation, delivery outcomes (200 delivered / 202 kept-and-queued / 502 genuinely lost), the kept-202's sending-off-vs-will-retry error text, `X-Client-Request-Id` replay (delivered AND kept), concurrent-duplicate coalescing, and rate-limit
- [`tests/unit/services/cli-feedback-routes.test.ts`](../../tests/unit/services/cli-feedback-routes.test.ts) — the body cap (a >1 MB attachment is accepted; over the cap still 413s), and the video route's promises: the link reaches the delivered message, a refused publish files nothing, a screenshot is refused, the share token comes back even when a later step throws, a retry never publishes twice, and an agent token is both admitted and confined
- [`.claude/memory/contracts/feedback-video-attachment-contract.md`](../../.claude/memory/contracts/feedback-video-attachment-contract.md) — the binding contract for the video route
