hadron CLI¶
Command surface and stable contracts of the hadron command-line
interface. Installation and sign-in live in
Install the hadron CLI. The CLI also
documents itself: hadron agentic-usage prints the complete agent-facing
contract, and hadron <command> --help covers every flag.
Source: hadron-memory/hadron-cli.
Command surface (v1)¶
hadron auth login | logout | whoami | status | token create|ls|validate | token revoke <id>
hadron memory ls [--shared-with-me] | get <id-or-urn> | set [<id-or-urn>] [--org <ref> | --owner-me | --app <ref> --agent <ref>] [--class <c>] [--max-rev-count <n>] [--schema <json> | --schema-file <path>] | attach <memory> --app <ref> --agent <ref> | set-active <id-or-urn> | rm <id-or-urn> | clone <id-or-urn> --target-urn <org:slug> | extract (<parentRef> | <loc> -m <memory>) <targetUrn> [--move] | export <id-or-urn> [--out <dir>] | validate <memoryRef> [--check <kind>]… [--limit N] [--fail-on-findings] | encrypt <memory> --data-key - | link-user <memoryRef> --external-user <id> [--data-key -] --yes
hadron memory member ls <memory> | member add|set-role <memory> --user <id> --role <r> | member rm <memory> --user <id>
hadron memory share ls <memory> | share create|set-role <memory> --grantee <user-ref> --role <r> | share rm|revoke <memory> [--grantee <user-ref>] [--yes]
hadron memory subscription ls <memory> | subscription create|set-role <memory> --org <id> --role <r> | subscription rm <memory> --org <id>
hadron access check <user> <resource> # effective access for one (user, resource) pair, with the grants that confer it
hadron node ls [-m <memory>] | get <node-urn> | add | update <node-urn> | move <node-urn> (--to-urn <urn> | --to-memory <memory>) | clone <node-urn> (--to-urn <urn> | --to-memory <memory>) | merge <node-urn> --into <urn> [--field <f>]… [--delete-source] --yes | rm <node-urn> [--hard] | export <node-urn> [-o <file>] [--format md|json] | import [<file>|- | --url <url>] [-m <memory>] [--loc <loc>] [--as-content] [--with-edges] [--task <ref> [--task-args <json>] [--app <ref>]]
hadron node revision list <node-ref> [-m <memory>] [--limit N] | revision get <revision-id> | revision restore <revision-id> [--truncate [--yes]] | revision label <revision-id> --label <text> | revision delete <revision-id> [--yes] | revision clear <node-ref> [-m <memory>] [--yes]
hadron edge ls <node-urn> | add | update <edge-id> | rm <edge-id>
hadron object create -m <memory> --type <t> (--fields '<json>' | --fields-file <path>) [--key <seg>] [--name <n>] | get <ref> | update <ref> (--fields '<json>' | --fields-file <path>) [--reason <text>] | find -m <memory> --type <t> [--match '<json>'] [--where '<json>'] [--sort '<json>'] [--limit N] [--offset N] | delete <ref> [--hard] [--yes]
hadron asset ls -m <memory> [--mine] [--mime <type>] [--include-deleted] [--limit N] [--offset N] | get <asset-ref> [-o <path>|-] [--force] | url <asset-ref> [-m <memory>] | upload <file> -m <memory> [--mime <t>] [--name <n>] [--description <d>] | rm <asset-ref> [--yes] | restore <asset-ref> | link <asset-ref> --node <new-node-urn> [--name <n>] [--description <d>]
hadron search <query> [-m <memory>]… [--mode hybrid|keyword|vector|regex] [--prefix <loc>] [--type <t>] [--tag <t>]… [--limit N] [--offset N] [-l]
hadron replace text <old> <new> --field <f>… (--node <node-urn> | -m <memory>)… [--prefix <loc>] [--regex] [--ignore-case] [--max-nodes N] [--reason <text>] [--dry-run] [--yes]
hadron task run (<node-urn> | <loc> -m <memory>) [--arg k=v]… [--app <ref> [--as-self]]
hadron chat read [--since <seq>] [--node <urn> | -m <memory> --messages-loc <prefix>] | post (--body <text|-> | --body-file <path>) [--node <urn>] [--handle <h>] [--identity <i>] [--role <r>] [--reply-to <loc>]
hadron team init -m <team-memory>
hadron team persona create --role <role> [--name <n>] [--team-agent <ref>] (uses --app) | list [--org <ref>] [--role <r>] | get <name-or-ref> | retire <name-or-ref> --yes
hadron team session start --as <persona> [-m <team-memory>] [--repo <r>] [--branch <b>] [--transcript <path>] [--host <h>] [--tool <t>] [--model <m>] [--force] | whoami | log (--pr | --issue | --commit | --branch) <ref> [--action <a>] [--detail <json>] [-m <team-memory>] | end [--summary <text>] [--session <id>] | list [--active] [--as <persona>] [--repo <r>] [--limit N] [--offset N] | list (--pr | --issue | --commit | --branch) <ref> [-m <team-memory>]
hadron team chat post <body|-> [--reply-to <seq>] [--as-me] (uses --app or the binding) | read [--since <seq>] [--mentions-me | --mentions <ref>] (uses --app or the binding)
hadron spec ls [-m <memory>] | get <citation>|--prefix <p> | describe | use [<memory>] | register [--check] | find <query> [--match-exactly] | grep <pattern> [--regex] [-i] [--field content|abstract] [--prefix <loc>] | replace <pattern> <replacement> [--regex] [--word-boundary=false] [--dry-run] [--yes] [--max-specs N] | new … | edit <citation> | extract <citation> --to-feature <fff> | link <from> <to> | lint [<citation>] | check-tools [--prefix <loc>] | citations [--src <path>]… [--exclude <glob>]… [--loose] [--stale-abstracts] [--strict] | supersede <citation> | import spec-kit|code
hadron coding review list -m <memory> [--root <loc>] [--broken] | review create <check-name> -m <memory> --trigger <cond> --description <d> [--scope <s>] [--tag <t>]… [--link <ref>[=<label>]]… [--seq N] [--content <text|-> | --content-file <path>] | review lint -m <memory> [--root <loc>] [--toolchain <t>|-] [--strict] [--suggest] [--fix [--yes]] | preflight list -m <memory> [--root <loc>] [--broken] | preflight create <loc> -m <memory> --route <action> --description <d> [--name <n>] [--symptom <s>] [--section <heading>] [--type <t>] [--tag <t>]… [--link <ref>[=<label>]]… [--seq N] [--content <text|-> | --content-file <path>] [--no-back-edge] [--no-body-line] [--dry-run] | preflight route <node-ref> -m <memory> --route <action> [--description <d>] [--symptom <s>] [--section <heading>] [--no-back-edge] [--no-body-line] [--dry-run] | preflight lint -m <memory> [--root <loc>] [--strict]
hadron agent ls [--org <id>] [--type ASSISTANT|CHATBOT] [--visibility ORGANIZATION|PERSONAL|PUBLIC] | ls --public [--type <t>] [--limit N] [--offset N] | get <ref> | create --org <id> --name <n> [<field flags>] | update <id> [<field flags>] | rm <id> --yes
hadron run trigger --app <ref> --entry <node-urn> [--arg k=v]… [--as-self] [--ai-config <name>] [--wait [--wait-timeout <dur>]] | get <id> | list [--app <ref> | --org <ref>] [--status <s>] | cancel <id> [--yes]
hadron schedule create --app <ref> --name <n> --cron '<expr>' --entry <node-urn> [--tz <IANA>] [--arg k=v]… [--as-self] [--policy <json>] [--disabled] | list [--app <ref>] | update <id> [--cron …] [--entry …] [--enabled=false] | rm <id> [--yes]
hadron webhook create --app <ref> --name <n> --entry <node-urn> [--args-schema <json>] [--as-self] [--policy <json>] [--disabled] | list [--app <ref>] | rm <id> [--yes] | rotate <id> [--yes]
hadron ticket mint --org <ref> --action comm.outbound --count <n> [--app <id>] [--note <text>] [--expires <iso8601>] | list --org <ref>
hadron grant create --org <ref> --user <ref> --action <a>[,…] [--expires <iso8601>] | ls [--org <ref>] [--user <ref>] | revoke <id> --yes
hadron app ls --org <org> | install | uninstall <id> | use <urn>
hadron ai-config ls [--app <id-or-urn>] [--agent <id-or-urn>] | create ((--app|--agent|--org <id-or-urn>) --name <n> --provider <p> --model <m> [--api-key -] | --file <path>) | update <id> … | rm <id>
hadron mcp-server ls [--org <ref>] | get <id> | tools <id> | create --org <ref> --slug <s> --name <n> --url <u> [--header 'Name: value']… [--allow <tool>]… [--disabled] | update <id> [--name <n>] [--url <u>] [--header …]… [--clear-headers] [--allow <tool>]… [--clear-allow] [--enabled|--disabled] | rm <id> --yes
hadron connection grant create --connection <ref> --app <ref> --scopes <s>[,…] [--expires-at <iso>] | grant ls [--connection <ref>] | grant revoke <grant-id> --yes
hadron secret create --name <n> --scope user|org|app|memory [--owner <ref>] --kind generic|webfetch-auth [--value-file -|@file] | ls --scope <s> [--owner <ref>] | rm <id> --yes
hadron org create --name <n> --urn <urn> | get <id> | public <org-ref> | update <id> | rm <id> | member ls <org-id> | member add|set-role <org-id> --user <id> --role <r> | member rm <org-id> --user <id> | invite create <email> --org <id> --role <r> | invite accept <slug> | invite show <slug>
hadron user search [query] [--limit N] [--offset N] | set-roles <userRef> --role <r>… --yes | merge <source> --into <target> --yes
hadron profile set [--name <n>] [--email <e>] [--handle <h>]
hadron config get | set | list
hadron api <query-or-mutation> # raw GraphQL escape hatch
hadron server-info # the server's version + capability surface
hadron version
hadron completion <shell>
hadron agentic-usage # prints the agent contract
Global flags¶
| Flag | Effect |
|---|---|
--json |
Machine-readable output on stdout; progress and errors on stderr (see --json on the failure path). Field names are stable: new fields may be added, existing ones are never renamed or removed without a major version bump. |
--server <url> |
Hadron server base URL for this invocation (default https://srv.hadronmemory.com; persist with hadron config set server). |
--app <urn> |
App URN context for this invocation (persist with hadron app use). Most calls need none. |
With --json, errors are emitted on stderr as
{"error":{"code":<exit-code>,"message":"..."}} — with one exception, and a
consequence for stdout that scripts need to know about; both are covered under
--json on the failure path.
Exit codes (stable contract)¶
| Code | Meaning |
|---|---|
| 0 | success |
| 1 | generic failure |
| 2 | usage error (bad flags/arguments, missing --yes) |
| 3 | authentication required or rejected |
| 4 | not found (or not visible to this principal) |
| 5 | conflict or validation failure (e.g. duplicate install; spec lint / spec check-tools / register --check findings) |
| 6 | cancelled / timed out waiting for the user |
Scripts and agents should branch on exit codes, not parse error text.
--json on the failure path¶
Branching on the exit code matters more than it looks, because --json does
not guarantee that stdout holds JSON when a command fails.
| Situation | Exit | stdout | stderr |
|---|---|---|---|
| Success | 0 | JSON payload | progress / notes, if the command emits any |
A single-entity read fails (node get <missing>, memory get <missing>, spec get <missing>) |
4 | empty | {"error":{...}} |
Bad input (node get 'not-a-urn') |
2 | empty | {"error":{...}} |
| Server or transport failure (the server is down, a gateway returns 5xx) | 1 | empty | {"error":{...}} |
A batched read has some unavailable refs (node get <a> <b> …) |
4 | JSON payload, with the misses listed under unavailable |
progress / notes, if any |
| A list read finds nothing, including in a memory that doesn't exist or isn't visible | 0 | [] |
progress / notes, if any |
An unknown flag (--nosuchflag) |
2 | empty | plain text, not JSON |
Four practical consequences:
- Parse the exit code before parsing stdout. A script that pipes stdout straight into a JSON parser will fail with a decode error at character 0 — which looks like corrupt data rather than the actual cause. The real message is on stderr.
- A non-zero exit does not always mean stdout is empty. Batched reads
deliberately return their payload and a non-zero code, so partial results
are still usable: read
unavailableto see which refs missed. - An empty list is not an error. Listing a memory that does not exist — or
that the caller cannot see — returns
[]and exit 0, deliberately, so the CLI never reveals whether a resource exists. Do not treat[]as a failure, and do not infer existence from it. - Output on stderr is not a failure signal. Successful commands write
progress and diagnostics there too — the affected-node count printed before a
replace text --yeswrite, or the notesearchemits when it degrades to keyword mode on a memory with no vector index. A script that treats "stderr is non-empty" as "it failed" will misread a normal run. Use the exit code.
Unknown-flag errors are the one case that ignores --json entirely: flag
parsing fails before --json is bound, so the message is plain text. Handle
exit 2 without assuming the stream is parseable.
Partial writes exit non-zero¶
A command that writes its primary entity but can't complete a secondary
step reports the partial success on stdout and exits 1 — it never
prints a clean success on a half-finished write. This covers the
edge-wiring commands: node import --with-edges, spec new,
spec extract, and spec supersede. The node (or spec) is written and the
unwireable edges are reported (unwiredEdges in --json, each with a
reason), but the non-zero exit tells a script the graph isn't fully
connected. spec supersede --json additionally reports each edge's
status (created / failed / skipped). Re-run once the missing
endpoints exist — edge wiring is idempotent.
memory set --slug on create has the same shape for a different reason: since
createMemory can't set the slug, a custom slug is applied by a follow-up
rename, and a failed rename leaves the memory under its name-derived slug. The
memory is reported and the command exits non-zero — re-run
memory set <urn> --slug <slug>.
Reference conventions¶
- Memory references are
org:memoryURNs (e.g.acme.com:kb, or the flatacme.com:kbform) or IDs — every command that takes an ID also accepts the URN. - Node references are fully-qualified URNs, flat and single-colon:
hrn:node:<root>:<memory>:<loc>(e.g.hrn:node:acme.com:kb:findings:flaky-ci). Keep thehrn:node:prefix (legacyurn:node:and the v1::form are also accepted). The prefix is what makes the reference unambiguous: the loc keeps its own colons (findings:flaky-ci), so without the type word there is no way to tell where the memory slug ends and the loc begins — a bare flat node ref is rejected for that reason. A bare loc is accepted only when you also pass-m <org:memory>(the flag fixes the arity, so bare is fine there); without-m, a bare loc is rejected with exit 2, since the same loc can exist in several memories. See URN composition. - Edges are directed, first-class entities: each carries an optional
name(the relationship) and alocthat is its identity (hrn:edge:<org>:<memory>:<loc>).edge add --from <node> --to <node>creates an edge (optionally--name <relationship>,--loc,--description, or--runnable);edge update/edge rmaddress it by its edge ID (shown byedge lsand innode get --json). A nameless edge prints its loc. Cross-memory edges are allowed. - Null/omitted flags mean "unset" — update commands only change the fields you pass.
Write semantics¶
memory setcreates when called without a positional argument (requires--organd--name) and updates when given one. On create, the URN slug is derived from--name(kebab-cased, e.g."Project KB"→project-kb) unless you pass--slug <bare-slug>to set it explicitly (so a display name and slug can differ in one command); the output echoes the resulting URN and the server-assignedclassandvisibilityso you can confirm the effective values (an unset--classdefaults toknowledge; an unset--visibilitytakes the server default). Because create has no slug input server-side, a custom--slugon create is a create plus a rename: if that rename fails the memory still exists under its name-derived slug and the command exits non-zero (a partial write) naming the retry. On update,--slugrenames the memory, changing its URN and the URNs of every node and edge under it (their ids are stable, so links survive).- Three ways to create a memory.
memory setpicks one by which owner flags you pass:- Free-standing, org-owned —
--org <ref> --name <n>(the default described above;--classdefaults toknowledge). - User-owned —
--owner-me --name <n>creates a memory with no org, owned by you in your own handle namespace, so its URN roots on your handle (hrn:mem:<handle>:<slug>). Owner-only:--class personal|private(defaults topersonal);knowledge/groupstill require--org. The server derives the handle and bare URN — never construct it client-side. - App-scoped —
--app <ref> --agent <ref> --class app|personal|private --name <n>. Both refs accept an ID or a URN, and the Agent must be installed in the App. App-scoped create rejects--slug: App-class URNs are name-derived by the server, while personal/private URNs use a per-owner opaque id.
- Free-standing, org-owned —
memory attach <memory> --app <ref> --agent <ref>binds an existing free-standingpersonal/privatememory to that App and installed Agent. The memory must be caller-owned, and it keeps its URN, class, and owner — attaching re-scopes access, it doesn't re-address the memory. Typed server errors distinguish already-scoped, cross-org, membership, and install failures.node addfails if the loc already exists;node updatepreserves unset fields. Content comes from--content "<text>",--content -(stdin), or--content-file <path>.- The
databag — replace vs. merge.--data '<json>'/--data-file <path>replace the node's wholedataobject (omit to preserve,--data nullto clear). To merge a few top-level keys while keeping the rest, use--data-merge '<json>'(-reads stdin) or--data-merge-file <path>: the patch overwrites the keys it names and leaves the others intact. The merge is shallow — a nested object value is replaced wholesale, not deep-merged — the patch must be a JSON object, and--data-mergeis mutually exclusive with the--datareplace. (This is the CLI counterpart of thehadron_update_node_dataMCP tool.) --runnable(the node flag).isRunnablegates whetherhadron task runwill execute a node.node addandnode updatetake--runnableto set it; onupdateit is tri-state —--runnablesets true,--runnable=falseclears it, omitting it preserves the current value.node getprints arunnable:line (and theisRunnablefield in--json);node lsadds aRUNcolumn (✓ for runnable) plusisRunnablein--json, andnode ls --runnablefilters server-side to runnable nodes (--runnable=falsefor the explicitly non-runnable; omit for all). This--runnableis distinct from theedge add --runnableflag above, which marks an edge runnable.- Restructuring nodes.
node move <urn> (--to-urn <urn> | --to-memory <memory>)relocates a node and its whole loc-subtree (new loc and/or memory, keeping every node's identity);node clonecopies it to a new loc/memory; both scope a bare source<loc>with-m, and the destination is always the explicit--to-urn/--to-memory. A cross-memory move (--to-memory, or a--to-urnin another memory) carries the subtree's embeddings and rewrites its edges into the destination, but is refused (safe subset) when either the source or the destination memory is encrypted or Git-backed, the subtree contains a chat-root node, or a moved node would break the destination memory's schema. A collision refuses with an "already exists" error.node merge <urn> --into <target>folds one node into another —--field <f>(repeatable) picks which fields to carry over, and--delete-sourceremoves the source after.node mergemutates the target, so it requires--yesnon-interactively whether or not you pass--delete-source. user merge.user merge <source> --into <target>globally consolidates a duplicate source user into the surviving target and returns the target;<source>is soft-deleted,--into <target>survives. Each reference is a user id, bare handle, orhrn:user:<handle>URN, passed through verbatim (the server resolves and authorizes — platformADMIN/OWNER, or an orgADMIN/OWNERover both members). There is no server dry-run, so it requires--yesnon-interactively.- Soft vs. hard delete.
node rmsoft-deletes by default — the node disappears from reads but is recoverable from revision history.node rm --hardremoves the row entirely, cascading its edges and revision history; it's irreversible and prompts with a distinct warning. - Destructive commands (
memory rm,memory encrypt,node rm,node merge,user merge,node revision restore --truncate,node revision delete,node revision clear,edge rm,node importwhen it overwrites an existing node,app uninstall,ai-config rm,org rm,org member rm,memory member rm,memory share rm,auth token revoke,grant revoke,spec supersede,run cancel,schedule rm,webhook rm,webhook rotate) prompt on a terminal and require--yeswhen run non-interactively. Without it they exit 2. (Anode importthat creates a new node is never gated.)
Node revision history¶
Every content-changing write to a node snapshots the prior state as a
revision, so edits are recoverable. node revision is the CLI over that
history:
hadron node revision list <node-ref> [-m <memory>] [--limit N]
hadron node revision get <revision-id>
hadron node revision restore <revision-id> [--truncate [--yes]]
hadron node revision label <revision-id> --label <text>
hadron node revision delete <revision-id> [--yes]
hadron node revision clear <node-ref> [-m <memory>] [--yes]
listshows the revisions of a node (newest first), each with its id, who edited it, and any label.getprints one revision in full.restorerolls the node back to a revision. By default it's non-destructive — the pre-restore state is itself snapshotted, so a restore is undoable.--truncateinstead collapses every revision newer than the selected one (which becomes the new baseline), discarding that forward history — so it requires--yes.labelattaches a human note to a revision (e.g.--label "before the auth refactor") so you can find it later.deleteremoves a single revision;clearremoves a node's entire history and prints the count purged. Both are destructive (--yes).
Reads (list, get) need memory read access; the mutations need memory
write access. A soft-deleted node is hidden from history reads, but its
history stays purgeable via delete / clear (the write path). The number
of revisions kept per node is capped by the memory's --max-rev-count
(hadron memory set … --max-rev-count <n>); the oldest overflow is pruned
as new revisions land.
Running tasks¶
A task is a runnable node (isRunnable: true) — usually a prompt template
whose accepted --args it declares in Node.data.args. hadron task run
(<node-urn> | <loc> -m <memory>) [--arg k=v]… has two modes:
- Render (default). Compiles the node's template with your
--args and prints the result — the instructions for you to run. - Execute (
--app <ref>). Instead of rendering, the server mints aMANUALheadless run of the task under that App (a real LLM run) and prints the run id; follow it withhadron run get <id>.--as-selfruns it on behalf of you (reaches your personal memories; authenticated user only). In--json,modeis"render"or"execute", and execute mode addsrunId.
node import --task <ref> is the same execute path bolted onto an import: after
the node is stored, the server runs <ref> against it (--task-args <json>,
--app <ref>) and returns the run id as jobId.
Search¶
hadron search <query> retrieves nodes ranked by relevance — the CLI's
first-class search, distinct from spec find (which is scoped to spec
corpora). The default mode is hybrid (semantic + keyword, fused); on a
memory with no vector index it degrades to keyword and prints a note on
stderr.
hadron search <query> [-m <memory>]… [--mode hybrid|keyword|vector|regex] \
[--prefix <loc>] [--type <t>] [--tag <t>]… [--limit N] [--offset N] [-l] [--json]
--modeselects the ranking:hybrid(default),keyword(stemmed full-text with boolean operators — uppercaseAND/OR/NOT, quoted phrases,-term),vector(semantic only), orregex(POSIX over literal fragments).-m/--memory(repeatable) scopes to one or more memories by ID or URN; omit to search everything you can access.--prefixfilters by node loc prefix;--typeby node type;--tag(repeatable) by tag.--limitcaps hits (default 15;0uses the server default);--offsetpages.- Each hit carries a relevance score plus the node's description and
abstract in
--json, so results are assessable without a follow-upnode getper hit.-l/--longprints the abstracts in the text output too.
hadron search "how do users report a bad actor" -m micromentor.org:mmdata
hadron search "(auth OR login) AND token" --mode keyword --prefix findings:
hadron search 'reportUser|contentConcern' --mode regex --limit 30 --json
Object store¶
hadron object is the collection-oriented CRUD-and-query surface over
structured storage — the legible "records, not graph
nodes" projection. An object is a node with an objectType, presented as a
flat record { id, type, ...fields }: id/type are the reserved envelope,
the node's typed properties are the top-level fields, and loc/name are
auto-derived and hidden. On a memory with a declared schema, writes are
validated against the collection. The command also answers to objects and
obj.
hadron object create -m <memory> --type <t> (--fields '<json>' | --fields-file <path>) [--key <seg>] [--name <n>]
hadron object get <ref>
hadron object update <ref> (--fields '<json>' | --fields-file <path>) [--reason <text>]
hadron object find -m <memory> --type <t> [--match '<json>'] [--where '<json>'] [--sort '<json>'] [--limit N] [--offset N]
hadron object delete <ref> [--hard] [--yes]
create—--fieldsis the record as a JSON object (or--fields-file).--keysets a natural id (a single loc segment, no:); omit for a server-generated id.--nameoverrides the auto-derived node name.idandtypeare reserved and can't be field names. Prints the flat record.get <ref>—<ref>is an object id or a node URN. Exits4(not found) when the ref names nothing readable, or a node that isn't an object.update <ref>—--fieldsis shallow-merged into the existing record (patch wins on collision, unmentioned fields kept; atomic server-side), then re-validated against the schema. Contrastnode update --properties, which replaces the whole bag.--reasonis recorded in revision history.find(aliasls) — query one collection.--matchis an equality shorthand ({field: value}, ANDed intoeqper field, cast inferred from the schema);--whereis the full predicate (thesearch --wheregrammar), AND-combined with--match;--sortis{"<field>":"asc"|"desc"}. Prints the matching objects and atotal.delete <ref>(aliasrm) — soft by default (row retained, hidden from reads);--hardremoves the row. Non-recursive.--yesskips the prompt.
hadron object create -m acme.com:market --type competitor \
--fields '{"name":"Letta","stage":"series-a","fundingUsd":12000000}' --key letta
hadron object find -m acme.com:market --type competitor \
--where '{"path":["fundingUsd"],"as":"number","gt":10000000}' \
--sort '{"fundingUsd":"desc"}' --json
See Work with objects from the CLI for the walkthrough and the "which layer?" guidance, and Object store API for the GraphQL + MCP equivalents.
Memory health audit¶
hadron memory validate <memoryRef> runs the server's memory health audit
(validateMemory) and reports its findings.
| Finding | Meaning |
|---|---|
broken-ref |
An edge pointing at a missing or soft-deleted node. |
embed-failed |
A node whose embeddingFailedAt marker is set — an embedding attempt actually failed. Not the same as "has no vector": a node still pending, or one with nothing embeddable, lacks a vector without being a finding. |
sparse |
No description, content, or abstract. |
schema |
objectType / properties violating the memory's declared schema. |
stale-abstract |
The abstract's origin hash no longer matches the content hash. |
Read stale-abstract narrowly
It fires when the body isn't the version the abstract was written against — including when the change altered nothing the abstract says. It means "the body moved under this abstract", not "this abstract is wrong". Measured on a live corpus it carries essentially no signal about whether the abstract still describes the body.
Two counts, deliberately: totalFindings is the true count across every
check before truncation — gate on that, never on findings.length —
while matchedFindings counts what was listed after --limit and
--check.
ok is false whenever a check was skipped, even with zero findings:
health can't be claimed for a check that didn't run. skippedChecks says
which and why — stale-abstract is skipped on an encrypted memory, because
the validator doesn't decrypt.
--check <kind> filters client-side, and the server caps findings
before that filter runs, so --check requests the server maximum (1000)
unless you pin --limit. If the result is still truncated, the report says the
filtered view may be incomplete.
Assets¶
hadron asset manages binary attachments stored in object storage and
addressed by id or URN (hrn:asset:<root>:<memory>:assets:<id>). The URN
carries its memory, which is why asset url accepts a URN alone but needs
-m with a bare id.
hadron asset ls -m <memory> [--mine] [--mime <type>] [--include-deleted] [--limit N] [--offset N]
hadron asset get <asset-ref> [-o <path>|-] [--force]
hadron asset url <asset-ref> [-m <memory>]
hadron asset upload <file> -m <memory> [--mime <t>] [--name <n>] [--description <d>]
hadron asset rm <asset-ref> [--yes] | restore <asset-ref>
hadron asset link <asset-ref> --node <new-node-urn> [--name <n>] [--description <d>]
lsrequires-m. The CLI's listing is memory-scoped by design — it won't fan out across memories. (The GraphQL API does have a cross-memoryassetsquery returning every asset the caller can reach; the CLI simply doesn't expose it yet.) It pages to exhaustion by default;--limitfetches one explicit page.--minenarrows to your own uploads.getmints a short-TTL presigned URL and streams the bytes to the asset's own filename,-o <path>, or-o -for stdout. It refuses to clobber an existing file without--force(the default name comes from server metadata, not from you) and deletes a partial file on failure.uploadis three steps behind one command: the server reserves the asset and returns a presigned PUT, the bytes go straight to object storage, and a final call marks it usable. Size and MIME are declared up front, so the cap and the MIME allowlist reject a bad upload before any bytes move.
Downloads are gated on virus scanning¶
A PENDING asset is refused because the verdict hasn't settled — that's
"not yet", not a dead end: the server's sweep retries on a backoff, so try
again shortly. A BLOCKED asset is refused permanently, its bytes deleted
and the row kept as an audit tombstone. An upload whose bytes fail the scan
fails on the last step, after the transfer, leaving a BLOCKED record on
purpose — re-uploading the same file always fails the same way. All three
exit 1. asset ls shows the scan status, and for a BLOCKED row the engine
signature that matched (scanSignature in --json, null on every other row).
asset url prints an unauthenticated link
The public hotlink has no read gate — anyone holding it can fetch the file. It's absent (exit 5, with the reason) when the asset isn't CLEAN, its memory is encrypted, or the deployment has no public origin. Never construct that URL yourself.
Bulk search and replace¶
hadron replace text <old> <new> search-and-replaces a piece of text across
many nodes in one call — the CLI mirror of the hadron_replace_globally MCP
tool. replace is a top-level command group (not hadron node replace),
and <old>/<new> are positional arguments.
hadron replace text <old> <new> --field <field>… \
(--node <node-urn> | -m <memory>)… [--prefix <loc>] [--regex] [--ignore-case] \
[--max-nodes N] [--reason <text>] [--dry-run] [--yes] [--json]
Parameters¶
<old>(positional) — the text to find (literal by default; a regular expression with--regex).<new>(positional) — the replacement. With--regex, supports replacement patterns with backreferences ($1for a capture group,$&for the whole match).--field <field>(repeatable, required) — which text fields to search:content,name,alias,description,abstract,tags.- Selection — at least one of:
--node <node-urn>(repeatable) — specific nodes by URN or ID.-m/--memory <id-or-urn>(repeatable) — every node in one or more memories.--prefix <loc>— narrow a--memoryscope to a parent loc and its descendants (matched on:path boundaries, soauthmatchesauthandauth:tokensbut notauthoring).--regex— treat<old>as a regular expression.-i/--ignore-case— fold case.--max-nodes N— refuse to apply if more than N nodes would change (0= no limit). A guard against a whole-memory rewrite from a wrong--memoryURN or a forgotten--prefix.--reason <text>— recorded in the node's revision history.--dry-run— preview match counts without writing.--yes— skip the confirmation prompt; required in non-interactive use.
The blast-radius guard¶
A real run first previews the per-node match counts and asks for
confirmation before writing. Pass --yes to skip the prompt (an agent or
script must), or --dry-run to preview without writing. Even with
--yes, the affected-node count is printed to stderr before the write —
combined with --max-nodes, that's the guard against an accidental
whole-memory rewrite. Every change is saved to revision history, so
replacements are undoable.
--json output¶
{
"nodesScanned": 12,
"nodesChanged": 3,
"totalReplacements": 4,
"dryRun": false,
"results": [
{
"nodeId": "n1",
"loc": "findings:flaky-ci",
"memoryId": "mem1",
"replacements": 3,
"fields": ["content", "description"]
}
]
}
dryRun echoes whether this was a preview. A zero-match run returns
"results": [].
Examples¶
# Preview only — no write
hadron replace text old-url.com new-url.com \
-m acme.com:kb --field content --field description --dry-run
# Apply across a subtree, two fields, no prompt (agent-safe)
hadron replace text foo bar \
-m acme.com:kb --prefix services: \
--field content --field description --yes
# Regex with a backreference on one node
hadron replace text 'api/v(\d+)/users' 'api/v$1/users' \
--node hrn:node:acme.com:kb:api:users-endpoint --field content --regex --yes
# Cap the blast radius: refuse if more than 20 nodes would change
hadron replace text 'seperator' 'separator' \
-m acme.com:kb --field description --field tags \
--ignore-case --max-nodes 20 --yes
Import and export¶
Nodes and memories serialize to portable, self-describing files, and
node import has a second mode for ingesting external source.
node import — two modes¶
node import [<file>|-] does one of two things depending on its input:
- Restore (default) — reconstitute a node-export file produced by
node export(frontmatter-markdown, or--format json). Read-to import from stdin, so an export pipes straight into an import. The target memory and loc come from the file's own keys;-m/--memoryand--locoverride them to re-home a node into another memory. Outgoing edges are imported only with--with-edges(off by default).--create-onlyrefuses to update an existing node;--dry-runclassifies without mutating. The server recomputes content hashes, so a clean export→import round-trips losslessly. - Content — ingest raw external source (a web page, a captured HTML
DOM, a Markdown file, or a PDF) and let the server convert it to the
node's Markdown body. This mode is selected by
--url, by--as-content(force it for an otherwise-ambiguous.md/.json/stdin source), or automatically for a.pdf/.htmlfile. Target the node with-m/--memory+--loc, or with--node <node-urn>. Extra flags:--content-type(text/html·text/markdown·application/pdf; inferred from the extension, but required for a PDF over stdin),--name,--type(defaults towebpage, orinfofor a PDF), and--properties/--properties-file(a provenance JSON object merged into the node). A PDF's text layer is extracted to Markdown — the CLI base64-encodes the file for you; scanned/image-only PDFs error server-side.
An import onto an existing node overwrites it — and is gated
If the target loc already holds a node, node import overwrites it
(the prior version is kept in history). Like the destructive commands,
that prompts on a terminal and requires --yes non-interactively. An
import that creates a new node is never gated. With --with-edges,
an edge that can't be wired is reported under unwiredEdges (with a
reason) and the node is still written — but the command then exits 1
(see Partial writes); re-import is
idempotent.
Memory export, clone, and extract¶
memory export <id-or-urn> [--out <dir>]writes every node to a local directory (--outdefaults to.) as one frontmatter-markdown file per node (<out>/<loc>.md; colons in the loc become path segments) — the same layout the server's git sync produces, but on disk and with no remote.data-type nodes are skipped; nodes the read API can't return are listed underunavailablein the--jsonsummary (a client-side export is bounded by per-node read access). Existing files are overwritten; files for removed nodes are not deleted.memory clone <id-or-urn> --target-urn <org:slug>deep-copies a memory (nodes, edges, pending edges) into a new memory named by--target-urn(a fully-qualifiedorg:slugURN), rewriting references to the source URN inside node content and abstracts. The target org may differ from the source's, cloning into another org — you must be a non-reader member of that target org (cross-org clones drop edges to nodes in other memories). Version history, shares/subscriptions, assets, and git-sync config are not copied; encrypted and agent-system / app memories cannot be cloned.memory extract <parentRef> <targetUrn> [--move]lifts a parent node and its whole loc-subtree into a new memory named by<targetUrn>(a fully-qualifiedorg:slugURN), making the parent the new memory's root. Locs are rebased —findings:authbecomes the memory slug andfindings:auth:oauthbecomes<slug>:oauth.<parentRef>is the parent node's ID or fully-qualified URN; pass-m <org:memory>with a bare loc instead. Edges wholly inside the subtree carry over; boundary-crossing edges and unresolved pending edges are dropped. Without--movethe subtree is copied (source untouched);--moverelocates it, soft-deleting the source subtree —--moveneeds source write access and cannot target the source root (that would empty the source; clone then delete instead). The new memory preserves the source's class, so an extract never widens who can read the content (a member-restricted group stays group, a personal/private source stays owner-owned). The target org may differ from the source's, dropping the extract into another org. v1 limitation: node content is copied verbatim — because both the slug and node locs change, URN references among the moved nodes will break. Encrypted and system / app memories are rejected. See the GraphQLextractParentNodeToMemorymutation for the underlying operation. Not yet in a tagged CLI release (merged in hadron-cli#229; the latest release,v0.6.1, predates it) — until the next release ships, invoke the GraphQL mutation directly withhadron api.
# Round-trip one node through a file (or a pipe)
hadron node export hrn:node:acme.com:kb:findings:flaky-ci -o flaky.md
hadron node import flaky.md --with-edges
# Ingest external source as a node's content (server converts it)
hadron node import --url https://example.com/post -m acme.com:kb --loc clips:post
hadron node import paper.pdf -m acme.com:kb --loc papers:attention
hadron node import notes.md --as-content -m acme.com:kb --loc notes:today
# Export a whole memory to local markdown, one file per node
hadron memory export acme.com:kb --out ./kb-backup --json
# Extract a subtree into a new memory (copy, then a move into another org)
hadron memory extract hrn:node:acme.com:kb:findings:auth acme.com:auth-kb
hadron memory extract -m acme.com:kb findings:auth other-org:auth-kb --move
Team chat¶
hadron chat is a low-friction surface for a team chat — a shared memory
where several agents and humans coordinate. Each message is a message-type
node under a common parent, the payload in the node's data
(author/body/timestamp, plus identity/role and parsed @mentions),
ordered by a server-assigned seq. A reply is a reply edge from the new
message to the one it answers. It's the same protocol you can drive by hand with
node add + edge add; chat just does the plumbing. For the full
workflow (kickoff prompts, push delivery), see the
Set up an agent team chat how-to.
Naming a chat¶
Identify a chat by the node whose direct children are its messages:
--node <urn>— the message-parent node URN (hrn:node:<org>:<memory>:<loc>), one copyable value that packs the memory and the message location. This is the primary form.-m <memory> --messages-loc <prefix>— the equivalent two-field form (what the push channel'smemory+messagesLocencode). Mutually exclusive with--node.
These, and the agent's handle/identity/role, resolve from a flag, then the
matching HADRON_CHAT_* env var, then the project-local .hadron/config.json
(the same file the push channel reads):
{
"handle": "iris",
"chat": {
"node": "hrn:node:acme.com:team-chats:team-chat:api:messages",
"identity": "Claude Fable 5",
"role": "Backend Engineer"
}
}
chat.node is the single-URN form; chat.memory + chat.messagesLoc is the
two-field form the push channel requires (keep those if you run the channel).
With config in place, a turn is just chat read --since <seq> / chat post
--body "…".
chat read — pull new messages¶
chat read [--since <seq>] returns messages after <seq> (omit or --since 0
for all history) in one call — a compact transcript
([<seq>] <author> (<role>): <body>), or with --json:
{"messages":[{"seq":42,"loc":"…","author":"iris","identity":"Claude Fable 5","role":"Backend Engineer","timestamp":"…","body":"…"}],"nextSince":42}
Pass nextSince back as --since next turn — that's the whole cursor.
chat post — send a message¶
chat post builds the timestamped, colon-safe loc, assembles the data
payload (parsing @mentions from the body), writes the message node, and — with
--reply-to <loc> (the target's loc is in chat read's output) — adds the reply
edge, all in one call. It also best-effort materializes the message-parent
node so the chat is a real, copyable node in the portal.
The body comes from exactly one of --body <text> (inline), --body - (stdin),
or --body-file <path> (a file, for a composed multi-line message). A human sets
--identity human; an agent passes its model name.
# One-URN addressing, explicit identity
hadron chat post --node hrn:node:acme.com:team-chats:team-chat:api:messages \
--handle iris --role "Backend Engineer" --body "@rufus schema looks good"
# Config-backed: read the whole thread, then reply
hadron chat read --since 41 --json
hadron chat post --reply-to team-chat:api:messages:2026-06-21T213400Z-rufus \
--body "shipping it"
Team: personas and coding sessions¶
hadron team coordinates a team of humans and AI agents. A persona is an
Agent carrying persona metadata (personaName / personaRole /
personaPrompt) — a named team member ("Iris") re-driven across many sessions,
by the same human or a different one. A session binds one git worktree to
one persona and records the provenance of the work. The model behind both is in
Teams, personas, and sessions.
Shipped
The team group is shipped end to end: team persona, team session
(including the worklog milestones — session log (--pr | --issue |
--commit | --branch) — and the session list --pr <ref> provenance
query, after a one-time team init), and team chat (the group chat,
backed by the server's team-chat operations — hadron-server#939).
team persona — the roster¶
hadron team persona create --role <role> [--name <n>] [--team-agent <ref>] (uses --app)
hadron team persona list [--org <ref>] [--role <r>]
hadron team persona get <name-or-ref>
hadron team persona retire <name-or-ref> --yes
persona createis a thin wrapper over the server'screateTeamPersona(hadron-server#935): the server locates the Team Agent (or takes--team-agent), reads theroles:<role>definition from its system memory, allocates a free name from the role's register (a caller-supplied--namewins — a taken one is the typedPERSONA_NAME_TAKEN, exit 5), composes the persona prompt, and installs the persona into the team App. The CLI passes the values through and retries nothing.persona retiretakes a persona off the roster. It never frees the name — commit trailers and chat history reference it forever — and there is deliberately nopersona rm. Requires--yesnon-interactively.persona list/getnarrow the agent list client-side, merging your member-org scope with your own user-owned agents (which the unfiltered listing omits).getalso accepts a persona name, resolved case-insensitively; a name that is ambiguous across orgs asks for--orgor a URN.
team session — binding, provenance, presence¶
hadron team init -m <team-memory>
hadron team session start --as <persona> [-m <team-memory>] [--repo <r>] [--branch <b>] [--transcript <path>] [--host <h>] [--tool <t>] [--model <m>] [--force]
hadron team session whoami
hadron team session log (--pr | --issue | --commit | --branch) <ref> [--action <a>] [--detail <json>] [-m <team-memory>]
hadron team session end [--summary <text>] [--session <id>]
hadron team session list [--active] [--as <persona>] [--repo <r>] [--limit N] [--offset N]
hadron team session list (--pr | --issue | --commit | --branch) <ref> [-m <team-memory>]
session start --as <persona>records the provenance (repo / branch / host / tool / transcript path / model) server-side and writes a local binding under the worktree's git dir (resolved withgit rev-parse --git-dir, not a literal.git/— worktrees have a.gitfile).--hostdefaults to this machine's hostname; the rest are only recorded if you pass them.--astakes a persona name, an agent ID, or an agent URN, and--orgdisambiguates a name that exists in more than one org.session whoamireads that binding back. It is local-only, no network — which is what makes it the recovery path after a context compaction.- Two different conflicts both exit 5.
startrefuses if this worktree is already bound (end that session first, or--forceto replace the binding), and refuses if the persona has a still-active session elsewhere, naming who has been driving it and since when. A server-side stale-session reaper (hadron-server#930) auto-expires sessions idle beyond their window (24h by default), so a crashed session frees its persona eventually — but within that window it still reads as taken, which is whystartprints the last driver and start time rather than deciding staleness for you. --forcenever ends another driver's session. It starts yours alongside theirs. The one session--forcedoes end is the one this worktree's own binding named, best-effort, so a replaced binding never orphans an active session.session endends the bound session. The persona is freed unless another active session still holds it — a forced takeover leaves the taken-over session open, so checksession list --active.end --session <id>is the recovery path when the local binding is gone but the server session is still open.endrefuses with exit 2 when the binding was started against a different--serverthan the current one.session list(no ref) is the presence view — newest first, persona names joined in.--activeand--asnarrow client-side.
The commit trailer Persona: <name> is what carries the persona name into a PR;
it survives a squash-merge where a branch name doesn't.
The worklog — recording and querying milestones¶
The worklog is the append-only record of externally visible work, and the authoritative artifact↔session join. Three commands touch it:
team init -m <team-memory>declares the worklog collection's schema in the team App memory. Once per team, idempotent, and it preserves any other collections already declared there.session start -m <team-memory>records the worklog's home in the worktree binding, so later commands don't need-m.session log (--pr | --issue | --commit | --branch) <ref>appends a milestone, with--action(defaultworked-on) and an optional--detailJSON bag of display extras.
Refs normalize to one canonical string per artifact (owner/repo#371,
owner/repo@sha, owner/repo:branch), so a URL and a short form become the
same lookup key. The CLI accepts more spellings than the server does: a bare
number, SHA, or branch name is qualified from the session's --repo or the
git remote before it is sent, because inferring the repo is the client's
job. A bare --branch value is always read as a branch name, never as
owner/repo. The full grammar is the work-ref
contract.
--pr and --branch additionally denormalize onto Session.prNumber /
Session.branch — latest wins, display only. Every logged milestone,
including --issue and --commit, counts as session liveness for the
inactivity reaper. With no team memory configured, --pr and --branch
degrade to that denormalization alone (--json reports "recorded":
"session" instead of "worklog"), while --issue and --commit refuse —
there is nowhere to put them.
session list (--pr | --issue | --commit | --branch) <ref> is the
provenance query: it looks the canonical (ref, kind) up in the worklog and
returns the sessions that produced the artifact. Several rows are expected and
correct — a PR spanning three sessions yields three transcripts. A recorded
session you can't read lists as an id-only stub rather than vanishing, so the
count never silently understates the work.
The worklog commands are thin wrappers over the platform operations
session log and session list --pr delegate to the server's
recordTeamWork / teamWorkItems
(hadron-cli#414): team init is no longer a precondition for worklog
writes, and a worker session is always App-bound, so -m is a
per-call override rather than a prerequisite. A SESSION_NOT_IN_APP
on a worklog write means -m named a different App's memory than
the bound worker's — a mismatch to fix, not a session to restart.
team chat — the team App's group chat¶
hadron team chat post <body|-> [--reply-to <seq>] [--as-me] (uses --app or the binding)
hadron team chat read [--since <seq>] [--mentions-me | --mentions <ref>] (uses --app or the binding)
A thin wrapper over the server's team-chat operations
(hadron-server#939): ONE well-known chat per team App, living at chats:team
in the Team Agent's shared app memory and bootstrapped by the server on the
first post — no init step. The server also owns message ordering (an
atomically allocated per-chat seq), author derivation, and mention
extraction; the CLI composes no message node and never parses mentions.
- The App resolves from
--app(or the configured App context), falling back to the worktree binding's team memory. - Authorship: with a session binding,
postis authored by the bound persona through that session — the server verifies the session is yours, active, and of this App, and records it, so an agent message always traces to the driving human. Without a binding, or with--as-me, the post is authored by you. - Mentions are written as
@persona-name/@handle— a multiword name by its slug (@mary-jane) — and extracted server-side into the message.read --mentions-mefilters to the bound persona's mentions;--mentions <ref>takes a persona name, an agent ref, or a user handle. Filters match the stored tokens, never re-parsed bodies. --since <seq>is the read cursor: only messages with a strictly greater seq return, and the response'snextSinceis the value to pass next turn.--reply-to <seq>wires a reply edge server-side; a seq that names no message is the typedTEAM_CHAT_REPLY_NOT_FOUND(exit 4).
# Once per team: declare the worklog collection
hadron team init -m acme.com:eng-team-shared
# Mint a persona (the server allocates the name from the role's register)
hadron team persona create --role backend-engineer --app acme.com:eng-team
# Bind this worktree and work under it
hadron team session start --as Iris -m acme.com:eng-team-shared \
--repo acme/api --tool claude-code \
--transcript ~/.claude/projects/acme-api/session-8f2.jsonl
hadron team session whoami
# Log milestones — the bare number is qualified from the session's --repo
hadron team session log --pr 412 --action opened
hadron team session log --commit a1b2c3d --action pushed
# Coordinate in the team chat (posts as Iris via the bound session)
hadron team chat post "@rufus rate-limit middleware is up in #412, over to you"
hadron team chat read --since 42 --mentions-me
hadron team session end --summary "Rate-limit middleware + tests"
# Who is working right now, and what did Iris touch?
hadron team session list --active
hadron team session list --as Iris --limit 20 --json
# The provenance query: which sessions produced this PR? (a URL works too)
hadron team session list --pr acme/api#412 --json
Product specs¶
hadron spec maintains product-spec nodes in a memory that follows the
loc-as-citation convention: a spec's loc is its citation number — a
legal-code-style address where each colon level is a real parent/child node.
A memory uses one of two schemes:
- flat —
<module>:<feature>:<rule>[:<flow>](e.g.msg:010:02). - product-rooted —
<product>:<module>:<feature>:<rule>[:<flow>](e.g.api:cha:010:01), for a memory that spans several products.
The scheme is self-describing — the second segment is alpha (a module) when
product-rooted, numeric (a feature) when flat — and a memory should use only
one. A register node
holds the frozen module-code table and the number ledger. Every subcommand
takes -m/--memory; specs are addressed by a bare citation, not a full
node URN. Every spec subcommand resolves -m the same way — it accepts a
memory PK, an hrn:/urn: URN, a bare org:memory, or a memory
name.
| Command | Purpose |
|---|---|
spec describe [--declare flat\|product] |
Report the memory's scheme (flat/product), products, modules, and counts. --declare records the scheme in the memory's data. |
spec list [--prefix <loc>] |
List specs, optionally under a citation prefix. Paged to exhaustion unless --limit/--offset request one page. Aliased as spec ls. |
spec use <memory> |
Set the default memory hadron spec commands use when -m/--memory is omitted (stored as spec_memory in the CLI config); pass "" to clear it. Separate from the global active memory, so switching your working memory doesn't change your spec corpus. |
spec get <citation> (or --prefix <p>) |
Show one spec — abstract, edges, body, and a lint summary; --body-only prints just the raw body. With --prefix, dump every spec under a citation branch instead (paged; --limit/--offset fetch a single page). |
spec register [--check] |
Print the number ledger derived from live nodes. --check reports drift against the register node and exits 5 if any is found. |
spec find <query> [--match-exactly] |
Find specs by meaning (hybrid keyword + vector). --match-exactly switches to literal regex matching over name/loc/description/tags — use it for exact-fragment lookups such as a citation, since keyword search is now full-text ranked/stemmed rather than substring. |
spec grep <pattern> |
Exhaustive, line-oriented search over every spec's body + abstract across the corpus (one bulk read, not a per-spec loop), printing each match as citation:line: text. Literal by default; --regex (RE2), -i fold case, --field content\|abstract, --prefix <loc> to scope. The complement to spec find — use it to discover where a token actually lives in the prose (deliberately broad, no word boundary). |
spec replace <pattern> <replacement> |
Citation-aware bulk find/replace over spec bodies + abstracts. Word-boundary-aware by default (whole-token only, so h-read-node never hits h-read-nodes); --word-boundary=false for substring, --regex for a pattern with $1 backrefs. Gated like other bulk writes: --dry-run previews per-citation counts, --yes non-interactively, --max-specs N caps blast radius. Versioned, and re-lints the changed specs afterward. |
spec new [--product <ppp>] [--module <mmm>] --title <t> … |
Allocate the next citation and scaffold the rubric plus edges. --new-product / --new-module / --new-feature mint their respective tiers; --contract scaffolds the tier's general-provisions contract; --dry-run previews. Creating a root also scaffolds that tier's contract (<p>:gen / <m>:000 / <f>:00) unless --no-contract. |
spec new <citation> --new-path --title <t> |
Create that citation and every missing ancestor in one call — each with its tier template, and each created root with its general-provisions contract. Ancestors are titled from their own citation segment (srv:api → api); rename with node update --name. Not combinable with the tier-selecting flags. |
spec edit <citation> |
Open the spec's body and abstract together in $EDITOR (they are one logical unit). --content - / --content-file and --abstract - / --abstract-file replace a field non-interactively; an omitted field is preserved, and an unchanged field is not rewritten. --dry-run previews. |
spec extract <citation> --to-feature <fff> [--rule <rr>] |
Split a sub-rule out of a fat parent into its own citation under another feature — piping the moved chunk via --content - / --content-file, auto-wiring the cross-ref edge. --strip-source also trims the chunk from the source body. |
spec link <from> <to> [--label <l>] |
Cross-reference one spec from another by their bare citations — a convention-aware edge add that validates both endpoints are specs in the same corpus and synthesizes the label when omitted. --dry-run previews. |
spec lint [<citation>] [--prefix <loc>] [--product <ppp>] [--module <mmm>] [--all] [--strict] |
Validate one spec, a subtree (--prefix, that node plus its descendants), a product, a module, or the corpus against the rubric and stability rules. Exits 5 on errors; --strict promotes warnings to errors. |
spec check-tools [--prefix <loc>] |
Scan the corpus for hadron_* tool references and flag any that aren't a real registered tool — the drift that let stale h-* shorthand rot. Checked against a manifest baked into the binary (the union of hadron-server's MCP + runner tool registries), with a small ignore-list for known non-tools (e.g. the hadron_token cookie). Exits 5 on findings, so CI can gate on it; --json and --prefix <loc> supported. |
spec citations [--src <path>]… [--exclude <glob>]… [--loose] [--stale-abstracts] [--strict] |
Scan source code for Spec: citations and check each against the corpus — the pointers that live outside the graph, where spec lint can't see them. Reports a citation that doesn't resolve (a typo, or a spec deleted rather than superseded) and one that resolves to a superseded spec, naming its replacement. Matching is anchored on the Spec: prefix and takes every citation on the line; --loose drops the anchor and scans every line for citation-shaped tokens (finds unconventional pointers, at the cost of false positives in prose). --stale-abstracts adds a warning when a cited spec's abstract has drifted from its body — off by default, because that's a property of the spec, not the pointer. Exits 5 on errors so it can gate CI; --strict promotes warnings too. Alias: check-citations. |
spec supersede <old> --title <t> [--copy-body] --yes |
Retire a spec and mint its replacement with a superseded-by edge. |
spec import spec-kit\|code <path> |
Planned extractors — not yet implemented (exit 2). |
Numbering and stability:
- Features are numbered in tens (
010,020, …); rules and flows increment by one. Allocation is monotonic — strictly above the current maximum — so a retired number is never recycled. - General-provisions contracts. Each tier has a reserved contract its
siblings inherit: rule
00under a feature (msg:010:00), feature000under a module (msg:000), and — in a product-rooted corpus — modulegenunder a product (api:gen). Creating a root with--new-product/--new-module/--new-feature/--new-pathscaffolds that tier's contract alongside it unless--no-contractis passed;spec new … --contractadds one to a tier that lacks it. Introducing a contract into a tier that already has siblings is retroactive:lintthen requires each existing sibling to be back-wired with an inheritance edge before it passes. - Citations are never renumbered. To relocate or replace a spec, use
spec supersede;specrefuses to renumber an existing citation. - The register is advisory. Next-free numbers come from the live nodes;
specnever writes the register node. Usespec register --checkto catch a stale ledger. - A missing parent tier is rejected before anything is written — exit 4
(
NotFound), e.g.module "srv:qqq" does not exist — create it first with --new-module. Create the tier and re-run, or use--new-path. - A spec is never silently orphaned. Distinct from the case above:
spec new(andspec extractandspec supersede) fail loudly — exit 1 — when a required table-of-contents, cross-ref, or inheritance edge can't be wired, instead of reporting✓ createdon a node that ended up disconnected. Here the spec is written and the unwired edges are reported, so the run genuinely half-completed; fix the target and re-run, or wire the edge withhadron edge add.spec supersede --jsonreports each edge'sstatus(created/failed/skipped). This is the same partial-writes contract asnode import --with-edges. spec findis semantic by default and relies on the memory's vector index; without one it falls back to full-text keyword search and prints a note. Like the other destructive commands,spec supersederequires--yeswhen non-interactive.
hadron spec describe -m acme.com:platform-specs # which scheme?
hadron spec new -m acme.com:specs --module msg --feature 010 \
--title "W4 — 7-day check-in" --dry-run # preview the allocation
hadron spec lint --all -m acme.com:specs # check the corpus
hadron spec find "re-engage idle users" -m acme.com:specs
See Read and cite product specs to find and resolve a spec, and Maintain product specs for the authoring workflow.
Coding-workflow graph¶
hadron coding reads, extends and validates the coding-workflow graph in
a memory — the review:* checklist tree and the preflight router. Those
nodes are executable infrastructure, not prose: a check is triaged by reading
its edge label back to the review parent, and preflight routes symptom →
finding along its outgoing edges. A malformed label makes the check or route
silently stop firing — the node still exists and simply never matches
again. list shows the graph as its readers see it, create adds a node
with every edge that makes it discoverable, and lint detects the
silent-skip defects mechanically.
hadron coding review list -m <memory> [--root <loc>] [--broken]
hadron coding review create <check-name> -m <memory> --trigger <cond> --description <d> [--scope <s>] [--tag <t>]… [--link <ref>[=<label>]]… [--seq N] [--content <text|-> | --content-file <path>]
hadron coding review lint -m <memory> [--root <loc>] [--toolchain <t>|-] [--strict] [--suggest] [--fix [--yes]]
hadron coding preflight list -m <memory> [--root <loc>] [--broken]
hadron coding preflight create <loc> -m <memory> --route <action> --description <d> [--name <n>] [--symptom <s>] [--section <heading>] [--type <t>] [--tag <t>]… [--link <ref>[=<label>]]… [--seq N] [--content <text|-> | --content-file <path>] [--no-back-edge] [--no-body-line] [--dry-run]
hadron coding preflight route <node-ref> -m <memory> --route <action> [--description <d>] [--symptom <s>] [--section <heading>] [--no-back-edge] [--no-body-line] [--dry-run]
hadron coding preflight lint -m <memory> [--root <loc>] [--strict]
A node counts as a checklist item when it sits under the parent's loc prefix
(review: by default) and isn't tagged meta. Findings exit 5;
--strict promotes warnings to errors. --suggest proposes label fixes and
--fix applies them (with --yes to skip the prompt). --broken narrows a
listing to just the malformed rows.
Adding a route¶
preflight create writes the three things a routed node needs to be
reachable, because a node with only some of them looks healthy and isn't:
- the router's outgoing edge, labelled
to <action>— whatpreflight listandpreflight lintread; - the mirrored back-edge, so the route reads the same when someone lands on
the node from search instead of via the index (
--no-back-edgeto skip); - the routing line in the router's body —
- **"<symptom>"** → [[<loc>]] — <description>— which is what a human or an agent readingpreflighttop to bottom actually scans.
<loc> is the target's full loc: route targets live wherever they belong
(findings:…, conventions:…, ops:…), with no prefix rule. Where the
routing line goes is resolved before anything is written. A router with one
routing list takes it automatically; one with several headed sections needs
--section <heading>; a router that routes purely by edge label needs
--no-body-line. An ambiguous router is a usage error listing the headings —
never a half-written route. --dry-run rehearses all three writes, including
which section the line lands in, and issues none of them.
If the route edge or the body update fails, the node still exists: the
command prints the exact repair — an hadron edge create line, or the
routing line to paste — and exits 1, never 0.
Routing at a node that already exists¶
preflight create mints the node it routes to. When the node is already there
— usually a finding or task somebody else wrote, and often in another
memory — route does the same wiring without creating anything:
hadron coding preflight route hrn:node:acme.com:specs:tasks:create-platform-spec \
-m hrn:mem:acme.com:dev --route "add or change a spec in the law corpus" \
--section "Maintaining the memories themselves"
Two differences from create:
--descriptionis optional. The target already describes itself, so its own description becomes the routing line's text unless you override it.- A cross-memory target is referenced as
`<loc>` in `<memory>`, not as a bare[[loc]]. A wikilink resolves against the router's memory, so a bare one would point at nothing there and render as a dead link with no error anywhere. The memory name is read from the server, so every accepted spelling of the ref you pass yields the same canonical label.
Re-running is safe: an existing edge is not duplicated, an edge under a
different label is recognised as a different route rather than mistaken for
this one, and a body that already references the target is left alone.
--dry-run rehearses all three writes and issues none.
AI service configs¶
hadron ai-config ls lists the masked set of AI service configs
resolvable in an App's chat context — every distinct config name a chat
could select. It is the config picker an app or UI presents; each row is the
one resolveAIConfig would pick for that name.
| Column | Meaning |
|---|---|
NAME |
Config name, unique per owner (e.g. default, fast, frontier). |
OWNER |
Tier that owns the winning row: HADRON_SERVER, ORGANIZATION, APP, or AGENT. |
PROVIDER / MODEL |
Provider id (anthropic, openai, glm, bedrock) and model identifier. |
ENABLED |
Whether the config is enabled. |
KEY |
Key preview (ellipsis + last 4 characters), or — when no key is set. |
Resolution and masking:
- Resolution walks App → Agent → Org → HadronServer, deduped by name with
the innermost owner winning, enabled-only. Only the agent you name with
--agentcontributes — sibling agents installed in the same App are not consulted. - Output is masked. It never carries key material — only
hasApiKeyand the shortapiKeyPreview.--jsonemits the full masked record:id,name,ownerType,ownerId,provider,model,hasApiKey,apiKeyPreview,params,enabled,createdAt,updatedAt. --appdefaults to the configured App context (hadron app useor the global--appflag);--agentnarrows to one agent. Both accept an ID or a URN.- You must be a member of the App, and — when
--agentis given — the agent must be installed in it. App membership, not org-admin, is the bar because the result is masked. Platform admins may omit--app.
hadron ai-config ls --app acme.com:juno-app
hadron ai-config ls --app acme.com:juno-app --agent acme.com:juno --json
Create, update, and remove configs¶
ai-config ls is read-only and masked; to manage the underlying provider rows
it resolves over, use create / update / rm:
hadron ai-config create (--app|--agent|--org <id-or-urn>) --name <n> \
--provider <p> --model <m> [--api-key -] [--param k=v]… [--disabled]
hadron ai-config create --file <path>|- # whole config (key included) from JSON
hadron ai-config update <id> [--name <n>] [--provider <p>] [--model <m>] \
[--api-key -] [--param k=v]… [--enabled=false]
hadron ai-config rm <id> [--yes]
createneeds an owner — exactly one of--app,--agent, or--org(an ID or URN) — plus--name,--provider, and--model. The name is unique per owner (1-64chars,[a-z0-9_-]).- The API key is a secret. It is read from stdin via
--api-key -and is never echoed back — output is the same masked recordlsreturns (hasApiKey+apiKeyPreviewonly). Pipe it in:printf '%s' "$KEY" | hadron ai-config create … --api-key -. --file <path>(or--file -for stdin) reads the whole config — key included — from a JSON object, so nothing sensitive touches argv or shell history. The keys mirror the flags (all optional):app/agent/org,name,provider,model,apiKey,params(an object),enabled. The file seeds every field and an explicit flag overrides the matching one (e.g.--file cfg.json --model gpt-4o);--paramreplaces the file'sparamswholesale. Unknown keys are rejected (catches typos), and passing both--file -and--api-key -is an error (both read stdin).update <id>changes only the fields you pass.--api-key ""clears the key; omitting--api-keykeeps it.--param k=v(repeatable) replaces the whole params object.--enabled=falsedisables the config.rm <id>requires--yeswhen non-interactive.
printf '%s' "$ANTHROPIC_KEY" | hadron ai-config create --org acme.com \
--name frontier --provider anthropic --model claude-opus-4-8 --api-key -
hadron ai-config create --file frontier.json # same config, key included, from JSON
hadron ai-config update cfg_123 --model claude-sonnet-4-6
hadron ai-config rm cfg_123 --yes
These rows are what ai-config ls resolves over; for the end-to-end provider
setup see
Configure your LLM provider.
External MCP servers¶
hadron mcp-server manages registered external MCP servers — the conduit
that lets a headless run call tools that live outside Hadron.
hadron mcp-server ls [--org <ref>] | get <id> | tools <id>
hadron mcp-server create --org <ref> --slug <s> --name <n> --url <u> [--header 'Name: value']… [--allow <tool>]… [--disabled]
hadron mcp-server update <id> [--name <n>] [--url <u>] [--header …]… [--clear-headers] [--allow <tool>]… [--clear-allow] [--enabled|--disabled]
hadron mcp-server rm <id> --yes
Each registered row exposes its tools to runs as mcp__<slug>__<tool>,
which a node's data.tools declares. Registration alone grants nothing —
the run must still be allowed tool.mcp__<slug>__<tool> by the policy chain.
--allow narrows which of the server's tools are exposed at all;
--clear-allow / --clear-headers reset those lists rather than appending.
tools <id> lists what the remote server currently advertises.
See Call external MCP tools from a flow for the end-to-end walkthrough.
Connections and grants¶
hadron connection grant delegates scoped access on your own external
connection (email / calendar) to a specific App install — for example a
headless assistant that reads your inbox or answers free/busy.
hadron connection grant create --connection <ref> --app <ref> --scopes <s>[,…] [--expires-at <iso>]
hadron connection grant ls [--connection <ref>]
hadron connection grant revoke <grant-id> --yes
You must be the connection's owner to grant on it. Scopes are drawn from
mail.read, mail.send, calendar.freebusy, and calendar.read. Grants may
carry an expiry and are revocable at any time.
Secrets¶
hadron secret manages the owner-scoped secret store. Values are
write-only: create reads the material from stdin, a file, or an
interactive no-echo prompt — never from argv — and ls prints only the
inspectable half (name, kind, metadata, audit fields), never the value.
hadron secret create --name <n> --scope user|org|app|memory [--owner <ref>] --kind generic|webfetch-auth [--value-file -|@file]
hadron secret ls --scope <s> [--owner <ref>]
hadron secret rm <id> --yes
--scope user may omit --owner to mean the caller; the org, app, and
memory scopes require --owner. A webfetch-auth secret additionally takes
--type bearer|basic|header plus --url-prefix, and the server derives its
metadata.type. rm requires --yes non-interactively.
Agents¶
An Agent is a builder's creation — the thing an App installs and runs (see
Building an agent). hadron agent manages
their lifecycle.
agent ls— the member-scoped view: agents in orgs you belong to. Filter with--org,--type ASSISTANT|CHATBOT,--visibility ORGANIZATION|PERSONAL|PUBLIC, and page with--limit/--offset.agent ls --public— a separate surface: the cross-org marketplace slice of every live PUBLIC agent, readable without org membership, so you can grab a foreign agent's URN to subscribe to or install.--typestill filters;--org/--visibilitydon't apply (they're a usage error here).agent get <ref>— one agent by ID or URN (full detail in--json).agent create --org <id> --name <n>— with optional--type,--visibility,--description,--system-prompt,--system-memory <id>, and--surface <s>(repeatable).agent update <id> [<field flags>]changes only the fields you pass (--surfacereplaces the set).agent rm <id>requires--yeswhen non-interactive.
# Discover a public agent, then read it
hadron agent ls --public --type ASSISTANT --json
hadron agent get acme.com:support-bot --json
hadron agent create --org acme.com --name "Support Bot" --type CHATBOT \
--visibility ORGANIZATION --description "Front-line triage"
Headless runs¶
A headless run executes an entry (prompt) node under an App's
identity, off any interactive session. The same execution kernel is reached
three ways: a manual trigger (run), a recurring schedule, or an
inbound webhook. run is also the audit and control surface — what ran,
why, what it cost, and the kill switch — and ticket governs the consumable
grants a gated run spends.
Every trigger names an entry node by fully-qualified URN and runs under
an --app. By default the run acts as the App; --as-self runs on
behalf of you — required to reach your personal memories, and usable only
by an authenticated user (an App-key caller gets UNAUTHENTICATED).
run — trigger, audit, cancel¶
hadron run trigger --app <ref> --entry <node-urn> [--arg k=v]… [--as-self] \
[--ai-config <name>] [--wait [--wait-timeout <dur>]]
hadron run list [--app <ref> | --org <ref>] [--status <s>] # alias: ls
hadron run get <id>
hadron run cancel <id> [--yes]
triggerstarts a MANUAL run now and prints the run id.--arg k=v(repeatable) sets template args (each value parsed as JSON or string).--ai-config <name>selects a named AI config for the run.--waitpolls to a terminal status and then exits non-zero if the run ended non-COMPLETED(FAILED/TIMED_OUT/CANCELLED), so a script can branch on the outcome.listis the audit surface — status, trigger kind, and run id. Scope with--appor--org(mutually exclusive); filter with--status(one ofPENDING,RUNNING,COMPLETED,FAILED,CANCELLED,TIMED_OUT).getshows one run in full: status, budgets, policy, and the failure payload when present.cancelis the kill switch — transitions a live run toCANCELLED. Prompts on a TTY; requires--yesnon-interactively.
hadron run trigger --app acme.com:ops --entry hrn:node:acme.com:ops:tasks:nightly-digest
hadron run trigger --app acme.com:ops --entry hrn:node:acme.com:ops:tasks:brief \
--arg topic=security --as-self --wait --json
hadron run ls --app acme.com:ops --status FAILED
hadron run cancel run_123 --yes
schedule — recurring triggers¶
hadron schedule create --app <ref> --name <n> --cron '<expr>' --entry <node-urn> \
[--tz <IANA>] [--arg k=v]… [--as-self] [--ai-config <name>] [--policy <json>] [--disabled]
hadron schedule list [--app <ref>] # alias: ls
hadron schedule update <id> [--cron …] [--entry …] [--enabled=false] …
hadron schedule rm <id> [--yes]
A schedule fires the entry node on a 5-field cron expression evaluated in
--tz (default UTC). The runs it spawns show up in hadron run ls.
--policy is a trigger-layer allow-list, e.g. '{"allow":["comm.outbound"]}'.
update changes only the fields you pass (--enabled=false disables;
--policy "" clears the allow-list). rm requires --yes
non-interactively.
hadron schedule create --app acme.com:ops --name nightly-digest \
--cron '0 6 * * *' --tz America/New_York \
--entry hrn:node:acme.com:ops:tasks:nightly-digest --as-self
webhook — inbound triggers¶
hadron webhook create --app <ref> --name <n> --entry <node-urn> \
[--args-schema <json>] [--as-self] [--ai-config <name>] [--policy <json>] [--disabled]
hadron webhook list [--app <ref>] # alias: ls
hadron webhook rotate <id> [--yes]
hadron webhook rm <id> [--yes]
A POST to the webhook's URL fires the entry node. The URL path and platform
token are printed ONCE, at create and rotate — store them then; the
secret is never queryable again (list never shows it). --name is
lowercase alphanumeric/dash (1–64 chars) and is part of the URL. rotate
invalidates the old URL/token immediately and prints the replacement once;
both rotate and rm require --yes non-interactively.
hadron webhook create --app acme.com:ops --name deploy-notify \
--entry hrn:node:acme.com:ops:tasks:on-deploy
ticket — action-ticket ledger¶
hadron ticket mint --org <ref> --action comm.outbound --count <n> \
[--app <id>] [--note <text>] [--expires <iso8601>]
hadron ticket list --org <ref> # alias: ls
A ticket is a consumable grant an org ADMIN mints into the org
ledger; a headless run consumes one per gated action (v1: comm.outbound).
The ledger records which run consumed each ticket, and expiries. --app
scopes tickets to one App (omit for org-wide); --note records why they
exist.
hadron ticket mint --org acme.com --action comm.outbound --count 100 \
--note 'nightly digest send budget'
hadron ticket ls --org acme.com --json
grant — per-user action grants¶
Where a ticket is a consumable budget for a metered action, a grant
is a standing permission: it hands one or more actions to a specific org
member so their runs pass the policy chain for those actions.
hadron grant create --org <ref> --user <ref> --action <a>[,…] [--expires <iso8601>]
hadron grant ls [--org <ref>] [--user <ref>] # alias: list
hadron grant revoke <id> --yes
create(org ADMIN) grants actions to a member.--actiontakes one or more action names — an exact action (memory.clone), a wildcard prefix (memory.*), or*for all.--expiressets an optional expiry. The grantee must be a live member; a grant dies with the membership.lsdefaults to your own grants; narrow with--organd/or--user(seeing another user's grants needs org-audit visibility).revokesoft-deletes a grant by id (requires--yes).
hadron grant create --org acme.com --user u_123 --action 'memory.*' --expires 2026-12-31T00:00:00Z
hadron grant ls --org acme.com --user u_123 --json
hadron grant revoke grant_456 --yes
Organizations¶
hadron org manages organizations and their members. There is no org-list
query — address an org by its id (the org behind a memory URN).
hadron org create --name <n> --urn <urn>
hadron org get <id>
hadron org update <id> [--name <n>] [--urn <u>] [--visible=false]
hadron org rm <id> [--yes]
hadron org member ls <org-id>
hadron org member add <org-id> --user <id> --role <role>
hadron org member set-role <org-id> --user <id> --role <role>
hadron org member rm <org-id> --user <id> [--yes]
createrequires--nameand--urn(e.g.acme.com).updatechanges only the fields you pass;--visible=falsehides the org from listings.rmrequires--yeswhen non-interactive.- Members.
member lslists the org's members (user id, name, email, role).member addandmember set-roletake--user <id>and a--roleofOWNER,ADMIN,CONTRIBUTOR, orREADER(case-insensitive).member rmremoves a user and requires--yesnon-interactively.
hadron org create --name "Acme Inc" --urn acme.com
hadron org member add org_123 --user usr_456 --role CONTRIBUTOR
hadron org member ls org_123 --json
Memory access control¶
Two mechanisms grant access to a memory, both under hadron memory and
addressing the memory by id or org:memory URN:
memory member— team membership on a (group-class) memory. Roles areowner,writer, orreader; a member is part of the memory's team and may be anowner.memory share— a per-user grant on apersonal-class memory only (privateis owner-only;knowledge/groupuse org membership or team members instead). Roles arewriterorreader— a share never confers ownership. Only the memory's principal may manage its shares.
Both add / create upsert: naming a user who already has a row updates
their role instead of failing.
hadron memory member ls <memory>
hadron memory member add <memory> --user <id> --role <owner|writer|reader>
hadron memory member set-role <memory> --user <id> --role <owner|writer|reader>
hadron memory member rm <memory> --user <id> [--yes]
hadron memory share ls <memory>
hadron memory share create <memory> --grantee <user-ref> --role <writer|reader>
hadron memory share set-role <memory> --grantee <user-ref> --role <writer|reader>
hadron memory share rm <memory> [--grantee <user-ref>] [--yes]
--grantee is a server-resolved user reference — an id, email, handle, or
hrn:user:<handle> URN. On share rm (aliases revoke / delete), omit
--grantee to remove your own share: the leave / stop-sharing-with-me
path. Revocation takes effect on the next read. See
Share a memory with someone.
- Member vs share. Use
memberfor the owning team (symmetric access, may include owners); useshareto grant one user read or write access without adding them to the team. The role vocabularies differ accordingly —owner|writer|readerfor members,writer|readerfor shares. member ls/share lslist the rows (user id, name, email, role).member rmandshare rmare destructive and require--yeswhen non-interactive.
hadron memory member add acme.com:kb --user usr_456 --role writer
hadron memory member ls acme.com:kb --json
hadron memory share create acme.com:kb --grantee usr_789 --role reader
Access checks¶
hadron access check answers "what access does this user have to this
resource?" for a single (user, resource) pair — the server-computed
effective capabilities plus the grants that confer them. It is the
audit/troubleshooting counterpart to memory member / memory
share, which list who can reach a memory. The
answer is authoritative: it comes from the server's effectiveAccess
resolver, which reuses the enforced authorization paths rather than
re-deriving authz in the CLI.
<user>— id, email, or handle (a leading@sigil is accepted, e.g.@alice). Resolved to a User ID viasearchUsers, which is itself scoped to the caller. An exact match on id, email, handle, or githubUsername wins; an ambiguous match is a usage error (no arbitrary pick). User URNs are not a first-class input form yet — ahrn:user:wrapper is unwrapped client-side.<resource>— a fully-qualified URN:hrn:mem:…,hrn:node:…,hrn:app:…, orhrn:agent:…; or a bareAiServiceConfigid (the one URN-less kind). An under-qualified shorthand likeacme.com:kb(nohrn:prefix) is rejected locally with guidance, before any network round-trip.- Auditing requires rights on the resource. Reading the answer requires
audit permission: a platform admin, an ADMIN/OWNER of the resource's owning
org, or — for a strict-owner (
personal/private) memory — the memory's principal. Otherwise the server returnsFORBIDDEN(exit 1). This is distinct from the subject having no access. - An empty
grants[]is the first-class "no access" answer (exit 0). It means the subject genuinely has no grant on the resource — not that the caller was forbidden from auditing.
Output¶
The human view prints the resolved user and resource, a role label, a
capability table (READ / WRITE / MANAGE / DELETE as ✓/✗), and the grants:
$ hadron access check holger@baragaun.com hrn:mem:micromentor.org:academy
User: holger@baragaun.com (019d28f1…)
Resource: micromentor.org:academy (memory)
Role: ADMIN
READ WRITE MANAGE DELETE
✓ ✓ ✓ ✓
Grants:
SOURCE ROLE VIA
PLATFORM_ROLE ADMIN —
ORG_ROLE OWNER micromentor.org
--json emits the same data as a stable object:
{
"user": { "id": "...", "name": "...", "email": "...", "handle": "..." },
"resource": { "urn": "hrn:mem:acme.com:kb", "kind": "memory" },
"canRead": true, "canWrite": true, "canManage": false, "canDelete": false,
"role": "writer",
"grants": [ { "source": "MEMORY_SHARE", "role": "writer", "via": null } ]
}
Grant sources¶
Each entry in grants[] is { source, role, via }. source is the gate that
confers the access; via names the conferring entity where one applies (for
example, a node reports its memory's access as NODE_INHERITED with via set
to the memory URN):
source |
Confers access because… |
|---|---|
PLATFORM_ROLE |
the user holds a platform-level role (e.g. platform admin). |
ORG_ROLE |
the user is a member of the resource's owning org; via is the org. |
MEMORY_PRINCIPAL |
the user is the strict owner of a personal/private memory. |
MEMORY_MEMBER |
the user is on the memory's team (memory member). |
MEMORY_SHARE |
the user has a per-user share on the memory (memory share). |
NODE_INHERITED |
a node inherits its memory's access; via is the memory URN. |
APP_MEMBER |
the user is a member of the App. |
AGENT_SUBSCRIPTION |
the user has an active subscription to the Agent. |
AICONFIG_OWNER |
the user owns the tier that owns the AI service config. |
PUBLIC_VISIBILITY |
the resource is public and readable by anyone. |
Exit codes¶
| Code | Condition |
|---|---|
| 0 | success — including the "no access" answer (empty grants[]). |
| 1 | the caller lacks audit rights on the resource (server FORBIDDEN). |
| 2 | usage error — under-qualified resource, or an ambiguous user match. |
| 4 | the resource could not be resolved (not found / not visible). |
hadron access check alice@acme.com hrn:mem:acme.com:kb
hadron access check @alice hrn:node:acme.com:kb:start-here
hadron access check usr_123 hrn:app:acme.com:support --json
See Debug PERMISSION_DENIED errors —
access check is the fastest first diagnostic when a call is denied.
Authentication¶
Authenticating and operating from the CLI never requires the web portal — a
self-hosted hadron-server is sufficient. There are portal-free ways to get a
hdr_user_* credential:
| Mechanism | Use |
|---|---|
hadron auth login |
Full server-side OAuth against the configured server (discovery → Dynamic Client Registration → 127.0.0.1 loopback → PKCE → token exchange). No portal in the path; the token is stored in the OS keychain. |
printf '%s\n' "$TOKEN" \| hadron auth login --with-token |
Store an existing hdr_user_* token — e.g. one bootstrapped on the server host with pnpm admin:mint-token, or minted by auth token create elsewhere. |
HADRON_TOKEN env var |
Overrides stored tokens — CI and agent contexts. |
hadron auth status answers "am I signed in?" via exit code (0 yes / 3 no).
Transport and credential-store safety¶
- HTTPS is enforced for the bearer token. The CLI refuses to send your
credential over cleartext
http://to a non-loopback host. To point it at a trusted self-hosted server over plain HTTP, setHADRON_ALLOW_HTTP=1— the escape hatch is explicit and scoped tohttp(it never admits a non-httpscheme). A loopback host (127.0.0.1,localhost) is always allowed without the flag. - Failures are loud, not silent. A corrupt
auth.jsonfails with a clear error instead of masquerading as "not signed in", so you fix the store rather than re-authenticating blindly. When no OS keychain is available,loginwarns that the token is stored unencrypted on disk rather than storing it silently.
Personal access tokens¶
Once signed in, mint and manage long-lived hdr_user_* personal access tokens
directly from the CLI — no portal:
hadron auth token create [--label <text>] # mint a PAT — the raw key is shown ONCE
hadron auth token ls # list your PATs (masked to a preview)
hadron auth token validate # check a token: exit 0 valid / 3 rejected
hadron auth token revoke <id> [--yes] # revoke one
auth token requires a user login — an app/agent key can't manage user
tokens. The raw key is printed only once on create, so capture it then; ls
shows just a masked preview. validate checks whether a token authenticates
without storing it — it reads the candidate from stdin (printf '%s'
"$TOKEN" | hadron auth token validate), so it never lands in your shell
history, and exits
0 (printing the owning user) if the server accepts it, 3 if not.
Server info¶
hadron server-info reports the hadron-server this invocation targets —
{url, version, baseUrl, authenticated}.
It works signed out (the field is public), so it doubles as a reachability probe: a failure reaching the server points at the server or the network, not a missing login. (A credential store that can't be read still fails loudly rather than silently querying anonymously.)
Two fields are easy to misread:
versionis the server's API-surface contract version — bumped when the query/tool surface changes in a caller-visible way. It is not a release version, so use it to decide whether a surface exists, not to compare against a release tag.urlis where the query was sent;baseUrlis what the server calls itself. A mismatch between them is worth noticing — it usually means a proxy or a stale--server/hadron config set servervalue.
The escape hatch: hadron api¶
Anything the curated commands don't cover is reachable as raw GraphQL (see the GraphQL API reference):
hadron api 'query { me { id email } }'
hadron api 'query($id: ID!) { memory(id: $id) { urn name } }' -F id=mem_123
cat op.graphql | hadron api -
-F key=value binds variables — values that parse as JSON are sent as
JSON, otherwise as strings. The verbatim GraphQL response envelope prints
to stdout; GraphQL errors are reflected in the exit code.
Recipes¶
# Inspect a memory and its nodes
hadron memory get acme.com:kb --json
hadron node ls -m acme.com:kb --json
# Read one node's content and edges (keep the hrn:node: prefix)
hadron node get hrn:node:acme.com:kb:findings:flaky-ci --json
# Create a node from stdin (-m takes a memory ref — single- or double-colon)
cat finding.md | hadron node add -m acme.com:kb --loc findings:flaky-ci \
--name "Flaky CI" --content -
# Connect two nodes
hadron edge add --from hrn:node:acme.com:kb:findings:flaky-ci \
--to hrn:node:acme.com:kb:start-here --name routes-to
# Bulk search-and-replace (preview, then apply with --yes)
hadron replace text old-domain.com new-domain.com \
-m acme.com:kb --field content --field description --dry-run
hadron replace text old-domain.com new-domain.com \
-m acme.com:kb --field content --field description --yes
# Delete non-interactively (agents must pass --yes)
hadron node rm hrn:node:acme.com:kb:findings:flaky-ci --yes