Operate AgentFeedback
Run AgentFeedback on one machine with no server, or run a server for several machines; wire the harnesses, back up, upgrade and migrate. One binary and one SQLite file; a server adds one API key. Every step is a command an agent can run.
Contents: Local mode (no server) · Run locally (a server in Docker) · Bare binary (no Docker) · Canonical issues · Wire the harnesses · Build from source · Deploy to a host · Authenticate a client · Configuration · Backups · Restore and migration · Upgrading and downgrading · Key rotation · Retention · Monitoring · Uninstall
Authenticate a client
Keep authentication separate from harness wiring. Select exactly one key source:
--key-from-stdin (paste an existing key), --login EMAIL (email device
login), --agent (self-enrolment), or --enrol-token-from-stdin (an
enrolment token). For each, use agentfeedback init --server URL with the
selected flag and --harnesses none --yes --json. cloud still means
https://api.agentfeedback.dev. Then wire separately with
agentfeedback init --harnesses HARNESS --yes --json.
The human types keys or tokens privately on stdin and completes email confirmation themselves; never put secrets in argv, chat, logs or shell traces. See the hidden terminal prompt. Email, agent and token routes must be advertised by the server's capabilities; OSS supports paste only. If interactive discovery fails, the menu still offers paste; explicit hosted methods fail rather than guess. Unsupported methods exit 2. Device polling honors the advertised interval; codes, keys and verification URLs are not printed. Agent enrolment prints its claim URL once with its deadline and reduced-attribution notice; treat that URL as private and never log it.
Existing configuration is not automatically replaced. To explicitly refresh
a configured machine's key through email, run
agentfeedback login --email EMAIL --server cloud --json (or a server URL).
login also accepts an interactive email when --email is omitted.
The key is saved through the existing 0600 config writer, not a browser
session. doctor --init remains the paste-key configuration path;
doctor --init --url cloud --key-from-stdin saves the hosted service URL,
just like install --server cloud and migrate --to cloud.
Hosted credentials are saved before harness selection and wiring. If either
later step fails, fix that setup problem and rerun init without an
authentication flag; do not consume another enrolment token. Unset
AGENT_FEEDBACK_API_KEY before acquiring hosted credentials. A claim URL in
either successful or failed output is private; do not paste it into agent chat
or retain it in session transcripts.
Authentication does not change feature-target or session collection policy.
Run locally
A server on this machine, for trying the HTTP API or serving other machines; reporting from this machine alone needs none (Local mode). Needs Docker with Compose. From the repository root:
cd infra/agentfeedback
test -e .env || (umask 077 && printf 'API_KEY=%s\n' "$(openssl rand -hex 32)" > .env) # keeps an existing key
docker compose up -d --build --wait
curl --fail --silent --show-error --retry 60 --retry-all-errors --retry-delay 1 http://127.0.0.1:8090/ready
The service listens on 127.0.0.1:8090 and stores its database in the named
volume agentfeedback-data (/data/agentfeedback.db in the container). First request:
export AGENT_FEEDBACK_URL=http://127.0.0.1:8090
export AGENT_FEEDBACK_API_KEY=$(sed -n 's/^API_KEY=//p' .env)
curl -sS -H "Authorization: Bearer $AGENT_FEEDBACK_API_KEY" -H 'Content-Type: application/json' \
-d '{"kind":"friction","summary":"local smoke test","payload":{"category":"test"}}' \
"$AGENT_FEEDBACK_URL/api/v1/submissions"
curl -sS -H "Authorization: Bearer $AGENT_FEEDBACK_API_KEY" "$AGENT_FEEDBACK_URL/api/v1/submissions?limit=5"
The service answers the v1 contract, openapi.yaml; bash scripts/e2e.sh "$AGENT_FEEDBACK_API_KEY" "$AGENT_FEEDBACK_URL" from the repository root runs the whole contract suite against it (it creates rows).
Tear down with docker compose down -v (deletes the database volume; .env
stays).
Without Docker: go build -o bin/agentfeedback ./cmd/agentfeedback && API_KEY=dev bin/agentfeedback serve. It listens on 127.0.0.1:8090 with the database in the data directory, ${XDG_DATA_HOME:-~/.local/share}/agentfeedback/agentfeedback.db.
Local mode (no server)
agentfeedback doctor # mode: local and the database path
agentfeedback submit friction --summary "local smoke test"
agentfeedback list
With no url in the config file and no AGENT_FEEDBACK_URL, the base
submission and processing commands work in-process against
${XDG_DATA_HOME:-~/.local/share}/agentfeedback/agentfeedback.db, with no
server running; agentfeedback doctor prints mode: local and the database
path. --local forces local mode and --server URL targets a server for one
invocation; a configured url is never ignored without --local. backup <dest.db> and import default to the same file, and serve started later on
the same machine serves the same database, so the machine keeps one queue.
Several processes may use the database at once (SQLite in WAL mode: commands,
hooks, ui, a local serve); keep the data directory on a local filesystem,
never on a network share, where SQLite's locking does not hold.
A local write that fails (the database busy past its 5 s timeout, a full
disk) is spooled like a failed send to a server: outcome spooled, exit 0,
and the next client command that runs the start-up pass (every
local-mode command that opens the database but doctor, sessions and
ingest) delivers it, before its own work, as does agentfeedback flush. When the spool cannot be written either, the body
is echoed on stderr and the command exits 1. submit spools the same way,
with one stderr line naming the cause, when the local database does not open
because it is busy or locked past the busy timeout (reason
local_database_busy) or was written by a newer binary (reason
local_database_newer: upgrade this binary and the next command delivers
it). Any other open failure (a database without the AgentFeedback stamp, an
unwritable data directory) is an error outcome with the body echoed on
stderr, and the read commands report every open failure. Every spool entry records the
destination it was written for, the server URL or local: flush sends an
entry only to that destination and leaves the others in place, naming each
with the command that sends it (flush --server URL, or flush --local), so
a url that changes never carries pending reports to the new server and
local reports never reach a server.
Local mode or a server: what differs
The base client commands, init and install work in both modes; the skill,
the rule, the hooks and the triage skill they wire are the same, and the
stdio MCP entry differs only in its argument (mcp, or mcp --server <URL>). What differs, with the section that holds the detail:
- Where the reports are: the data-directory database, or the server.
When
initordoctor --initwrites a new server, they name the local reports and themigrate --local --to <URL> --to-key-from-stdincommand that copies them (Restore and migration); nothing copies reports back from a server on its own. - Session state (
sessions) stays in the data-directory database in both modes.sessions push, run manually or by the automatic sweep, sends eligible, scrubbed transcripts to a server whose organisation enables processing; local digest and push watermarks remain separate. - The file inbox is delivered by local-mode commands only, and with a
server by
agentfeedback ingestalone (File inbox). - Delivering the spool: with a server the end-of-turn hook sends due submissions; in local mode the hooks send nothing and the next command that runs the start-up pass delivers them (see the spool paragraphs above).
- MCP: both modes serve the same tools, the session tools included; with
a server
mcprelays the server's tools. No MCP call is spooled in either mode (MCP and discovery). - Canonical issues require the selected backend to advertise the operation; local mode and the bundled self-hosted server do not implement issue aggregation (Canonical issues).
- No binary: a harness or chat app that cannot run the binary files to
a server (its
/mcp,/skill?format=promptor the HTTP recipes), or drops a file into the inbox, which the binary delivers later on that machine.
Data directory
${XDG_DATA_HOME:-~/.local/share}/agentfeedback/ holds everything that is
the only copy of a report:
| Path | What |
|---|---|
agentfeedback.db (+ -wal, -shm) | the local-mode database, also the default of serve, backup and import; in both modes it also holds the session watermarks of agentfeedback sessions (tables sessions_seen and triage_state, see sessions.md) |
spool/ | submissions waiting to be delivered, one af1-*.json file each with its destination |
rejected/ | submissions the destination refused for good (kept 30 days) |
inbox/ | envelope files written by an agent that cannot run the binary, one *.json each (see below) |
inbox/done/ | inbox files filed, renamed <id>-<name> |
inbox/rejected/ | inbox files refused, each with a <name>.reason beside it |
The client creates the directory 0700 and the database files 0600
(SQLite gives -wal and -shm the database file's mode). A directory or
file that already exists keeps its mode: agentfeedback doctor reports every
path other users can reach with the chmod to run, and changes nothing. The
cache directory, ${XDG_CACHE_HOME:-~/.cache}/agentfeedback/, holds nothing
that is the only copy of a report: the client log (log/client.jsonl), the
hooks' per-session counters (sessions/) and last-run records (hooks/),
the filed/ markers and digest/ output; a spool left there by a
version before 4.0 is reported by doctor and is not read any more.
The same layout applies on every operating system, macOS and Windows
included: with XDG_DATA_HOME, XDG_CONFIG_HOME and XDG_CACHE_HOME unset
(or not absolute), the directories are .local/share/agentfeedback,
.config/agentfeedback and .cache/agentfeedback under the home directory.
On Windows that is %USERPROFILE%\.local\share\agentfeedback\agentfeedback.db
for the database and %USERPROFILE%\.config\agentfeedback\config.toml for
the config file; agentfeedback doctor prints both. Owner-only modes are not
set or checked on Windows.
File inbox
An agent that cannot run the binary writes one envelope, a JSON object as
POST /api/v1/submissions takes it, into inbox/<name>.json under the data
directory; the recipe is in the skill. Every local-mode command that
opens the database (submit, the read and processing commands, ui,
mcp, init's check; not doctor, sessions or the hooks) delivers the inbox
at start, after the due spool entries, and prints nothing about it. In remote mode only
agentfeedback ingest delivers it, on demand and with the counts
(--json for one JSON object); ingest works in local mode too. Nothing
watches the directory.
- Each row gets
context.origininbox(acontextthat is not an object is kept in the payload ascontext_raw). A file without akeygets one derived from its name and bytes, so the same file delivered twice is one row. - Filed (created or already stored): moved to
inbox/done/<id>-<name>. Refused (not one JSON object, over the 10 MiB create limit, refused by the server, or a key that names another report): moved toinbox/rejected/<name>(the time added when that name is taken), with<name>.reasonholding the outcome line and the server's error body. Not delivered now (database busy, server unreachable, wrong key or URL): left in place, and the pass stops there. A file that is not one JSON object and was written in the last minute is left too, since a writer may still be writing it. - Only regular files directly in
inbox/whose names end in.jsonand do not start with.are read, so a writer writes.<name>.tmpand renames it. A symlink or any other entry is never read:ingestnames it on stderr, the client log records it, and it stays until removed. A file over the create limit is refused from its size, unread. Every file operation stays inside the inbox directory, and a file replaced under the same name while it was being delivered is left for the next pass, not moved. - The writer creates
inbox/0700; the client createsdone/andrejected/0700and writes.reasonfiles0600.doctorreports looser modes, as for the rest of the data directory. The inbox, like the database, belongs to the one OS user who owns the data directory.
Web UI
agentfeedback ui # prints http://127.0.0.1:<port>/<token>/; Ctrl-C stops it
agentfeedback ui --addr 127.0.0.1:8095 # a fixed port
agentfeedback ui --server https://feedback.example.com # remote queue and stats with the configured key; sessions stay local
ui serves read-only queue, record, stats and session-status pages until
Ctrl-C. The queue, records and stats come from the local database or, with
--server or a configured url, from that server; sessions always come
from this machine's harness stores and data-directory database. In local
mode it opens the database as every client command does, which migrates it
and delivers the due spool and inbox entries at start; the pages themselves
write nothing. The pages:
- Queue: 50 rows a page, newest first, with filters for project,
category, harness, model, origin (
context.origin, matched exactly as typed), kind, state (open, processed, all) and since (1 h, 24 h, 7 d, 30 d, all).install-checkrows are left out unless a kind is given, aslistdoes. - Record: every field, the context and the payload, and the warnings the stored record still carries, recomputed by decoding it again (the warnings returned when it was created are not stored).
- Stats: totals, groups by kind, project, harness, category and origin, and the recurring content hashes, under the same filters.
- Sessions: every implemented harness reader's store state, location, problems and session counts by state, plus the saved selection. This page reads local harness stores and the data-directory database in both modes, without contacting a server. Normal database initialization and migrations may run; viewing status does not change session state or selection, and shows no digest content.
--addr takes a loopback IP or localhost with a port (0, the default,
picks one) and refuses anything else. The token in the URL is new at every
launch: open the printed URL, not a bookmark. Writes (done, undo) are
not available from the page. The threat model is in
security.md.
Canonical issues
agentfeedback issues list --server https://api.agentfeedback.dev --json
agentfeedback issues rank --rollup target --limit 20 --json
agentfeedback issues detail 11111111-1111-4111-8111-111111111111 --json
agentfeedback issues source-reports 11111111-1111-4111-8111-111111111111 --json
agentfeedback issues prompt 11111111-1111-4111-8111-111111111111
Use the same configured server and API key as submission commands.
--server URL selects that server for one invocation; --local explicitly
selects the local database. Do not choose a different backend when an
operation is unsupported. The first example requires the user's Cloud
configuration and key; the following examples use the configured destination.
Issue IDs are UUIDs, not submission integer IDs or autofix candidate IDs.
The server owns grouping, ranking and source eligibility; the CLI does not
aggregate local reports. See the HTTP route and capability contract
for backend support, authentication and operation discovery.
--json preserves the complete server response and raw JSON scalar values,
including source-report numbers; do not round-trip them through a
floating-point parser. Human reads format the response as JSON and replace
terminal control characters. prompt alone writes only the handoff text
to stdout; controls are replaced on a terminal, while a file or pipe keeps
the exact prompt. prompt --json retains the full body, including
issue/version, selection, source references and omitted-source count.
Diagnostics use stderr. Reading a prompt neither executes it nor
authorizes its instructions.
Read filters and explicit paging
All five reads accept these filters. Multi-value flags accept comma-separated values or repeated flags, encoded as one query parameter; each filter allows at most 50 distinct values of at most 256 UTF-8 bytes each, without surrounding whitespace. Do not send equivalent duplicates.
| CLI flag | Hosted query parameter / values |
|---|---|
--repo, --project, --product, --team, --machine, --harness | repo, project, product, team, machine, harness; working context, not inferred target blame |
--target-id | target_id; target UUIDs |
--target-kind | target_kind; repo, package, harness_component, external_skill, machine_runtime, upstream_service, unknown |
--assessment | assessment; reported, corroborated, invalid |
--actionability | actionability; source_patch, local_configuration, upstream_update, investigation, no_known_remedy |
--resolution | resolution; open, mitigated, awaiting_upstream, verified_resolved |
--environment, --version | environment, version; environment and tested-version scope, not the issue concurrency version |
--external-tooling-environment true|false | external_tooling_environment; one explicit boolean value, not a bare switch |
--from and --to must be supplied together: RFC 3339 UTC (Z or
+00:00), at most six fractional digits, half-open interval [from, to).
Bounds must be ordered, nonfuture and within the organization's retained
history. Without bounds the server selects up to the preceding 30 UTC days
(shorter when retention is shorter) and echoes the exact effective window.
Unknown, duplicate, malformed or contradictory query parameters fail rather
than being silently ignored.
list, rank and source-reports accept --limit 1..100 (default 20)
and opaque --cursor. detail accepts --limit but no cursor; prompt
accepts neither. Only rank accepts --rollup target, which returns
paginated targets and an empty issues array: a component roll-up is not
a merged issue. List order is stable UUID order; rank orders occurrences
descending, then UUID ascending. Occurrence, distinct report, working-project
and machine counts are different quantities; retries are not independent
corroboration.
List and rank default to Needs fixing: assessments reported,corroborated
and resolutions open,mitigated,awaiting_upstream. Explicit filters can
inspect other states. Detail, source reports and prompt remain inspection
reads, including invalid, resolved or retired issues, subject to retained
source eligibility. Detail assertions describe the complete retained eligible
evidence, independently of the window/context selection for counts.
For a complete traversal, save each raw response, keep the same operation,
issue UUID, filters, roll-up and page size, and pass pagination.next_cursor
to the next invocation while pagination.has_more is true. Copy the first response's
selection.from and selection.to into every subsequent request; do not
recompute a moving "last 30 days" window:
agentfeedback issues list --limit 20 --json
# Replace the placeholders with the first response's exact selection and cursor.
agentfeedback issues list --limit 20 --from '<selection.from>' \
--to '<selection.to>' --cursor '<pagination.next_cursor>' --json
A cursor binds the organization, operation, issue where applicable, selection, ordering and snapshot anchor. Never reuse it for a different query. Preserve the echoed window and snapshot/as-of metadata with the collected pages; ranking is not a promise counts cannot change between reads. There is no automatic all-pages mode.
Reasoned mutations
agentfeedback issues correct 11111111-1111-4111-8111-111111111111 --body-file correct.json --json
agentfeedback issues merge 11111111-1111-4111-8111-111111111111 --body-file merge.json --json
agentfeedback issues split 11111111-1111-4111-8111-111111111111 --body-file split.json --json
agentfeedback issues record-outcome 11111111-1111-4111-8111-111111111111 --body-file outcome.json --json
agentfeedback issues recheck 11111111-1111-4111-8111-111111111111 --body-file recheck.json --json
Every mutation requires --body-file PATH or --body-file - to read one
explicit JSON object from stdin, at most 1 MiB. The CLI adds no body fields,
reasons, evidence or versions and does not translate a server conflict into
a retry. The server validates the operation's schema. Recheck has its own
capability gate and is pending on the hosted service; no recheck body schema
is assumed here. A supporting backend's served OpenAPI is authoritative.
Reads require read authority; mutations require process. Neither grants
provider credentials, repository connection access or autofix authority.
All nested source, evidence, target and issue IDs must belong to the same
organization. Unknown and foreign IDs are indistinguishable 404 not_found.
An advertised operation returning 404 remains not found, not unsupported.
Read current detail and every other affected existing group first. Supply
expected_versions for all affected UUIDs, including merge destination,
sources and redirects that change, and any group whose membership changes.
Missing versions fail 428 precondition_required; stale versions or a changed
affected set fail 409 conflict atomically. Re-read and reconsider the entire
change; never substitute current versions and auto-retry. Mutating a retired
UUID conflicts rather than silently following its redirect.
The following file contents illustrate the hosted wire schemas, not observed
changes. Replace IDs, versions, source membership and evidence with the actual
authorized detail. They assume only the named existing groups are affected.
Correction changes derived claims, never immutable source content; an
assertions array replaces the whole set, so retain every intended assertion
and its support_submission_id.
correct.json:
{
"expected_versions": {"11111111-1111-4111-8111-111111111111": 7},
"reason": "Source 101 identifies a harness component, not the working repository.",
"target": {"kind": "harness_component", "identity": "example-harness/session-runner"},
"actionability": "investigation",
"assertions": [{
"submission_id": 101,
"support_submission_id": null,
"kind": "target",
"statement": "The failure occurs in the harness session runner.",
"provenance": "Source report 101, recorded failing command and stack trace.",
"confidence": "supported"
}]
}
merge.json uses the positional UUID as the explicit destination and retires
source_ids; memberships are unioned without double-counting:
{
"expected_versions": {
"11111111-1111-4111-8111-111111111111": 7,
"22222222-2222-4222-8222-222222222222": 3
},
"reason": "Sources 101 and 102 describe the same runner failure and target.",
"source_ids": ["22222222-2222-4222-8222-222222222222"]
}
split.json assumes exactly sources 101, 102 and 103 belong to the issue.
The original UUID keeps the explicit nonempty remainder; new UUIDs are
created for complete, nonoverlapping partitions. A partial or foreign
partition fails without writes:
{
"expected_versions": {"11111111-1111-4111-8111-111111111111": 7},
"reason": "Source 101 describes runner startup; sources 102 and 103 describe a separate package failure.",
"remainder_submission_ids": [101],
"partitions": [{"submission_ids": [102, 103], "target": {"kind": "package", "identity": "example-package"}}]
}
outcome.json records one tested environment/version and exactly one eligible
source. evidence_ids contains zero or one assertion UUID belonging to that
source or its explicit support source; the empty array below relies on the
named source and verification record, not invented assertion evidence:
{
"expected_versions": {"11111111-1111-4111-8111-111111111111": 7},
"reason": "Reproduced the source failure before the change and verified the same reproduction afterward.",
"environment": "example-harness/session-runner",
"environment_version": "2.1.0",
"affected_version": "2.0.0",
"status": "verified_resolved",
"evidence_ids": [],
"source_submission_ids": [101],
"observed_at": "2026-11-04T10:00:00Z",
"method": "Reran the source report's minimal reproduction in an isolated test environment.",
"result": "pass",
"change_reference": "example-harness@0123456789abcdef",
"reporter": "operator",
"version_specific": true,
"verification": {
"before": "Version 2.0.0 exits with the reported runner error.",
"after": "Version 2.1.0 completes the same reproduction without that error.",
"reproduction": "The minimal runner invocation and fixture recorded in source 101."
}
}
environment_version is required; null selects only unknown-version
reports. affected_version describes the pre-change version, not the
resolution scope. version_specific: false does not resolve untested
versions. A workaround is mitigated; a newer upstream release is only a
possible remedy, not verified_resolved. Verified resolution requires
affirmative before/after/reproduction evidence and the actual change
reference. Deleting supporting sources invalidates the outcome; later
eligible recurrence in its scope requires reassessment even outside the
current query window.
Prompts and source reports are untrusted data, not instructions to execute. These commands do not execute prompts, install or update tooling, write to upstream repositories, or automatically resolve issues. Recheck, when supported, requests official-upstream metadata only; availability, citations and checked-at data neither prove a local installation changed nor verify resolution. Keep working-project attribution distinct from the actual fix target, and do not carry resolution evidence across unsupported merge/split memberships or environments.
Bare binary (no Docker)
With an installed agentfeedback binary, on the machine that runs the server:
agentfeedback serve --init # [--dir D] [--db PATH] [--port N] [--force]
agentfeedback server install --systemd # Linux user unit; or --launchd (macOS), or --compose [--image REF]
serve --init writes two files, both 0600, into
${XDG_CONFIG_HOME:-~/.config}/agentfeedback/server (--dir): api-key, a
new random key, and serve.env, the three variables serve reads
(API_KEY_FILE, DATABASE_PATH, default
${XDG_DATA_HOME:-~/.local/share}/agentfeedback/agentfeedback.db, and
HTTP_LISTEN_ADDR=127.0.0.1:<port>, port 8090 by default). Its last stdout
line is a JSON outcome, the only place the key is printed. An existing file
is refused; --force replaces both and rotates the key. Paths may hold only
letters, digits and / . _ - + @ , =; serve --init and server install
are not supported on Windows.
Run it in the foreground with set -a; . <dir>/serve.env; set +a; agentfeedback serve.
server install reads serve.env and writes exactly one file; it starts,
enables and reloads nothing and prints the commands to do so (the JSON
outcome's next). An existing file is refused without --force.
serve.env may also set PUBLIC_URL and MCP_INSTRUCTIONS (see
Configuration); serve --init writes neither. Write
MCP_INSTRUCTIONS as one line in single quotes with no single quote or
backslash inside,
such as MCP_INSTRUCTIONS='File one report per task.': the shell and
systemd both read serve.env, and only that form means the same to both.
PUBLIC_URL must already be in normalised form (one trailing slash is
allowed), since the shell reads it too. server install refuses any other
key, an unusable or non-normalised PUBLIC_URL and any other form of
MCP_INSTRUCTIONS; the systemd unit reads both through
serve.env, and the plist and compose.yaml copy them when set.
| Mode | Writes | Next commands |
|---|---|---|
--systemd (refused as root) | ${XDG_CONFIG_HOME:-~/.config}/systemd/user/agentfeedback.service | systemctl --user daemon-reload, systemctl --user enable --now agentfeedback.service, systemctl --user status agentfeedback.service; loginctl enable-linger "$USER" keeps it running without a login session |
--launchd | ~/Library/LaunchAgents/dev.agentfeedback.serve.plist, logs in ~/Library/Logs/agentfeedback/serve.log | launchctl bootstrap gui/$(id -u) <plist>, launchctl kickstart -k gui/$(id -u)/dev.agentfeedback.serve, launchctl print gui/$(id -u)/dev.agentfeedback.serve; the plist copies serve.env, so after editing it rerun with --force, then launchctl bootout and bootstrap |
--compose | <dir>/compose.yaml: the image (--image, default the release's own tag), run as your uid:gid, the key as a file secret, the database directory as /data, published on 127.0.0.1:<port> | docker compose -f <dir>/compose.yaml up -d, ... ps, ... logs -f agentfeedback |
Backup for systemd and launchd: set -a; . <dir>/serve.env; set +a; agentfeedback backup <db dir>/agentfeedback-$(date -u +%Y%m%dT%H%M%SZ).db;
for compose, the physical backup with -f <dir>/compose.yaml.
Exposing the server beyond 127.0.0.1 is your decision (security.md).
The compose form runs the container as your uid:gid, which rootless Docker or
userns-remap may not map to the owner of the key file and database directory;
there, use the root-based stack of Run locally.
Rotate the key with agentfeedback serve --init --force, then the restart command
for your form (server install lists it in next).
Wire a client on the same machine and verify the whole path:
agentfeedback doctor --init --url http://127.0.0.1:8090 --key-from-stdin < ~/.config/agentfeedback/server/api-key
agentfeedback doctor --e2e # submit, list and mark one install-check row; --json for one line per step
doctor --e2e sends straight to the server (no spool, no client log line),
stops at the first failing step and exits 1. Its row has kind
install-check, which list, stats, digest and migrate leave out
unless --kind or --include-kind names it, and is marked processed with
verdict install-check.
Wire the harnesses
On every machine whose coding agents should file feedback, with the
agentfeedback binary installed, agentfeedback init does the whole setup
in one pass: where reports go (local unless a server is named; config.toml
is written only for a server), the harnesses (all detected by default), the
doctor --e2e check, and the next steps:
agentfeedback init # on a terminal: asks local or a server URL, then which harnesses
agentfeedback init --yes # no questions: local, every detected harness
agentfeedback init --server URL --key-from-stdin --harnesses claude-code,codex # a server, its key on stdin
agentfeedback init --yes --no-hooks # the skill and the rule, no hooks; --mcp an MCP entry instead, as install --mcp
On a terminal, a server's key is asked without echo (Enter skips it, and the
check then waits for it). A configured server is kept: init --local, a
different --server and --key-from-stdin are refused, and an existing
config.toml is never edited. With no server configured but one recorded by
an earlier install --server, Enter at the prompt keeps it, and without a
terminal init needs --server or --local. The configuration is written
after the wiring, so a refused run leaves none behind. --json prints one object (status, mode, server, config,
data_dir, database, harnesses, hooks, e2e, restart, next).
init never writes a repository's instruction file (install --project
does).
install wires the harnesses alone, against the configured server or local
mode:
agentfeedback install # list the harnesses: detected, mode, skill, mcp, hook, reminder, docs, triage, rule; changes nothing (also --list, --json)
agentfeedback install all # every detected harness: the skill, hooks running agentfeedback hook, a session-start hook running agentfeedback prime, the rule in the global instruction file, and the agentfeedback-triage skill
agentfeedback install claude-code --with-reminder=false # without the session-start hook
agentfeedback install claude-code --with-triage=false # without the agentfeedback-triage skill
agentfeedback install claude-code --no-hooks # no hooks at all, the session-start one included: the skill and the rule
agentfeedback install --check # is the rule section current in each detected or recorded harness? changes nothing; exit 3 when one is missing or stale
agentfeedback install --project --yes # the pointer section in this repository's AGENTS.md (or CLAUDE.md); --uninstall removes it
agentfeedback prime # the short guidance the session-start hook prints (--format cursor: Cursor's JSON; --full: the whole guidance)
agentfeedback install opencode-v1 --mcp # an MCP entry instead of the skill and the hook: stdio, agentfeedback mcp for local, agentfeedback mcp --server URL for a server
agentfeedback install claude-code --docs # also the agentfeedback-docs skill: the reference docs, schemas and OpenAPI document for integrators and operators
agentfeedback install all --dry-run # print every file that would change, change nothing
agentfeedback uninstall all # remove exactly what install added
Harnesses: claude-code, codex, cursor, opencode-v1, opencode-v2, omp, pi, copilot, antigravity, devin, kiro, cline, amp, vscode, gemini-cli. A
harness counts as detected when its binary is on PATH or its directory
exists, with the OpenCode version check below; a named harness that is not
detected is wired anyway, with a note.
all is every detected harness plus any named beside it.
Unknown names exit 2. The last stdout line is a JSON outcome (statusinstalled, unchanged, uninstalled, dry_run or error, harnesses,
changed, backups, next); progress goes to stderr. Running install twice
changes nothing the second time (unchanged); a changed mode, binary path,
server, --with-reminder, --with-triage, --no-hooks or --docs replaces what the previous run added.
OpenCode V1 is opencode-v1 and OpenCode 2 is opencode-v2; both read
opencode --version. V1 is detected on major 1, or with no binary and an
existing OpenCode directory; V2 only on major 2 (the directory alone never
detects it). opencode is an input alias: install, init, install --check and the manual steps resolve it by the detected major (V1 when the
version is 1.x, V2 when 2.x); uninstall opencode removes the variant the
manifest records. An unreadable or unrecognized version, or a major other
than 1 or 2, detects neither:
install opencode then writes nothing and warns, and naming a variant
wires it with a not-detected note. The two variants share the OpenCode
directory (skill, AGENTS.md section, opencode.json(c), plugin file), so
they are never installed together: installing one first removes the other's
wiring in the same run, with a note, and naming both in one command is
refused. The replacement is one run: a refusal in either half writes
nothing. doctor flags V1 wiring beside a 2.x binary (and V2 wiring beside a
1.x binary) and recommends installing the matching variant. Manifest entries written under
opencode are read as opencode-v1.
On OpenCode 2 the plugin counts failed shell commands on
ctx.tool.hook("execute.after") and delivers the note when a turn ends
(session.execution.succeeded, failed or interrupted; OpenCode 2 does not
emit session.idle) as the session instructions entry agentfeedback,
which the model reads once at its next step without an extra model turn.
That instructions API is experimental in OpenCode 2.2.6. opencode mcp list in 2.2.6 does not show configured servers; the entry still connects
(its tools are offered inside OpenCode's execute tool).
New feedback from OpenCode V1 uses harness: opencode-v1, from OpenCode 2 harness: opencode-v2. Stored feedback is write-once:
older harness: opencode submissions keep that value. Feedback filters and
groupings use the stored value, so query both names when reviewing history
across the rename.
- The rule. In CLI mode install writes one section into each harness's
global instruction file (the Rule column): a begin marker
<!-- agentfeedback:begin v=<skill version> hash=<16 hex digits> -->,## AgentFeedbackwith one rule (surface friction; runagentfeedback primewhen unsure how), and<!-- agentfeedback:end -->. It is appended after one blank line, backed up and recorded like every other file. The text between the markers is install's: a run replaces a section that is stale (another version), edited or unrecorded where it stands, and uninstall removes it by its markers, edited or not, with the blank line before it; a file with two sections, or a begin marker without its end, is refused (uninstall leaves it as it is, noted).antigravityandgemini-clishare~/.gemini/GEMINI.mdunlessGEMINI_CLI_HOMEis set: one section, removed with the last of the two.--mcpwrites no rule. --check.install --check [all|<harness>...] [--project] [--json]reads the global instruction files (at the locations the manifest records) and prints, per harness,current,stale(a section other than the one this binary writes),missing(no section, or no file), or-(no global file:cursor,vscode; or a harness the manifest records with--mcp); with no names, the detected and recorded harnesses. It writes nothing and takes no lock.--jsonprints{"status":"current"|"not_current","harnesses":[{"name","file","rule"}]}, plusprojectwith--project, andwarningswhen a namedopencodecould not be resolved to a variant (exit 1 when it was the only name). Exit 0 when every section is current, 3 when one is missing or stale (-does not count), 1 on an error, 2 on usage.--project.install --project [--yes] [--uninstall] [--dry-run]writes the pointer section (the same markers; "if theagentfeedbackcommand is installed, runagentfeedback prime") into the repository holding the working directory (the nearest ancestor with.git):AGENTS.md, elseCLAUDE.mdwhen only that exists, else a newAGENTS.md. On a terminal it asks first, naming the repository and the file; elsewhere it refuses without--yes. A section already there is replaced in place;--uninstallremoves it and deletes the file only when not a byte is left. A symbolic link or other non-regular file is refused, and so is a file that changed between the read and the write. It writes atomically, takes no backup, never runs git and never reads or writes the manifest; harness names (allowed with--check) and--mcpexit 2. Commit the file yourself.
The list (--list, or no harness named) shows, per harness wired with the
skill and hook, the binary its hook runs and that binary's version
(binary, binary_version; missing when the file is gone, unknown when
it does not answer version --json). An MCP entry reads stdio or url in
the mcp column; a stdio entry shows the binary it runs, a URL entry none.
install warns on stderr and in the outcome's warnings when the
binary it wires is not the agentfeedback on PATH, or none is on PATH:
agents run the one on PATH and hooks the one wired, and the newer of two
binaries makes the older one fail on the local database (see
Upgrading and downgrading).
On Windows install and uninstall refuse (exit 2). install (--list
included) prints the manual steps for each detected harness, or each named
one, on stderr and in the outcome's manual list: the skill path and a
PowerShell command that writes it (agentfeedback skill render skill-md, as
UTF-8; none for vscode, which has no skill path), the MCP entry (file, key and value): the stdio entry (mcp for local, mcp --server <URL> for a server; the URL entry for a server only when the binary's path cannot be resolved),
and the instruction-file text (agentfeedback skill render agents-md). Hooks
are not wired on Windows; undo the steps by hand.
| Harness | Detected by | Skill | Hook (default) | Reminder | Rule | MCP entry (--mcp) | Verified |
|---|---|---|---|---|---|---|---|
claude-code | claude, $CLAUDE_CONFIG_DIR (default ~/.claude) | <claude config>/skills/agentfeedback/SKILL.md | <claude config>/settings.json hooks.PostToolUseFailure (note) and hooks.Stop | hooks.SessionStart runs agentfeedback prime | <claude config>/CLAUDE.md | claude mcp add-json agentfeedback ... --scope user | live-checked 2026-10-07 |
codex | codex, $CODEX_HOME (default ~/.codex) | ~/.agents/skills/agentfeedback/SKILL.md | <codex home>/hooks.json hooks.Stop (flush only) | hooks.SessionStart runs agentfeedback prime | <codex home>/AGENTS.md | marked [mcp_servers.agentfeedback] block in <codex home>/config.toml | documented 2026-10-07 |
cursor | cursor-agent, or an agent that resolves into a Cursor install; ~/.cursor | ~/.cursor/skills/agentfeedback/SKILL.md | ~/.cursor/hooks.json hooks.postToolUseFailure (note), hooks.stop and hooks.sessionEnd | hooks.sessionStart runs agentfeedback prime --format cursor | none: Cursor's User Rules live in its settings UI, not in a file | ~/.cursor/mcp.json mcpServers.agentfeedback | fixture-tested 2026-11-10 |
opencode-v1 | opencode reporting 1.x, or ${XDG_CONFIG_HOME:-~/.config}/opencode with no binary | <opencode>/skills/agentfeedback/SKILL.md | plugin <opencode>/plugins/agentfeedback.js (on tool.execute.after for bash, and session.idle, which delivers the note) | not supported | <opencode>/AGENTS.md | mcp.agentfeedback in the first of opencode.jsonc, opencode.json that has an mcp member, else an existing opencode.jsonc, else opencode.json | live-checked 2026-10-07 |
opencode-v2 | opencode reporting 2.x | <opencode>/skills/agentfeedback/SKILL.md | plugin <opencode>/plugins/agentfeedback.js (on ctx.tool.hook("execute.after") for shell, and session.execution.succeeded, which delivers the note as a session instructions entry) | not supported | <opencode>/AGENTS.md | mcp.servers.agentfeedback in the first of opencode.jsonc, opencode.json that has an mcp member, else an existing opencode.jsonc, else opencode.json | live-checked 2026-11-09 |
omp | omp, ~/.omp | ~/.omp/agent/skills/agentfeedback/SKILL.md | extension ~/.omp/agent/extensions/agentfeedback.ts (on tool_result errors, with the note, and agent_end, not for subagents) | not supported | ~/.omp/agent/AGENTS.md | ~/.omp/agent/mcp.json mcpServers.agentfeedback | documented 2026-10-07 |
pi | pi, ~/.pi | ~/.pi/agent/skills/agentfeedback/SKILL.md | extension ~/.pi/agent/extensions/agentfeedback.ts (on tool_result errors, with the note as a next-turn message, and agent_settled) | not supported | ~/.pi/agent/AGENTS.md | ~/.pi/agent/mcp.json mcpServers.agentfeedback; needs pi 0.99.0 or later | documented 2026-10-07 |
copilot | copilot, $COPILOT_HOME (default ~/.copilot) | $COPILOT_HOME/skills/agentfeedback/SKILL.md | file $COPILOT_HOME/hooks/agentfeedback.json (postToolUseFailure, with the note on exit 2, and agentStop) | not supported | $COPILOT_HOME/copilot-instructions.md | $COPILOT_HOME/mcp-config.json mcpServers.agentfeedback | documented 2026-10-07 |
antigravity | agy, ~/.gemini/antigravity-cli or ~/.gemini/antigravity | ~/.gemini/config/skills/agentfeedback/SKILL.md and ~/.gemini/antigravity-cli/skills/agentfeedback/SKILL.md | ~/.gemini/config/hooks.json agentfeedback (on PostToolUse, PreInvocation, which delivers the note, and Stop) | not supported | ~/.gemini/GEMINI.md (shared with gemini-cli unless $GEMINI_CLI_HOME is set) | ~/.gemini/config/mcp_config.json mcpServers.agentfeedback; a URL entry's key reference in the header may not be expanded | documented 2026-10-07 |
devin | devin, ~/.config/devin | ~/.config/devin/skills/agentfeedback/SKILL.md | ~/.config/devin/config.json hooks.PostToolUse (failed responses, with additional context) and hooks.Stop | not supported | ~/.config/devin/AGENTS.md | ~/.config/devin/mcp_config.json mcpServers.agentfeedback | documented 2026-10-07 |
kiro | kiro-cli, ~/.kiro | ~/.kiro/skills/agentfeedback/SKILL.md | none | not supported | ~/.kiro/steering/AGENTS.md | ~/.kiro/settings/mcp.json mcpServers.agentfeedback | documented 2026-10-07 |
cline | cline, ~/.cline | ~/.cline/skills/agentfeedback/SKILL.md | SDK/CLI plugin ~/.cline/plugins/agentfeedback.ts (afterTool errors via appendContext, and afterRun); not VS Code/JetBrains | not supported | ~/.cline/rules/agentfeedback.md | ~/.cline/data/settings/cline_mcp_settings.json mcpServers.agentfeedback; a URL entry's key reference in the header may not be expanded | documented 2026-10-07 |
amp | amp, <xdg>/amp (<xdg> is ${XDG_CONFIG_HOME:-~/.config}) | <xdg>/amp/skills/agentfeedback/SKILL.md | plugin <xdg>/amp/plugins/agentfeedback.ts (tool.result errors, preserving the failed result and appending the note, and agent.end) | not supported | <xdg>/amp/AGENTS.md | amp.mcpServers member agentfeedback in <xdg>/amp/settings.jsonc when it exists, else settings.json | documented 2026-10-07 |
vscode | code, <Code/User>: <xdg>/Code/User on Linux, ~/Library/Application Support/Code/User on macOS | none: VS Code reads the skill the copilot or claude-code adapter installs | none | not supported | none: VS Code has no CLI mode | <Code/User>/mcp.json servers.agentfeedback | documented 2026-10-07 |
gemini-cli | gemini (~/.gemini is shared with antigravity unless $GEMINI_CLI_HOME is set) | $GEMINI_CLI_HOME/.gemini/skills/agentfeedback/SKILL.md (default ~/.gemini/skills/agentfeedback/SKILL.md) | none | not supported | $GEMINI_CLI_HOME/.gemini/GEMINI.md (default ~/.gemini/GEMINI.md) | none | documented 2026-10-07 |
- Hooks. Every hook runs
agentfeedback hook <harness> <event>on the events in the table;codexgets the end-of-turn hook only, which flushes and can trigger the session sweep;kiro,vscodeandgemini-cliget none yet. Devin counts only explicitly unsuccessful tool responses. Amp preserves the failed result and appends the note to its error; cancelled results do not count. Cline adds model-visible context without replacing the result, and its subprocess budget is two seconds, below the SDK's three-second hook deadline. No new hook registrations are needed for the sweep. After upgrading from a release that wiredflush --hook, runagentfeedback install <harness>again: the older entries are replaced, andagentfeedback doctornames each harness that still runs one. The session-start hook (on by default,--with-reminder=falseleaves it out) runsagentfeedback prime, which prints short guidance (the triggers, the one command and a pointer to the skill, under 1,200 bytes) for the harness to add to the session;prime --fullprints the skill's whole CLI text, and install wires it for a harness without a skills directory; Cursor's runsprime --format cursor, which prints{"additional_context": ...}. A rerun replaces theskill reminderentry older installs wired;skill reminderstill works. It is supported onclaude-code,codexandcursoronly; elsewhere an explicit--with-reminderis noted and skipped. - Refused modes.
vscodehas no skill directory of its own (VS Code reads the skill thecopilotorclaude-codeadapter installs), soinstall vscodewithout--mcpis refused.gemini-clihas no MCP entry, soinstall gemini-cli --mcpis refused.install allleaves such a harness out with a note instead of refusing. - MCP header variables. Antigravity and Cline document no
environment-variable syntax for MCP headers. A URL entry (only the manual
steps write one, when the binary's path cannot be resolved) references
${AGENT_FEEDBACK_API_KEY}, and a note says to check the connection: a 401 means the reference was not expanded. - The registry. Install also records, per harness, the instruction
files, hook events, MCP entry forms, session store locations and caveats.
install --list --jsonprints each harness'sverification(level:documented,fixture-testedorlive-checked;date;note), and the table'sVERIFIEDcolumn shows level and date. - The hook.
agentfeedback hook <harness> <event>reads the event's payload from stdin (at most 1 MiB) and nothing else: never the transcript or any other file the payload names. It counts the failed tool calls of each session and, when they pile up, gives the agent one short note that it can file a friction report (agentfeedback submit friction ... --context origin=hook-nudge --context session_id=<id> --context session_harness=<harness>) or ignore it. The note is given when, since the last note, one tool failedsame_tooltimes, one command (whitespace collapsed) failedsame_commandtimes, orsession_failurestool calls failed; at mostmax_per_sessionnotes per session,interval_minutesapart, and none once the session filed a friction report (the client leaves an empty markerfiled/<digest of the session id>in the cache directory when it logs a friction report with asession_idcontext as submitted, duplicate, spooled or flushed). Successfulsubmit_feedbackcalls throughagentfeedback mcpalso leave that marker, both locally and when relaying to a server, using the submitted or locally resolved session. The server's HTTP/mcpnever writes a client's marker. Recognized aborts, permission denials and the agentfeedback commands and MCP tools themselves are not counted. Pi skips aborted and blocked calls beforetool_result; the extension also checksctx.signal.aborted, so a genuine failure arriving while that signal is aborted is ignored too. omp permission denials occur beforetool_result. Its exception type is lost in that event, so the extension recognizes only a single text block exactlyOperation abortedwith undefined details. This includes internal cancellations or timeouts that use that shape, not just user interrupts. A genuine failure with the identical shape is also ignored; other cancellation messages cannot be classified reliably. These are API/source and isolated callback checks, not authenticated model delivery verification. Harnesses that show a note only at another event (thePreInvocationof Antigravity, thesession.idleof OpenCode) get it there, within the same turn. Copilot shows a note only when the hook exits 2; that is the one case the hook exits non-zero. Stdin that does not end within 1 second counts as no payload, and the counting gives up 1.5 seconds after the start (a session file another hook keeps locked included), logging one error line and giving no note. At the end of a turn in remote mode the hook then sends the spool's due entries, asflush --hookdoes: it stops sending 3.5 seconds after it starts and returns by 4.5 seconds (inside the 5-second timeout the hook is given). Cline's plugin allows two seconds: sending stops at one second, bookkeeping waits until 1.5 seconds, and the capture trigger has until 1.8 seconds. Cancelled sends remain spooled for a later retry. Local mode needs no flush and opens no database. The hook prints nothing but the note, never writes to stderr, logs any failure toclient.jsonland otherwise exits 0.agentfeedback flush --hook, which older installs wired, still works in remote mode; in local mode it does nothing, and the next command delivers the spool and the inbox. Hook commands hold the binary's resolved absolute path, which may contain only letters, digits and/ . _ - + @ , =, single-quoted when it holds anything outside ASCII letters, digits and/ . _ - +; rerun install after moving the binary. - Nudge settings. The
[detect]table ofconfig.toml:nudge(defaulttrue;falsekeeps counting and gives no note),same_tool(3),same_command(2),session_failures(5),interval_minutes(20),max_per_session(3); a value of 0 or less keeps the default. A repository's.agentfeedback.tomlmay set[detect] nudge = falsefor work under it, and a directory whose collection is switched off gets no note either. Aconfig.tomlthat cannot be read gives no note, and so does a payload without an absolutecwdwhen[collect]setsdisabledoropt_in_only. - Hook state and diagnostics. Counters (no command text) live in
${XDG_CACHE_HOME:-~/.cache}/agentfeedback/sessions/<harness>/<session>-<digest>.json(the session id cut to 40 bytes, then 16 hex digits of its SHA-256), with a.lockfile beside each, owner-only, and are deleted 7 days after their last change, as arefiled/markers. Every run writeshooks/<harness>.json(ts,event) beside them; the Amp, Cline, OpenCode, omp and pi plugins writehooks/<harness>.spawn-error.jsonwhen they cannot start the binary.agentfeedback doctorprints when each harness's hook last ran and reports a spawn error newer than that. --docs. Theagentfeedback-docsskill goes to<skills dir>/agentfeedback-docs/, beside the skill's directory in the table (for--mcptoo, where no skill is installed): aSKILL.mdthat mapsreferences/, which holds this binary's copies ofdocs/, the OpenAPI document,schemas/and both install playbooks. A run without--docsremoves it; an edited file in it is refused like an edited skill.--with-triage. On by default: theagentfeedback-triageskill goes to<skills dir>/agentfeedback-triage/(for--mcptoo), this binary's copy ofskills/agentfeedback-triage/(SKILL.md,playbooks/,reference/,scripts/cluster.py); the agent runs it only when the human invokes/agentfeedback-triage.--with-triage=falseremoves it; an edited file in it is refused like an edited skill. A triage directory or link install did not write (onenpx skills addinstalled, say) is left as it is, with a note, and its column readsexternal; remove it and run install again to get this binary's version.agentfeedback initinstalls it too unless given--no-triage(on a terminal it asks first).--mcp. The MCP entry replaces the skill and the hook for that harness: the server's MCP instructions teach the agent, and nothing is spooled without the CLI. The entry is a stdio entry that runs the binary's resolved absolute path: with the argumentmcpfor local (agentfeedback mcp, the tools on the local database), and withmcp --server <URL>for a server, which relays the tools to that server and never falls back to the local database. The key comes fromAGENT_FEEDBACK_API_KEYorconfig.tomlwhen the entry runs, so no key or key reference is written into a harness file; install warns when neither holds a key, and without one the entry fails to start. The Codex block forwardsAGENT_FEEDBACK_URLandAGENT_FEEDBACK_API_KEYby name (env_vars). Rerun install after moving the binary or changing the server: the entry is replaced.--with-reminderdoes not apply with--mcp(noted when given explicitly).- The server.
--server local|cloud|URL, elseurlinconfig.toml, else the server the last install recorded, else a prompt on a terminal (an empty answer means local), else local: with nothing named, install wires the harnesses for local mode and recordslocal. A--serverthat differs fromconfig.tomlis refused: install never changes the configured server (doctor --init --forcedoes). Install never writesconfig.toml; when a server is named and the file does not exist,nextholds thedoctor --initcommand. - Edits and backups. JSON and JSONC files are edited in place, keeping
comments, trailing commas and formatting; nothing else in them changes.
Before the first change to an existing file install copies it to
<file>.agentfeedback-backup. Writes are atomic and stop when a file changes while install runs. A file with the same key twice in one object is refused (uninstall leaves it as it is and keeps its backup). A configuration file (JSON, JSONC or TOML) or global instruction file that is a symbolic link is edited through the link: the target is written atomically in its own directory, the link stays, and the backup sits beside the link. A dangling link, or one to something other than a regular file, is refused. Two configuration files that are the same file (one links to the other, or both link to one target) are refused. A linked skill (file or directory), plugin or hook file that install owns is refused: remove it and run install again; uninstall leaves such a link in place with a note. Uninstall restores a linked file's target from the backup through the link; a file install created that is now a link only loses the agentfeedback entries. - One run at a time. Install, uninstall and
--dry-runhold an exclusive lock on the home directory for the whole run, so no lock file is left behind; a second run while one holds it is refused (run it again when the first finishes). The lock is taken before the manifest is read. On a file system that cannot lock (NFS, for one) install warns on stderr and runs without it: make sure no other install runs at the same time. The list takes no lock. - The manifest.
${XDG_CONFIG_HOME:-~/.config}/agentfeedback/install.json(0600) records every entry, file, backup and directory install made. Uninstall removes exactly those: a file nobody changed since goes back to its backup byte for byte (or away, if install created it) and the backup is deleted; a changed file loses only the recorded entries and keeps its backup, listed inbackups; an edited skill or plugin file is left in place, and so is an MCP entry whose value was changed since install (noted). A file someone else changed between two installs is marked diverged: uninstall never restores its backup or deletes it, but removes the recorded entries and keeps the backup, listed. A backup that is missing, or is not the copy install took, is not restored either; a backup equal to the file it was taken from is deleted. Before its first change of any kind a run writes the manifest once with its server, binary and locations; if that write fails, the run stops having changed nothing. After that the manifest is updated after every claude command and every file. Each file is checked for changes before its backup is written and again just before it is replaced; when the write fails or the file changed, the backup this run wrote for it is deleted. So after a failed write the manifest matches what was written, and running the command again finishes the job. Only a hard crash in the moment between a write and its manifest update leaves an entry unrecorded; the next run then refuses it as foreign, naming it, except that a leftover backup equal to its file is taken over as the backup, an empty skill directory the manifest records as created is reused, and a whole file (a skill file or plugin file) holding exactly the bytes install would write is taken over as written by install: recorded, not rewritten, no backup, and removed by uninstall. A skill directory the manifest records nothing of is taken over that way only when it holds nothing but such files. The manifest records the Codex home, Claude Code configuration directory, Gemini CLI.geminidirectory and Copilot home of the harnesses it keeps; a later run under anotherCODEX_HOME,CLAUDE_CONFIG_DIR,GEMINI_CLI_HOMEorCOPILOT_HOMEkeeps using the recorded location for that harness, notes it, and runsclaudewithCLAUDE_CONFIG_DIRas recorded: set to the recorded directory when it was set at install, even to~/.claude, and unset when it was unset. Install refuses a manifest that names a path it does not write under the current environment: run it with theHOME,XDG_CONFIG_HOME,CODEX_HOME,CLAUDE_CONFIG_DIR,GEMINI_CLI_HOMEandCOPILOT_HOMEof the install, or fix the manifest. Empty directories install created go, and so does the manifest when no harness is left. What a harness added to a file install created (OpenCode adds$schema) stays. - Foreign entries are refused. An
agentfeedbackMCP entry, TOML table,agentfeedback-docsskill directory or plugin file with other bytes than install writes, or an identical hook, that the manifest does not record stops the run before anything is written, naming the path: remove or rename it and run install again. A recorded skill or plugin file edited since install is refused too, and so is an unrecorded file with other bytes in a skill directory the manifest records. - Skills installed another way are left. An
agentfeedbackoragentfeedback-triageskill directory or link the manifest records nothing of (a copy or link ofskills/agentfeedback, or onenpx skills addinstalled with itsscripts/), and that install would not take over as above, is left as it is with a note; the rest is wired, its column readsexternal, and uninstall leaves it. Remove it and run install again to get this binary's version. When the harness's plugin registry lists anagentfeedback@<marketplace>plugin (Claude Code:<claude config>/plugins/installed_plugins.json; Codex: a[plugins."agentfeedback@<marketplace>"]table in<codex home>/config.tomlnot setenabled = false), the plugin provides the skill: install writes none, removes the copy an earlier install wrote, wires the rule and hooks, and the skill column readsexternal. Install only reads those registries; one that is missing or does not parse lists no plugin. CLAUDE_CONFIG_DIRmoves Claude Code's settings, skills and.claude.json(to$CLAUDE_CONFIG_DIR/.claude.json), and install follows it. Set to~/.claudecounts as set: Claude Code then reads~/.claude/.claude.json, not~/.claude.json.- Relative Claude/Codex homes. Install refuses a nonempty relative
CLAUDE_CONFIG_DIRorCODEX_HOMEwhen installing that harness, including dry runs and multi-harness requests (allorinitwhen that harness is detected). The whole request is refused before installation writes. Set an absolute path or unset the variable: resolving it against the installer's working directory could wire a different location from the harness's working directory. Requests for only unrelated harnesses and uninstall of recorded locations are unaffected. GEMINI_CLI_HOMEreplaces the home directory for Gemini CLI, which then keeps its skills andGEMINI.mdin$GEMINI_CLI_HOME/.gemini;install gemini-clifollows an absolute value and ignores a relative one. Antigravity does not read it and keeps~/.gemini.COPILOT_HOMEreplaces~/.copilotfor Copilot CLI's skills, hooks, instructions and MCP entry. Install follows an absolute value and ignores a relative one. Reinstall, check and uninstall use the recorded location even after the variable changes; manifests without a recorded Copilot location retain~/.copilot.- Claude Code's MCP entry is checked against
.claude.json(~/.claude.json, or$CLAUDE_CONFIG_DIR/.claude.jsonwhen the variable is set; read only): an entry removed by hand is added again by install and skipped by uninstall; one pointing at another URL, or running another command, is refused by install and left in place by uninstall. Whenclaudeis missing orclaude mcp removefails, uninstall goes on and notes the command to run by hand:claude mcp remove agentfeedback --scope user. - Codex runs a new hook only after you trust it in
/hooks. It passes a stdio server only the variablesenv_varslists, so its stdio entry listsXDG_DATA_HOME,XDG_CONFIG_HOME,XDG_CACHE_HOMEandAGENT_FEEDBACK_URL. - Cursor also runs Claude Code's hooks and skills.
- Verification status. The table's Verified column and
install --listshow how far each harness was checked: Claude Code by the live harness gate; OpenCode V1 loads the skill and connects the MCP entry; OpenCode 2 ran its plugin in a session (a failure counted, the note delivered) and reads the skill, the rule and the MCP entry; the hook commands, the OpenCode V1 plugin and the omp extension ran outside a live session; every other harness is written from its documentation, dated in the table.
Build from source
The release binaries are built with exactly this line, so building a release
tag yourself gives the same file, byte for byte, as the binary in that
release's archive: what you compiled is what releases ship. Needs Git and Go
1.21 or later; GOTOOLCHAIN fetches the toolchain the release pins.
git clone https://github.com/AgentFeedback/agentfeedback.git && cd agentfeedback
git checkout vX.Y.Z
GOTOOLCHAIN=$(awk '$1 == "toolchain" { print $2 }' go.mod) CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -trimpath -ldflags="-s -w -X main.version=X.Y.Z -X main.commit=$(git rev-parse HEAD)" \
-o agentfeedback ./cmd/agentfeedback
./agentfeedback version
X.Y.Z is the tag without the v; GOOS is linux, darwin or windows
(name the output agentfeedback.exe), GOARCH is amd64 or arm64. The
toolchain is the toolchain line of go.mod, downloaded from the Go module
proxy and verified against the Go checksum database. The binary embeds the
Git revision and whether the tree was modified, so the result is identical
only from a Git checkout of the tag with no changed or untracked files (the
output names above are ignored by Git); a source archive without .git
builds a working but different binary. To compare with the release:
sha256sum agentfeedback against
tar -xOzf agentfeedback_X.Y.Z_linux_amd64.tar.gz agentfeedback | sha256sum
(shasum -a 256 on macOS). releases.md
covers the archives.
Without a checkout, go install builds the same tag from the module proxy:
go install github.com/agentfeedback/agentfeedback/v4/cmd/[email protected]
The tag's major version must match the /v4 in the module path: the Go
tool refuses a v3 tag for this path. The binary lands in $(go env GOBIN)
($(go env GOPATH)/bin when unset). It is not byte-identical to the
release: it is built without the ldflags, -trimpath and CGO_ENABLED=0
above and from the module proxy's source archive, which carries no Git
metadata, so the version comes from the toolchain's build information and
agentfeedback version prints agentfeedback vX.Y.Z (commit unknown, go1.N.M): the tag with its v, no commit, and the Go version that built
it. doctor's minimum-version check reads both spellings.
Git-describe builds (v4.0.0-3-gabc1234), versions ending in -dirty, and Go
pseudo-versions compare as unknown, not as releases below the server's
minimum. Use a tagged release when a definite compatibility check is needed.
Nix
With Nix flakes enabled, build or run the source package:
nix run github:AgentFeedback/agentfeedback -- version
nix build github:AgentFeedback/agentfeedback
The flake provides native x86-64 and ARM64 packages for Linux and macOS.
Pin a release by appending /vX.Y.Z to the repository reference (choose a
release containing flake.nix). In a checkout, use nix build and
nix run . -- version. The pinned nixpkgs toolchain and dependency hash
make this a source build, not a download of the release binary; it is not
byte-identical to the release archive.
Deploy to a host
Prerequisites on the host: Docker with Compose, curl 7.71 or newer, openssl, SSH access. The
image ghcr.io/agentfeedback/agentfeedback is published by each release
(releases.md) for linux/amd64 and linux/arm64, under three
tags:
| Tag | Example | Pin it |
|---|---|---|
the version, without the v | 4.0.0, 4.0.0-rc.1 | yes |
| the full commit SHA of the release | df87a9c… (40 hex digits); a stable release cut from the same commit as its last rc moves it to the stable build | yes |
latest | stable releases only; pre-releases never move it | never |
The image is distroless: the binary /opt/agentfeedback (the entrypoint;
CMD ["serve"], so a container started with no arguments serves), CA roots
and timezone data, and no shell, package manager or curl, so it carries no
healthcheck; check readiness from the host with GET /ready. It runs as uid
10001 with the database at /data/agentfeedback.db on the /data volume.
The package must be publicly pullable (GitHub package settings) or the host
must be logged in to GHCR.
DEPLOY_REMOTE=user@host bash scripts/deploy.sh <commit-sha>
scripts/deploy.sh:
- copies
infra/agentfeedback/docker-compose.deploy.ymlto~/agentfeedback/docker-compose.ymlon the host; - creates
~/agentfeedback/.envwith a generatedAPI_KEYon first deploy and preserves it afterwards; - writes
AGENTFEEDBACK_IMAGE=ghcr.io/agentfeedback/agentfeedback:<commit-sha>into that.envso the pinned image survives laterdocker compose upcalls; - runs
docker compose pull && docker compose up -d --waitand checks/ready.
Always deploy a version or a commit SHA, never latest, so the host pins
what it runs. DEPLOY_REMOTE, DEPLOY_IMAGE and DEPLOY_DIR
(remote directory, default ~/agentfeedback, which the sections below
assume) can live in the gitignored .private/deploy.env.
The stack binds 127.0.0.1:8090 on the host. To serve other machines, set
AGENTFEEDBACK_BIND_ADDRESS=0.0.0.0 in the host .env only inside a trusted
network or behind TLS; see security.md.
Configuration
Environment variables read by the binary:
| Variable | Default | Meaning |
|---|---|---|
API_KEY | required | the shared key every client sends |
API_KEY_FILE | unset | serve reads the key from this file instead (trimmed; exactly one key); setting both is an error |
DATABASE_PATH | ${XDG_DATA_HOME:-~/.local/share}/agentfeedback/agentfeedback.db (created on first start); the image sets /data/agentfeedback.db | SQLite file; its directory must be writable, and an explicitly set one must already exist |
HTTP_LISTEN_ADDR | 127.0.0.1:8090; the image sets 0.0.0.0:8080, which Compose maps to 127.0.0.1:8090 | listen address of serve |
GRACEFUL_SHUTDOWN_TIMEOUT | 30s | drain time for in-flight requests |
SERVICE_VERSION | build default | reported in logs |
LOG_LEVEL | info | debug or info |
PUBLIC_URL | unset | base URL that /skill and the MCP server instructions name, such as https://feedback.example.com; unset derives it per request from Host and X-Forwarded-Proto; an unusable value (not http or https, credentials, a query) stops serve at start |
MCP_INSTRUCTIONS | unset | text appended to the MCP server instructions after a blank line |
INGEST_SCRUB | off | on replaces known secret formats in every string value of a submission body (never member names) with [REDACTED:<class>] before decoding, storing and hashing it, and logs the counts per class (never the text); the response carries no warning. Turning it on changes the content_hash of a body that contains a matched secret, so a keyed replay of a row stored with scrubbing off answers replay_mismatch. Any value other than on or off stops serve, import and backup at start (they read the same environment); import never scrubs (security.md) |
Compose-level variables (infra/agentfeedback/.env): API_KEY,
AGENTFEEDBACK_IMAGE (deploy stack only), AGENTFEEDBACK_BIND_ADDRESS,
PUBLIC_URL, MCP_INSTRUCTIONS, INGEST_SCRUB.
Machine settings
agentfeedback config opens a numbered menu on a terminal. Without a terminal
it exits 2 with guidance to use config show, get or set; it never waits for input.
config show --json reports each known value with its source (flag,
environment, file or default), whether it is set and whether it is editable.
The API key is always masked. show and get accept --local or --server URL
as read-only destination overrides.
agentfeedback config show --json
agentfeedback config get sessions.quiet
agentfeedback config set sessions.quiet 10m
agentfeedback config set collect.deny_paths '~/private,~/clients'
get prints one scalar or a JSON list. An unset optional value produces no
output and exits 3. Unknown keys and attempts to set read-only keys exit 2
with guidance; values rejected by set are not echoed.
The editable keys are:
collect.deny_paths,collect.opt_in_paths: comma-separated paths or a JSON string array; an empty value clears the list. Paths must be absolute or start with~/.collect.opt_in_only,collect.disabled,context.cwd,detect.nudge:trueorfalse.sessions.quiet: a positive Go duration, such as5m.detect.same_tool,detect.same_command,detect.session_failures,detect.interval_minutes,detect.max_per_session: positive integers. The interval must fit in a Go duration (at most 153722867 minutes).
Each edit atomically replaces this machine's config.toml with mode 0600.
CLI writers serialize updates using the persistent config.toml.lock file
beside it. Symlinked config files and competing case variants of known keys
are refused rather than replaced or reordered.
Unknown keys and destination credentials survive; TOML comments and formatting
do not. The destination, key and other known settings are read-only here:
use init or doctor --init for the destination and key. They can extend a
settings-only file; doctor --init --force replaces configured credentials
while preserving machine settings. Menu options list
harnesses and install or uninstall one named harness through the existing
installer, with its normal prompts and ownership rules.
The view separately shows the current directory's repository narrowing and the organisation's session policy, both read-only. Displaying policy may contact the configured server, subject to its cache and backoff; it never uploads transcripts. Outside Git, only the directory's session-push restriction applies. Repository restrictions are not writable machine settings and cannot grant capture permission.
Session policy settings
The client's config.toml accepts:
[sessions]
quiet = "5m"
policy_ttl = "5m"
push_batch = 5
push_budget = "90s"
These are the defaults: the quiet period, policy cache lifetime, batch limit
and upload time budget. Durations use Go duration syntax; nonpositive values
keep defaults and malformed durations are configuration errors. doctor
reports unknown keys. A repository's .agentfeedback.toml accepts only
[sessions] push = false: a veto, never permission to collect. An unreadable,
malformed or symlinked repository file, or an invalid sessions.push value,
blocks uploads rather than bypassing a possible veto.
init, doctor and sessions status discover the configured server's
session policy and include capture in JSON output. init prints the
status line on stderr; the other two print it on stdout. The org decides
whether processing is enabled; no new consent question is asked. The line
names the org, server, quiet period and retention, or explains why only
friction reports apply. Missing retention metadata means "per the org's
retention setting". Policy discovery itself does not upload transcripts;
an eligible CLI invocation can also trigger the automatic sweep.
The data-directory database caches policy per server and org in
capture_policy, including the consent start and last printed notice
version. Cached permission is bound to the configured credential; changing
the key requires fresh policy discovery. Concurrent refreshes cannot replace
a newer policy response with an older one. Disabled-to-enabled starts a new
consent period. Only successful enabled text output records the notice as
printed; JSON output does not. Unsupported
servers are retried no more than daily, even across these commands; a 429
waits for Retry-After (60 seconds when absent). doctor reports
unauthorized and suspended policy reads with a next step. Local mode makes
no policy request.
Use agentfeedback sessions push for manual transcript
uploads. --due applies the configured quiet interval, batch and budget;
named references bypass only the quiet interval. sessions list and
sessions status show upload progress independently of digest/mark progress.
doctor and sessions status also expose the last automatic push record.
The OSS server has no upload routes; its unsupported policy prevents the
automatic sweep. See Automatic sweep for
triggers and bounds.
MCP and discovery
POST /mcp: remote MCP server over Streamable HTTP, stateless (noMcp-Session-Id;GETandDELETEanswer 405). It takes the API key header like/api/v1/*(Authorization: Bearer <key>orX-Api-Key: <key>) and checks it before anything else. Six tools:submit_feedback,list_submissions,get_submission,stats,mark_processed,get_schema, each answering with the REST body of its route.agentfeedback mcpserves the same tools over stdio: on the local database with no key in local mode; with a server URL configured, it relays them to<server>/mcpwith the configured key (it connects at start and exits 1 on stderr when the server, the key or the endpoint does not answer). Either way it adds the session tools andsubmit_feedback'scontext_fromargument, answered on this machine (sessions.md), and the server instructions say so.--localforces the local database,--server URLthat server (the entry install writes for a server passes it). A forwarded call has 60 seconds; a call whose upstream session is gone reconnects and is retried once; a remote tool without an object input schema is not served (one warning on stderr).POST /mcp/{project}: the same, withprojectpreset for the connection;submit_feedback,list_submissionsandstatsthen take noprojectargument.get_submissionandmark_processedtake ids and are not scoped by the preset;get_schemaneither.- An MCP request body may be up to 10551296 bytes (the 10 MiB create limit
plus 64 KiB for the JSON-RPC message); a larger one is 413
request_too_large. The tools apply the create and mark limits to their arguments. GET /skill?format=skill-md|agents-md|promptandGET /.well-known/agentfeedback.jsonneed no key and carry no data.- Behind a reverse proxy, set
PUBLIC_URL, or forward the publicHostandX-Forwarded-Proto, so/skilland the MCP instructions name the address clients use.
Backups
Two complementary forms.
Physical backup (fastest restore, a complete database file):
cd ~/agentfeedback
docker compose exec agentfeedback /opt/agentfeedback backup /tmp/backup-$(date -u +%Y%m%dT%H%M%SZ).db
mkdir -p backups
docker compose cp agentfeedback:/tmp/backup-<stamp>.db ./backups/
The copy goes to the container's /tmp, not the database volume, and is
gone when the container is recreated (the image has no rm to delete it
sooner).
agentfeedback backup <dest.db> uses SQLite's VACUUM INTO, which is safe on
a live database in WAL mode, so the service keeps running. It refuses an
existing destination and a DATABASE_PATH that does not exist. Never copy
agentfeedback.db from the volume while the service runs; the -wal file
holds committed data the main file lacks.
Logical backup (portable, inspectable, the migration format):
curl -sS --fail -H "Authorization: Bearer $AGENT_FEEDBACK_API_KEY" \
"$AGENT_FEEDBACK_URL/api/v1/export" > feedback-$(date -u +%Y%m%d).ndjson
tail -n 1 feedback-*.ndjson # the trailer: {"export_complete":true,"count":…,"sha256":…}
The export is format 2: a header line, one line per record (tombstones included), and a trailer with the record count and the SHA-256 over the record lines. A file without the trailer is incomplete. Store backups off-host and restrict them: they contain everything agents reported.
The export carries records only; the session watermarks of
agentfeedback sessions (sessions_seen, triage_state) are not in it, and
a database restored from an export offers every session as new again.
agentfeedback backup copies the whole database, those tables included.
Restore and migration
Restore a physical backup: stop the service, replace /data/agentfeedback.db
in the volume and remove the -wal/-shm files beside it, from a throwaway
container (the image has no shell), then start it and check /ready:
docker compose stop agentfeedback
docker run --rm --volumes-from "$(docker compose ps -aq agentfeedback)" -v "$PWD/backups:/backups:ro" alpine:3.24.1 \
sh -c 'rm -f /data/agentfeedback.db-wal /data/agentfeedback.db-shm && cp /backups/backup-<stamp>.db /data/agentfeedback.db && chown 10001:10001 /data/agentfeedback.db'
docker compose start agentfeedback
Restore a logical export (format 2) into the service's database:
docker compose stop agentfeedback
docker compose run --rm -v "$PWD/feedback.ndjson:/import.ndjson:ro" agentfeedback import --dry-run /import.ndjson
docker compose run --rm -v "$PWD/feedback.ndjson:/import.ndjson:ro" agentfeedback import /import.ndjson
docker compose start agentfeedback
agentfeedback import [--dry-run] <file> verifies the whole file (header,
record count, digest, every record, each content_hash recomputed) before it
writes anything, then restores it in one transaction. Records keep their id,
uid, content_hash, payload bytes, timestamps and processing fields, and
tombstones stay tombstones; the id sequence moves past the highest id in the
file, so a restored id is never handed out again. A record whose uid is
already stored is skipped whatever the two contain (a mark or redaction in
the file does not reach the stored row), so running the same restore twice
changes nothing.
A record whose id, or whose (kind, key) with different content, belongs to
another stored record is skipped and listed under conflicts (id_taken,
key_mismatch); conflicts never fail the restore. It prints one JSON line
(imported, skipped, conflicts, warnings, first_id, last_id,
dry_run); --dry-run reports the same counts and writes nothing; a
rejected file writes nothing either, and a dry run against a DATABASE_PATH
that does not exist reports a restore into an empty database without creating
it. Stop the service first, as above: a restore, dry run included, holds the
database's only writer until it finishes. The file is read whole into memory,
so the host needs a few times its size free. Flags go before the file name.
Move to another server with agentfeedback migrate, run from any
machine whose client is configured with the source's URL and key:
printf '%s' "$TARGET_KEY" | agentfeedback migrate --to https://target --to-key-from-stdin \
[--kind K] [--include-kind K] [--since T] [--limit N] [--dry-run]
It pages the source's export and writes each chunk to the target's
POST /api/v1/import (openapi.yaml), which keeps uid,
content_hash, the timestamps and the processing fields and assigns new ids.
The target skips uids it already holds, so a re-run only adds what is
missing, and marks or redactions made on the source after a record moved do
not propagate. install-check rows stay behind unless --kind or
--include-kind names that kind. A record whose (kind, key) the target holds
under another uid is printed as one conflict line and skipped. The run
stops at the first failure and says how many records landed. --dry-run
sends no record (it still reads the target's metadata and tombstones): it prints the count per kind, the first record of each, and
the source tombstones the target still holds unredacted; redact those on the
target by hand. --to cloud targets the hosted service at
https://api.agentfeedback.dev (a public beta). --local reads the
data-directory database instead of the configured server: a switch from
local mode to a server never forwards the local reports by itself, so init --server and doctor --init name them under local_reports with this
command (init offers to run it on a terminal).
Upgrading and downgrading
The newest binary wins. Every binary migrates the database forward when it
opens it (serve at startup, a local-mode command on its first request), and
a binary older than the database's schema refuses it rather than writing rows
the schema no longer expects: the command exits 1 with database schema is newer than this binary supports, and client.jsonl gets an error line
whose reason starts with schema_too_new; submit spools its report instead
(Local mode). Harness hooks never open the local
database, so a stale hook binary shows up in agentfeedback doctor, which
lists the binary each harness's hook runs, its
version, and the agentfeedback on PATH, and reports a hook binary that is
missing, does not answer version --json, or is older than the one running
doctor when both versions are comparable releases. Git-describe, dirty and
Go pseudo-version builds also skip this hook-version comparison. Keep one binary per machine,
and after an upgrade rerun agentfeedback install <harness> from it when
doctor names an older one.
Upgrade: replace the binary (or bash scripts/deploy.sh <new-commit-sha> for
the image) after a backup. Schema changes are forward only; rolling back the
binary or the image does not roll back the schema.
Downgrade: with the newest binary, agentfeedback backup <copy.db> and
agentfeedback export > records.ndjson (or keep an export you already have);
then move agentfeedback.db and its -wal and -shm files aside (a
server with its own DATABASE_PATH points that at a new file instead) and
run the older binary's agentfeedback import records.ndjson, which creates
the database. Import keeps the ids. The backup stays as the copy to return
to.
From v3 to v4 the database starts over: a v4 binary refuses a v3 database
at startup and names the path. Keep the old database with the image that wrote
it, point DATABASE_PATH at a new file (a new volume), and start empty. A v3
export is not format 2, so agentfeedback import does not take it.
Key rotation
No overlap window. For the compose stacks, edit API_KEY in the host .env
and run docker compose up -d; for a server from serve --init, run
agentfeedback serve --init --force and the restart command of your form
(Bare binary). Then update the key on every client
(agentfeedback doctor --init --url <URL> --key-from-stdin, or
AGENT_FEEDBACK_API_KEY). Spooled payloads on clients retry with the new
key on their next call. Local mode has no key.
Retention
Nothing is deleted automatically. processed means acted on, not removed.
To purge, export first, then delete with an explicit predicate from a
throwaway container that mounts the service's volume (the image has no shell
and no sqlite3), for example rows processed more than a year ago. It is
safe while the service runs (WAL mode, a 5 s busy timeout); the final
chown keeps any file it created writable by the service's uid:
docker run --rm --volumes-from "$(docker compose ps -q agentfeedback)" alpine:3.24.1 sh -c 'apk add --no-cache sqlite >/dev/null && sqlite3 -cmd ".timeout 5000" /data/agentfeedback.db "DELETE FROM submissions WHERE processed_at < (unixepoch() - 31536000) * 1000000"; s=$?; chown 10001:10001 /data/agentfeedback.db*; exit $s'
Client spools live in ~/.local/share/agentfeedback/spool/ on each producer
and age out (frictions bound to a server after 20 hours; everything else,
local-bound frictions included, after 30 days): the next flush bound to an
entry's destination deletes it, which a local-mode command's start-up pass
does for local ones, and logs an error line in client.jsonl; an entry
bound to a server this machine no longer uses stays until a flush to that
server runs.
Monitoring
GET /health: process is up.GET /ready: database answers and schema is current;503while shutting down.GET /metrics(Prometheus):http_requests_totalandhttp_request_duration_secondsby route, method and code;submissions_created_totalby outcome;agentfeedback_submissions_unprocessed(the backlog);agentfeedback_db_bytes(database plus WAL size);agentfeedback_sqlite_busy_total(writes that waited out the busy timeout, should stay at zero).- Logs are JSON on stdout:
docker compose logs -f agentfeedback.
Uninstall
Two independent parts: the skills on each machine and the service (the compose stack below, or a service from server install). Take a backup before
removing any service; the data is gone with the volume.
Skills, on every machine that has them
- Run
agentfeedback uninstall allfirst: it removes whatagentfeedback installwired, theagentfeedback-triageskill it wrote included, and restores the files it changed. - Find the installed copies (
agentfeedback,agentfeedback-docs,agentfeedback-triage) in every harness skills directory you use, e.g.ls -la ~/.claude/skills | grep feedback. Entries may be symlinks into a shared checkout; remove the links, then the checkout if nothing else uses it. - Flush or discard unsent payloads first:
agentfeedback flushsends whatever is spooled; or delete~/.local/share/agentfeedback/spool/and~/.local/share/agentfeedback/rejected/to drop it. - Remove the directories or links, then
rm -rf ~/.cache/agentfeedback(the client log and digests), and the binary:rm "$(command -v agentfeedback)"(scripts/install.shputs it in~/.local/bin).~/.local/share/agentfeedback/also holds the local-mode database; back it up withagentfeedback backupfirst if the reports matter, then remove it. - Remove
AGENT_FEEDBACK_URL,AGENT_FEEDBACK_API_KEY,AGENT_FEEDBACK_MACHINE,AGENT_FEEDBACK_MODEL,AGENT_FEEDBACK_HARNESS,AGENT_FEEDBACK_SESSION_IDAGENT_FEEDBACK_REVIEW_DIRSandAGENT_FEEDBACK_TRIAGE_ROOTS(plusTYPESAFE_API_KEYif only triage used it) from shell profiles (grep -n AGENT_FEEDBACK ~/.zshenv ~/.zshrc ~/.bashrc ~/.profile 2>/dev/null). - Remove any directive in your agent system prompt that tells agents to submit friction, and any hook in a review runner that submits reviews.
The service
On the host, in the directory holding docker-compose.yml (default ~/agentfeedback):
cd ~/agentfeedback
docker compose exec agentfeedback /opt/agentfeedback backup /data/final.db && docker compose cp agentfeedback:/data/final.db ./final-backup.db # keep a copy elsewhere
docker compose down -v # stops the container and deletes the agentfeedback-data volume
docker image rm $(sed -n 's/^AGENTFEEDBACK_IMAGE=//p' .env)
cd ~ && rm -rf ~/agentfeedback # compose file, .env with the API key, local backups
Skip -v and the last line to keep the data for a later reinstall. Also
remove any reverse-proxy or firewall rule that exposed port 8090, and any
monitoring scrape of /metrics.
A service from server install
Back up first with the backup command server install printed (its next)
and move the backup out of the database directory (it is written beside the
database, /data for compose, which the last command below removes). Then
stop the service and remove its file, for the form you installed:
systemctl --user disable --now agentfeedback.service && rm ~/.config/systemd/user/agentfeedback.service && systemctl --user daemon-reload
launchctl bootout gui/$(id -u)/dev.agentfeedback.serve && rm ~/Library/LaunchAgents/dev.agentfeedback.serve.plist && rm -rf ~/Library/Logs/agentfeedback
docker compose -f ~/.config/agentfeedback/server/compose.yaml down --rmi all
Then remove the server directory (api-key, serve.env, compose.yaml) and
the database with its -wal and -shm files, or keep them for a later
reinstall: rm -rf ~/.config/agentfeedback/server ~/.local/share/agentfeedback.
Paths are the defaults; --dir, --db and the XDG_* variables move them.
If you ran loginctl enable-linger "$USER" only for this server, undo it with
loginctl disable-linger "$USER".
Verify
curl -s -o /dev/null -w '%{http_code}\n' http://<host>:8090/health must fail
to connect; docker ps -a | grep agentfeedback and docker volume ls | grep feedback must be empty; a fresh shell must have no AGENT_FEEDBACK_*
variables.
API
The HTTP API of an AgentFeedback server: write-once submissions from AI agents, their processing marks, aggregates, export and import, schemas, remote MCP and…
Security
By default AgentFeedback is one binary and one database owned by one OS user on one machine, with nothing listening (The client on a machine).