Headless CLI Client
engram is one binary that is both the server (engram serve) and a client for
that server. The three client verbs — engram search, engram list, and
engram store — let anything with a shell talk to a remote engram server
over the Connect API, with no MCP client involved: a subagent with a closed
tool list, a CI step, or a cron loop.
Get the binary through Install. To register an MCP connection
for an agent, follow Agent Setup and its release
availability notice. MCP setup uses --url / ENGRAM_URL; the Connect commands
below use --server / ENGRAM_SERVER_URL and the credential precedence described
here. Setup’s generic-only --token-file behavior does not apply to these
Connect commands.
The server must be running with the Connect lane mounted (connect.headless: true, or a UI-enabled deployment) — see Configuration.
The three verbs
Section titled “The three verbs”| Command | Purpose |
|---|---|
engram search --server <url> --query <text> --scope <scope> |
Vector search over stored memories |
engram list --server <url> --scope <scope> |
Paged, filtered recall |
engram store --server <url> --content <text> --scope <scope> |
Write a memory (the only write verb) |
Run engram <verb> --help for the full flag list of each command — every
flag mirrors a field on the corresponding Connect request message.
Shared flags
Section titled “Shared flags”All three commands accept the same five flags, each resolved flag-then-
environment-then-default through the same internal/config registry the
server side uses:
| Flag | Purpose |
|---|---|
--server |
Server base URL. Falls back to ENGRAM_SERVER_URL if unset. Required (from one or the other) — there is no localhost default. |
--token-file |
Path to a file containing the bearer credential. Falls back to ENGRAM_TOKEN (env wins over file if both are set). |
--insecure |
Skip TLS certificate verification. Always prints an unconditional warning to stderr — this cannot be suppressed and has no environment fallback, deliberately: it cannot be silently enabled by an inherited environment variable. |
--output |
Force "json" or "text". Default: JSON when stdout is not a terminal, a human table when it is. |
--timeout |
Bounds the RPC call. Falls back to ENGRAM_TIMEOUT. Default 30s. 0 is rejected as a usage error — it does not mean unbounded. See Request timeout below. |
Credential precedence, in order: ENGRAM_TOKEN environment variable, then
the file named by --token-file. There is no --token flag. This is
deliberate: a credential must never be able to reach argv, ps output, or
shell history. Omitting both is legal — an anonymous call against a
no-issuer server is a normal request, not an error.
Recall scope selection
Section titled “Recall scope selection”engram search and engram list each require exactly one of two flags — there
is no default that silently picks one for you:
| Flag | Purpose |
|---|---|
--scope |
Limit recall to one scope; scope is required unless cross_spine is true; omit and pass --cross-spine to span every scope you can read; mutually exclusive with --cross-spine. |
--cross-spine |
Span every scope you can read; mutually exclusive with --scope. |
Passing neither, or passing both, is rejected by the CLI itself — before any
network call — with exit 2. The server would in fact accept --scope
together with cross_spine=true, silently discarding the scope and logging
the discard at Info on the server side, where the calling agent never sees
it. The CLI is deliberately stricter than the server on this one combination:
an explicitly-typed filter being discarded without the caller learning about
it is exactly the kind of surprise this interface is designed to avoid, so
the client rejects the pair outright rather than forwarding it.
Paging engram list
Section titled “Paging engram list”engram list supports two independent paging styles — offset-for-UI and
cursor paging — never combined:
| Flag | Purpose |
|---|---|
--limit |
Max results per page; 0 resolves to the maximum, 1000; a value above 1000 is rejected. |
--offset |
Offset-for-UI paging; cursor_mode, offset, and page_token are mutually exclusive. |
--cursor-mode |
Opt into cursor paging on the first (tokenless) page; mutually exclusive with --offset and --page-token. |
--page-token |
Opaque cursor from a previous response’s next_page_token; mutually exclusive with --offset and --cursor-mode. |
Passing more than one of the three paging-mode flags is rejected by the CLI
itself — before any network call — with exit 2, via a declared cobra flag
group (the same mutual-exclusion enforcement mechanism as
--scope/--cross-spine above). engram search --k carries the same
contract as --limit above except for its own zero-value default: 0
resolves to 20, and a value above 1000 (the shared maximum) is rejected.
Either rejection surfaces as exit 2 (see Exit codes).
Output contract
Section titled “Output contract”Data goes to stdout as one JSON object per invocation, mirroring the Connect
response’s own field names (memories, total, next_page_token, id,
short_id, …). Every diagnostic — warnings, the --insecure notice,
errors — goes to stderr, so engram search ... | jq is always safe. An empty
result set is a success: engram search and engram list exit 0 with
"memories":[], never null.
On a --cross-spine call, text-mode output for both engram search and
engram list appends a coverage footer after the table:
searched_scopes: 3or, when the server reports the authorized span was truncated:
searched_scopes: 3 scopes_truncated: trueor, when the server’s own coverage-enumeration query failed after hits were already found — the call still succeeds, but there is no count to report:
scopes_unknown: trueThe footer reports a count of the scopes searched, never the scope names
themselves — except the scopes_unknown form, which carries no count
because there is none. It prints only on a --cross-spine call — output for
every other invocation is unchanged, byte-for-byte, from before this
capability existed. The JSON lane already carried searched_scopes and
scopes_truncated on every response before this release, now joined by
scopes_unknown, and is unaffected by this change beyond that addition.
Text-mode output for both commands also appends a recall-gate hidden-count footer, after the coverage footer, whenever the response reports hidden records:
recall_gate_hidden: 5 archived: 2 superseded: 2 expired: 1 scheduled: 1This one prints on any engram search/engram list text call, not only
--cross-spine — a scope-confined call can hide records too. It prints
nothing when no records were hidden or the count is unavailable, and the
JSON lane carries recall_gate_hidden under the same name ({total, archived, superseded, expired, scheduled}), so this addition needs no
separate rendering code there.
When the server runs the Jev reranker (ENGRAM_SEARCH_RANKER=jev, see
Search reranking (Jev)),
engram search text output gains a RELEVANCE column and JSON output gains a
per-memory relevance field — the provider’s probability, 0 to 1, that the
record answers the query; values all near zero mean nothing returned answers
it. Without it, output is unchanged byte for byte. engram list output
never carries relevance.
Operator commands
Section titled “Operator commands”Every operator command — reindex, prune-expired, summarize-missing,
backfill-short-ids, migrate (plus its status and revert subcommands;
see the Migrate guide), migrate-remap-owner, its
deprecated alias migrate-set-owner, setup, and every engram spine-review
leaf (currently scan, verify, consolidate, archive, restore, and
purge) — also accepts --output:
| Value | Behavior |
|---|---|
json |
Write exactly one JSON document to stdout. |
text |
Render a one-line prose headline followed by one aligned line per field of the same document json emits. This is a human-readable view, not a stable interface, and is not intended to be parsed. |
| (absent) | Detect from the command’s own configured output writer: a human terminal renders text; anything else (a pipe, a file redirect) renders json. |
| anything else | Rejected as a usage error (exit 2), naming --output and its legal values — the same validator the three client verbs use. |
As with the client tier, the JSON document goes to stdout and any warning or
diagnostic goes to stderr, so engram <operator-command> --output json | jq .
is always safe. A sweep that affected zero records still emits zero-valued
counters and [] for any list-shaped field — never null — and exits 0.
Both lanes derive from one serialization: the json document is produced
by encoding/json over the command’s own report struct, and text is a
rendered view of that same value — never a second, independently maintained
format. The json document cannot carry a fact text omits, and text
cannot show a field json withheld, because both are read from the same
value. The json lane is the contract; the text lane may change shape in
any release. A preview is always distinguished from an applied mutation by an
explicit boolean field plus separate count fields, never by prose alone.
Destructive commands
Section titled “Destructive commands”This applies to every command routed through the registerDestructive choke
point — a wider set than the blast-radius table’s
destructive column alone. Today that is every command the table classifies
destructive (prune-expired, migrate-remap-owner, spine-review purge,
migrate revert), plus migrate, which is classified
non-destructive (its sweep is additive-only) yet still previews by
default and mutates only under --apply — the same contract, extended to a
mutating-but-not-destructive command. migrate status is read-only and
carries no --apply flag at all. A bare invocation of any --apply-gated
command reports what the sweep would do and exits 0 without touching the
collection; add --apply to perform the mutation. A forgotten --apply is
therefore a harmless no-op — the command just previews again.
reindex and summarize-missing are classified non-destructive and
keep their pre-existing opt-in preview idiom, --dry-run. This is a
deliberate two-idiom split, not an accident: on a command routed through
registerDestructive (above) a forgotten --apply costs nothing (it just
previews again), but on reindex/summarize-missing a forgotten
--dry-run merely performs the recoverable, additive thing the operator
already asked for.
summarize-missing --scope <scope> (or --all-scopes) sweeps that scope
(or every scope) for records with no summary yet; --scope and
--all-scopes are mutually exclusive, matching every other operator command
that offers this pair.
backfill-short-ids has MOVED from the --dry-run idiom to the --apply
one: it is now a thin delegating alias for engram migrate — see the
upgrade guide’s ## Unreleased entry on
backfill-short-ids --dry-run removal for the full behavior break. Its
own blast-radius row is still non-destructive (the sweep is
additive-only), exactly like migrate itself above — the boundary
between the two idioms is therefore no longer purely the blast-radius
table’s Destructive column; it is “does this command route through
registerDestructive”, which today is every Destructive:true command
plus migrate and backfill-short-ids.
spine-review scan
Section titled “spine-review scan”engram spine-review scan --scope <scope> (or --all-scopes) reports an
inventory of the memory spine and never mutates on any path. --scope and
--all-scopes are mutually exclusive. Alongside the
total and a per-scope/per-category breakdown, it reports the health signals
an operator needs before deciding what to curate: how many records carry a
summary and how many do not, how many are superseded, expired, or archived
(three independently observable states, each read from its own field, so a
record is never double-counted into the wrong bucket), how many carry
citations and how many citations exist in total, and the count of distinct
non-empty owners.
That last number is what makes the Subject-less claim observable rather
than asserted: scan is built on the operator tier, never on the
Subject-gated Search/List, so a sweep that had been silently narrowed
to one caller’s bucket would report a single owner even on a collection
holding records from several. The report also carries scanned_at — the
one instant the sweep took at the top of the run and evaluated
expired/scheduled against — so a reader knows exactly what those counts
are relative to.
scan counts what recall hides. Superseded and archived records are
soft-hidden from search_memory/list_memory but still appear here, which
is the point: an inventory that inherited recall’s blind spots could not
tell you what needs curating.
spine-review verify
Section titled “spine-review verify”engram spine-review verify --scope <scope> (or --all-scopes) classifies
every stored citation into one of four tiers: valid, moved (the cited
excerpt still exists in the same file, at a different byte offset —
ordinary drift from an edit above the cited range, never treated as
breakage), broken (the file is missing, or the excerpt is gone from it
entirely), and unverifiable (a commit/url/repo citation, an empty
cached excerpt, a citation whose owning record names a different repo than
the working tree, or a ref this command refuses to read for safety —
every such citation carries the reason it was not checked, so a clean
report can never be read as coverage the run did not have). --scope and
--all-scopes are mutually exclusive. It never reads
outside the working tree it was run in — not even through a symlink — and
never widens its search past the file a citation names.
verify exits 0 by default even when it reports broken citations —
“the command worked” and “the data is healthy” are different questions.
Pass --fail-on <tier> to ask the second one from a CI step without
parsing the report: it exits a distinct code (7, see below) when the
named tier has at least one entry.
spine-review consolidate
Section titled “spine-review consolidate”engram spine-review consolidate --scope <scope> (or --all-scopes)
reports ranked near-duplicate candidate pairs — (record A, record B, score) rows sorted by score, highest first — using each record’s
already-stored vector: no text is re-embedded and no vector ever
crosses the wire from engram to Qdrant. Exactly one of --scope or
--all-scopes is required: they are mutually exclusive, and supplying
neither is rejected with exit status 2, like spine-review scan,
spine-review verify and summarize-missing (#508).
The structural ranking never merges, never mutates, and never labels a
pair a “duplicate.” It ranks candidates and stops — deciding whether two
records are the same fact is a judgment for the operator (or a future
semantic skill) to make with the ranked list in hand, not something this
structural sweep pre-decides. There is no clustering and no default
similarity threshold: --min-score bounds report SIZE, not correctness,
and its absence (the default) means no filtering at all — a pair with
a negative cosine score is reported just like any other. --top-k bounds
how many neighbours are considered per record (a small default; see the
flag’s own --help text for the exact number). The optional advisory
verdict described below MAY name any relation, including duplicate —
that is the verdict object doing its documented job, not the structural
ranking pre-labelling a pair.
With --all-scopes, a candidate pair may span two different scopes —
two scopes holding the same fact is exactly the duplication an operator
wants surfaced, so cross-scope pairs are reported, and each row names both
records’ scopes (a_scope/b_scope in JSON) so a within-scope duplicate
is never confused with a cross-scope one.
A scope with fewer than two records reports zero candidates and exits 0;
the JSON candidates array is [], never null, in that case.
Advisory verdicts
Section titled “Advisory verdicts”When
ENGRAM_DECISIONS_PROVIDER is
set (off by default), each candidate pair ALSO gets an advisory
relation verdict, surfaced to you and never acted on — consolidate still
never merges or mutates a record because of one. --no-verdicts skips
this pass entirely for one run: no record content is sent and no
verdict key appears anywhere in the report.
Each request carries, per record, up to
ENGRAM_DECISIONS_VERDICT_STATE_CHARS characters total of its summary
followed by its content (default 1500) — see
configure’s “What leaves your deployment”
for what that means for your deployment. The decider is asked which of
five relations best describes how the newer record relates to the older
one; the more recently created record of the pair is always sent as
record_b, so updates means the NEWER record updates the older one,
regardless of which side of the pair (a/b) it happened to land on in
the structural ranking:
| Relation | Meaning |
|---|---|
duplicate |
Both state the same fact; keeping both is redundant (one may be more complete). |
contradicts |
They make incompatible claims about the same subject; one corrects or reverses the other. |
updates |
The newer record (record_b) is a newer state or a more complete version of the same fact. |
related |
Same subject area, but different and compatible facts; both are worth keeping. |
unrelated |
Different subjects. |
A second question asks whether both records are about the same specific
subject — same_subject, a probability. A verdict whose own relation
probability falls strictly below ENGRAM_DECISIONS_VERDICT_THRESHOLD
(default 0.9, or --verdict-threshold to override it for one run) is
flagged needs_review.
A successful verdict’s JSON shape:
{ "relation": "duplicate", "probabilities": {"duplicate": 0.93, "contradicts": 0.01, "updates": 0.02, "related": 0.03, "unrelated": 0.01}, "same_subject": 0.97, "needs_review": false, "model": "typesafe/jev-1.13-20260917"}A failed request instead reports only its failure class:
{"error": "timeout"}--output text renders the same object as verdict=duplicate p=0.93 same_subject=0.97 probabilities=duplicate:0.93,contradicts:0.01,updates:0.02,related:0.03,unrelated:0.01 model=typesafe/jev-1.13-20260917, appending [needs review] when
flagged, or verdict unavailable (<class>) for a failure — text is a
rendered view of the same JSON object (D-05), never a second source of
truth.
<class> is one of: auth, bad_request, context_too_large,
rate_limited, unavailable, timeout, response_too_large,
malformed_response, invalid_request, canceled, error, or
state_unavailable (the record’s stored state could not be fetched from
engram’s own store, before any request left for the provider). A failed
verdict is reported per pair and never changes this command’s exit
status — consolidate still exits 0 even when every verdict fails.
--timeout bounds the whole sweep, including the verdict pass: a
deadline reached mid-pass reports every still-outstanding pair verdict unavailable (timeout) rather than aborting the sweep or dropping
candidates.
Before sending anything, consolidate prints one line to stderr naming the
decisions provider, model, endpoint host and the per-record character
bound; after the pass, one summary line with requested/answered/
needs_review/unavailable counts and, on any failure, a per-class
breakdown. Neither line ever reaches stdout — a piped --output json | jq . consumer never sees them, and script-based consumption of the JSON
report is unaffected either way.
Reader guidance, not a rule the renderer enforces: a related
verdict paired with a high same_subject probability is often worth a
second look — a genuine refinement or state change that falls just short
of a clean updates verdict tends to land there.
spine-review archive / spine-review restore
Section titled “spine-review archive / spine-review restore”engram spine-review archive --id <id> [--id <id> ...] explicitly retires
one or more records: it stamps archived_at (see
Archiving for the field’s full
contract), an entirely new key orthogonal to both expiry (not_after) and
supersession (superseded_by). engram spine-review restore --id <id>
reverses it. Both accept --id more than once, processing every id and
reporting a per-id outcome — there is no filter form; a mutating verb’s
blast radius is always the explicit id set an operator supplied, never an
implicit scope sweep.
Each id resolves to exactly one of three outcomes, reported honestly rather than inferred from an error string:
| Outcome | Meaning |
|---|---|
changed |
The record’s archived state was mutated (stamped or cleared). |
already |
The record was already in the target state — no write issued. |
not_found |
The id does not resolve to an existing record. |
An unknown id reports not_found on both verbs identically — there is
no asymmetry where one verb errors and the other silently reports success
for the same typo’d id. Archiving an already-archived record, or restoring
a never-archived one, is idempotent: it reports already and mutates
nothing.
Retained, not deleted. Neither verb removes a point, erases content, or
drops a vector. An archived record stays fully fetchable via get_memory
(and the MCP tools) — only recall (search_memory/list_memory/
search_discovery/list_scheduled) hides it, exactly like a superseded or
expired record. engram spine-review scan reports an archived count as
its own bucket, separate from expired — an archived record’s not_after
may or may not also be lapsed, but the two states are independently
observable and never folded together.
spine-review purge
Section titled “spine-review purge”engram spine-review purge deletes purge-eligible records — the phase’s
sharpest edge, and the only irreversible command in the spine-review
group. It previews by default (see Destructive commands
above) and mutates only under --apply.
Eligibility classes (--class, repeatable): superseded (a
superseded_by link to an existing record, past a grace window),
expired (not_after lapsed, past a grace window), and archived
(archived_at past a retention window — 90 days by default,
overridable with --older-than). A class-only run needs no --scope: a
class is a derivation, not an operator judgment.
The free-form filter path is gated harder:
the free-form filter path requires an explicit --scope or --all-scopes: category or tags always engage it, and older-than engages it when no class is selected.Note the asymmetry, which is deliberate. --older-than alongside a
--class is read as parameterizing that class’s window (overriding
archived’s 90-day retention, say), so it does not engage the harder gate.
--category and --tags engage it even when a class is also selected,
because narrowing by category or tag is a semantic judgment no structural
class expresses — and D-10 gates on how much judgment the operator supplied,
not on how many records the run would touch. A --class superseded --category decision run is therefore narrower than --class superseded
alone yet still requires an explicit scope.
The extract-before-delete gate (rule 7smp8vy9hr). Every candidate
must satisfy one of two paths before it can be deleted: a server-set
superseded_by link naming a later record (unforgeable — this is the
identical field Store.Supersede writes and Update preserves, never a
caller-supplied tag), or an authoritative milestone-summary record
covering the batch. The per-record path and the batch floor carry
different strength: the per-record link cannot be satisfied by anything a
caller writes, while the batch floor’s marker is a caller-mintable tag
convention over a real, server-timestamped record — strictly stronger than
no artifact at all, strictly weaker than the per-record link. Neither is
ever presented as proof.
Preview and apply happen within ONE invocation. purge carries its
preview into --apply through an in-process manifest — there is no
--manifest/--token flag, and a preview cannot be carried into a later
invocation. --apply re-derives eligibility within its own run and
deletes only the intersection of what it just showed and what is
still eligible at that moment; a record that became ineligible since
preview is spared, and a record that became newly eligible is reported
appeared (never deleted — re-run to include it). Because the two
derivations run milliseconds apart in one process, the intersection
guards against a concurrent writer, not against operator delay — the
extract gate, the mandatory --scope on the filter path, and the
discovery/rule category exclusions carry the real safety weight.
Cross-invocation preview is a possible future ADDITIVE change requiring a
signature; it is not shipped here.
Exit codes
Section titled “Exit codes”The CLI uses the following exit-code meanings. Codes 8 and 9 belong to
setup, available from v0.16.0. See
Agent Setup for availability and result handling.
Code 10 comes from a client verb whose server response overflowed what one
response can carry — in practice list and search — or from an operator
command whose own Qdrant read overflows (Phase 5 of this milestone bounds
those sweeps).
| Code | Meaning |
|---|---|
| 0 | Success (including an empty result set) |
| 1 | Unclassified internal error — a backstop, not a general-purpose failure code. Reached by exactly two paths (see caution below). |
| 2 | Usage or validation error — a bad flag value, a violated mutually-exclusive flag group, or engram’s own semantic validation (a missing --server/ENGRAM_SERVER_URL, an invalid --output value, an empty required flag) |
| 3 | Authentication or authorization failure |
| 4 | Not found |
| 5 | Transport or server unavailable |
| 6 | Request deadline exceeded — the server accepted the request but did not answer within --timeout |
| 7 | Findings reported under an explicit opt-in flag (e.g. spine-review verify --fail-on) — the command itself succeeded; the data just didn’t pass the check |
| 8 | Setup partially failed: some attempted runtimes succeeded and some failed |
| 9 | All attempted setup runtimes failed |
| 10 | Response too large — the server’s result exceeded what one response can carry (Connect resource_exhausted, hint response_too_large); retry with a smaller --limit or --k, or without --full; retrying the same request fails the same way. |
Absent runtimes are skipped and do not count as failed setup attempts.
Request timeout
Section titled “Request timeout”Every client verb (search, list, store, get) bounds its RPC call with
--timeout (or ENGRAM_TIMEOUT), default 30s. 0, a negative value, and
a malformed duration are all rejected as usage errors (exit 2) before
any dial — --timeout 0 together with an unreachable --server still exits
2, not 5. A server that accepts the connection but never answers within
the window reports exit 6, distinct from exit 5 (the server refused the
connection, or was never reachable at all).
The operator commands’ own --timeout is a different flag with different
zero-semantics, and it is not uniform across commands:
| Commands | --timeout meaning |
0 behavior |
|---|---|---|
search, list, store, get, migration-status |
Per-RPC-call deadline | Rejected (usage error) |
reindex, prune-expired, summarize-missing, backfill-short-ids, spine-review scan, spine-review verify, spine-review consolidate, spine-review archive, spine-review restore, spine-review purge, migrate, migrate status, migrate revert |
Whole-sweep wall-clock budget | Disables the deadline (unbounded), unchanged |
migrate-remap-owner, migrate-set-owner |
Whole-sweep wall-clock budget | Rejected (usage error) — changed this release, see the upgrade guide |
A reader comparing engram search --help against engram reindex --help
and engram migrate-remap-owner --help side by side should not have to
infer this table from the flag’s one-line usage text — the three groups
genuinely disagree on what --timeout 0 does.
migrate --apply and backfill-short-ids --apply budget TWO full-backlog
passes under one --timeout: a fresh DryRun preview (which additionally
performs a MintShortID collision-probe Count per eligible record), then
the manifest-limited apply pass — with no way to allocate time between the
two. Size --timeout for both passes together, not just the write you
expect.
migrate revert --apply budgets an even larger share under one
--timeout: the CLI’s own whole-range preflight, Store.Revert’s
independent SECOND whole-range preflight (repeating the identical exhaustive
scan before it is allowed to write anything), and then the write-convergence
loop itself, which re-derives and re-scrolls on every pass until the backlog
reaches zero. An operator sizing --timeout for “one reverse walk of my
backlog” is actually budgeting for at least two full read-only passes over
that same range before any inverse is even applied.
The self-describe catalog
Section titled “The self-describe catalog”Run engram with no arguments. It writes one JSON document to stdout and
exits 0: every command in the live binary, every flag with its type,
default, and usage, and this same exit-code table — derived from the running
binary’s actual command tree and exit-code constants, not maintained by
hand. engram --help is unaffected and still prints ordinary human help.
This catalog is the machine-readable, authoritative form of everything on this page. When the two disagree, trust the binary — this page is prose describing it, not the contract itself.
engram | jq '.commands[] | select(.name == "search")'Blast radius
Section titled “Blast radius”Every command in the catalog carries a blast_radius object:
"blast_radius": { "read_only": true, "destructive": false, "idempotent": true, "open_world": false}This is the same taxonomy the MCP tool annotations publish — readOnlyHint, destructiveHint, idempotentHint, and openWorldHint — read from the same table, so an agent that has already learned to branch on one lane’s hints applies the identical logic on the other. The JSON keys are snake_case rather than the MCP wire’s camelCase, so the two forms rhyme without being byte-identical.
Each hint takes the conservative stance: a value is true only if it holds under every valid invocation of that command, not merely the common case. migrate-remap-owner, for example, is "destructive": true because its --from <value> form can overwrite an existing, non-empty owner — even though its --from-missing form only ever fills an empty one.
This classification is a discoverability aid, not an authorization mechanism — it tells an agent what to expect, it does not gate what the agent is allowed to run.
See also
Section titled “See also”- Configuration —
connect.headlessand the other server-side settings the Connect lane depends on.