ik command-line reference
ik is the command-line tool of the indiek package. Install it with uv tool install 'indiek[cli]' (or pip install 'indiek[cli]').
Quickstart
ik keygen --write # writes IK_AUTHOR_PRIVATE_KEY to .env
ik config --set-archive-path ~/archives/my-archive # writes IK_ARCHIVE_PATH to .env
ik create_node --content "Gödel's first incompleteness theorem" --title "Gödel I" # e1
ik create_node --content "Any consistent formal system … is incomplete." # e2
ik create_edge --source-node e2 --target-node e1 --content "states" # e3
ik create_node_tag --name theorem --description "A proven statement" # m1
ik set_tag --tag m1 --id e1 # m2, m3
ik read --mini
ik seal # prints: Seal 1 created: sha256:…
ik verify # re-checks everything
Invocation
ik [--dot-env PATH] [--logging-level LEVEL] [--verify LEVEL] [--trust-seal HASH] <command> [flags]
Global flags apply to every command and must precede it.
| Flag | Default | Meaning |
|---|---|---|
--dot-env |
.env |
Dotenv file to load. Real environment variables win over it. |
--logging-level |
WARNING |
DEBUG, INFO, WARNING, ERROR or CRITICAL, case-insensitive. |
--verify |
incremental |
none, incremental or full; see Verification levels. |
--trust-seal |
— | A seal hash you obtained separately; the signatures it covers are not re-checked. |
Logs go to stderr; stdout carries only command output (IDs, tables, reports), so it is safe to pipe.
| Command | Purpose | Prints on success |
|---|---|---|
keygen |
Generate an author key | IK_AUTHOR_PRIVATE_KEY=<seed> (nothing with --write) |
config |
Show or set IK_ARCHIVE_PATH |
IK_ARCHIVE_PATH=<value> |
create_node |
Add a node (managed, uri, local_file) |
Node created: <uuid> |
create_edge |
Add a directed edge between two nodes | Edge created: <uuid> |
supersede_node |
Replace a node’s content | Node superseded: <uuid> |
supersede_edge |
Replace an edge’s content | Edge superseded: <uuid> |
create_node_tag |
Define a tag for nodes | nothing |
create_edge_tag |
Define a tag for edges | Meta node created: <uuid> |
set_tag |
Assign a tag to a node or edge | nothing |
read |
Print both event logs as tables | two tables |
export |
Write the base network as GraphML | nothing |
ingest |
Bulk-create nodes and edges | nothing |
transcribe |
Attest a log with a transcription event |
Transcription appended: <hash> |
seal |
Append a signed seal of the whole archive | Seal <n> created: <hash> |
verify |
Fully re-verify, optionally against a known seal | counts and the latest seal |
fsck |
Fully verify and print a health report | a report |
manifest |
Write MANIFEST.json, SPEC.md, test_vectors.json |
MANIFEST written to: <path> |
Environment
| Variable | Meaning |
|---|---|
IK_ARCHIVE_PATH |
Root folder of the archive (~ is expanded). |
IK_AUTHOR_PRIVATE_KEY |
The author’s Ed25519 seed, 64 hex characters. |
IK_CACHE_DIR |
Optional. Directory of the local verification cache. |
Both main variables can live in the dotenv file (ik keygen --write,
ik config --set-archive-path).
keygenandconfigneed neither variable.read,export,manifest,verifyandfsckneed onlyIK_ARCHIVE_PATH; without a key the archive is opened read-only (andmanifestlists no author).- Every other command also needs the key; without it:
ik: error: An author is required to write to an archive.
Errors and exit codes
| Exit status | Meaning |
|---|---|
0 |
Success. |
1 |
The command failed: integrity, unknown item, authorship, archive rule, invalid key. One line ik: error: <message> on stderr, no traceback (--logging-level DEBUG adds it). |
2 |
Usage error: missing or invalid flag, or IK_ARCHIVE_PATH not set. |
A malformed key exits 1 with ik: error: IK_AUTHOR_PRIVATE_KEY is invalid: <reason>;
the key itself is never printed. The messages are those of the SDK errors
(see Errors).
ik keygen
Generates a fresh Ed25519 author key. Needs neither a key nor an archive.
| Flag | Meaning |
|---|---|
--write |
Store the seed in the --dot-env file (mode 600) instead of printing it. |
--force |
With --write, replace a key already in that file. The old key is lost. |
Without --write: prints IK_AUTHOR_PRIVATE_KEY=<64 hex> on stdout and
Public key: <hex> on stderr. With --write: nothing on stdout; stderr
names the file and the public key. If the file already holds a key and
--force is absent, exits 1 and leaves it untouched. The seed is your
signing identity: back it up privately, never share it.
ik --dot-env .env keygen --write
ik config
Shows, and optionally sets, IK_ARCHIVE_PATH in the --dot-env file. Needs
neither a key nor an existing archive.
| Flag | Meaning |
|---|---|
--set-archive-path |
Archive path to write into the dotenv file (stored single-quoted). |
Prints IK_ARCHIVE_PATH=<value> or IK_ARCHIVE_PATH is not set; when
setting, then IK_ARCHIVE_PATH=<new path>.
ik config --set-archive-path ~/archives/my-archive
ik create_node
Appends a create_node event.
| Flag | Required | Meaning |
|---|---|---|
--content |
for managed |
Inline text, or the path of a supported file. |
--title |
no | Title stored in the event. |
--resource-type |
no | managed (default), uri or local_file. |
--uri |
for uri |
External URL, never fetched. |
--path |
for local_file |
Local file, never copied. |
--media-type |
no | One of the archival media types; a hint for uri nodes. |
- managed: if
--contentnames an existing file, the file is read — text for.txt/.md, bytes for.png,.jpg,.tif/.tiff— with the media type detected from its name; any other file fails withUnsupported archival media type for <path>and nothing is written. Otherwise--contentis stored astext/plain.--media-typeis ignored for managed nodes. Missing--contentexits 2. - uri: records
--uriand the optional--media-type. Missing--uriexits 2. - local_file: records the absolute path, size, SHA-256 and time of
--path; media type detected as above; title defaults to the file stem. Missing--pathexits 2; a missing file fails withLocal file does not exist or is not a regular file: <path>. - If
--titleis omitted and--contentnames an existing file, the title is the file’s stem.
ik create_node --content "Gödel's incompleteness theorem"
ik create_node --content ./notes/limits.md --title "Limits"
ik create_node --resource-type uri --uri https://arxiv.org/abs/1234.5678 \
--title "The paper" --media-type "application/pdf; profile=pdfa"
ik create_node --resource-type local_file --path ~/scans/page.tiff
ik create_edge
Appends a directed edge that carries text.
| Flag | Required | Meaning |
|---|---|---|
--source-node |
yes | Source node (UUID or alias). |
--target-node |
yes | Target node (UUID or alias). |
--content |
yes | Edge text, stored verbatim as text/plain (a file path is not read). |
Both endpoints must be existing base nodes, checked before anything is
written (Unknown ID or alias '<ref>', SOURCE NODE <uuid> NOT FOUND IN ARCHIVE,
TARGET NODE <uuid> NOT FOUND IN ARCHIVE). Use ik read to see the new
e{n} alias.
ik create_edge --source-node e2 --target-node e1 --content "states"
ik supersede_node
Appends replacement content for a node; history is kept.
| Flag | Required | Meaning |
|---|---|---|
--node-id |
yes | Node (UUID or alias). |
--content |
yes | Replacement content, stored as text/markdown. |
--title |
no | New title; the current one is kept when omitted. |
Only the node’s creator may supersede it
(Only the author of Node '<ref>' may supersede it). Other errors:
Node '<ref>' does not exist in this archive,
ID '<ref>' does not reference a base-network node.
ik supersede_node --node-id e1 --content "Gödel's *first* incompleteness theorem"
ik supersede_edge
| Flag | Required | Meaning |
|---|---|---|
--edge-id |
yes | Edge (UUID or alias). |
--content |
yes | Replacement text (text/plain). |
Endpoints and title are kept. Same authorship rule and errors as
supersede_node, for edges.
ik supersede_edge --edge-id e3 --content "strictly generalises"
ik create_node_tag and ik create_edge_tag
Define a tag for nodes, or for edges, in the meta log.
| Flag | Required | Meaning |
|---|---|---|
--name |
yes | Tag name (not unique). |
--description |
yes | Tag description. |
The tag takes the next m{n} alias. create_edge_tag prints
Meta node created: <uuid>; create_node_tag prints nothing.
ik create_node_tag --name theorem --description "A proven statement"
ik create_edge_tag --name proof --description "Proof relation"
ik set_tag
Assigns a tag to a base node or edge.
| Flag | Required | Meaning |
|---|---|---|
--tag |
yes | Tag (UUID or m{n} alias). |
--id |
yes | Node or edge to tag (UUID or e{n} alias). |
The first assignment to an item writes two meta events (a representation,
then the assignment) and so consumes two m{n} aliases. Errors are those of
set_tag in the SDK. Prints nothing.
ik set_tag --tag m1 --id e1
ik read
Prints two tables on stdout: Events Log (base) and Meta Events Log, each row ending with the item’s alias.
| Flag | Meaning |
|---|---|
--mini |
Only timestamp, event type, author (and tag name in the meta table). |
Without --mini, the base table adds node/edge IDs, source and target,
resource type and title; the meta table adds IDs, tag description,
source/target and underlying ID. Missing values show as -.
ik export
Writes the base network as directed GraphML.
| Flag | Required | Meaning |
|---|---|---|
--output-path |
yes | Destination file (overwritten). |
--tags |
no | Comma-separated tags (UUIDs or aliases); keeps items carrying any of them. |
Each element carries author, timestamp, blob_hash and, for
text/plain content, a label. See Known limitations.
ik export --output-path theorems.graphml --tags m1,m4
ik ingest
Bulk-creates nodes from a folder, then edges from a file.
| Flag | Meaning |
|---|---|
--input-folder |
Every entry (sorted, not recursive) becomes a node, read like create_node --content, titled by its stem. |
--edges-def-file |
One edge per line: source,target,content (split on the first two commas). |
printf 'e9,e1,cites\n' > edges.csv
ik ingest --input-folder ./more --edges-def-file edges.csv
ik transcribe
Appends a signed transcription attesting the verified log.
| Flag | Default | Meaning |
|---|---|---|
--network |
base |
base or meta; the event goes into that same log. |
--note |
none | Free text stored in the event. |
Prints Transcription appended: <hash> (the new head). An empty log fails:
Nothing to transcribe: the <network> log is empty.
ik transcribe --note "re-verified before release"
ik seal
Fully verifies, then appends a signed seal of both logs and of the blobs they
reference. Prints Seal <n> created: <hash>: the string to publish. Refused
(exit 1) for an empty archive, an archive unchanged since the latest seal,
and while unreferenced blobs exist (the error lists the files to delete).
ik verify
Re-reads and fully verifies the archive: both logs, every referenced blob, every seal.
| Flag | Meaning |
|---|---|
--expect HASH |
Fail unless the seal log contains this seal. |
Prints Archive verified: <n> base events, <m> meta events, <k> seals, then
Latest seal: <hash> if any, and with --expect
Expected seal found: seal <i> of <k>. A failure exits 1 with its message,
e.g. Expected seal not found in seal log.
ik verify --expect "sha256:e1c0aa4631b1…"
ik fsck
Fully verifies (whatever --verify says) and prints a report:
Archive: /home/me/archives/demo
Base log: 10 events, head sha256:…
Meta log: 6 events, head sha256:…
Seals: 1, latest seal 1: sha256:…
Transcriptions: base 0, meta 0
Blobs: 9 files, 8 referenced, 1 unreferenced
unreferenced (safe to delete): /home/me/archives/demo/blobs/sha256/4f/cc/4fcc…
Representations without a tag: 0
Verification: full, OK
ik manifest
Writes MANIFEST.json at the archive root — format version, algorithms
used, the current author’s public key (if any), layout, rules and JSON
Schemas of every log line — and copies SPEC.md and test_vectors.json
next to it. Prints MANIFEST written to: <path>. Re-run it after upgrading
indiek to refresh the copies.
Example: a knowledge graph from scratch
With a folder holding notes/statement.txt, notes/proof-sketch.md,
figures/diagram.png, scans/page.tiff and more/cites.txt:
ik keygen --write
ik config --set-archive-path ~/archives/demo
ik create_node --content "Gödel's incompleteness theorem" # e1
ik create_node --content ./notes/statement.txt # e2, title "statement"
ik create_node --content ./notes/proof-sketch.md --title "Proof" # e3
ik create_node --content ./figures/diagram.png # e4
ik create_node --resource-type uri --uri https://arxiv.org/abs/1234.5678 \
--title "Survey" --media-type "application/pdf; profile=pdfa" # e5
ik create_node --resource-type local_file --path "$(realpath scans/page.tiff)" # e6
ik create_edge --source-node e2 --target-node e1 --content "states" # e7
ik create_edge --source-node e3 --target-node e2 --content "proves" # e8
ik supersede_node --node-id e1 --content "Gödel's *first* incompleteness theorem"
ik supersede_edge --edge-id e8 --content "sketches a proof of"
ik create_node_tag --name theorem --description "A proven statement" # m1
ik create_edge_tag --name proof --description "Proof relation" # m2
ik set_tag --tag m1 --id e1 # m3 representation of e1, m4 assignment
ik set_tag --tag m2 --id e8 # m5 representation of e8, m6 assignment
printf 'e9,e1,cites\n' > edges.csv
ik ingest --input-folder ./more --edges-def-file edges.csv # e9, e10
ik read --mini
ik export --output-path theorems.graphml --tags m1,m2
ik manifest
ik seal # publish the printed hash
ik verify
Integrity, seals and trust
Integrity verification
Every command and every Archive.open verifies the archive before doing
anything: the base log, then the meta log, then the seal log, then the replay
into the index. Every event names its algorithms (hash_alg: "sha256",
sig_alg: "ed25519"); hashes are written sha256:<hex>, signatures
ed25519:<hex>. A failure raises IntegrityError (CLI: exit 1) with one of:
| Message | Meaning |
|---|---|
Invalid log line: … |
A line is not UTF-8, not one JSON object, repeats a key, or holds a non-integer number or an integer outside ±(2^53−1). |
Invalid string: lone surrogate … |
A string is not valid Unicode. |
Unsupported format version … |
An event or seal is not format 3.0. |
Unsupported algorithm … |
hash_alg / sig_alg missing or unknown. |
Algorithm mismatch … |
A signature or prev_hash does not carry the declared algorithm prefix. |
Chain broken! |
An event does not link to the previous one (edited, reordered, inserted or removed events). |
Data tampered or bad author! |
A signature does not verify against its author’s key. |
File corrupted or missing! |
A referenced blob is absent or its bytes changed. |
Meta anchor does not match base history |
A meta event points to a base history that no longer exists (the base was rewritten or truncated). |
Meta anchor goes back in base history |
A meta event’s anchor is older than the previous one’s. |
Transcription does not match chain |
A transcription attests a head the log does not have. |
Seal chain broken! |
A seal is out of sequence, badly linked, badly signed or malformed. |
Seal does not match logs! |
A seal names a log tip that is not in the logs (rewritten or truncated, or meta/ missing). |
Seal goes back in log history |
A seal covers fewer events than the previous one. |
Blob set does not match seal! |
A seal’s blob_root does not match the blobs its events reference. |
Expected seal not found in seal log |
--expect / --trust-seal / trusted_seal names an absent seal. |
Supersede event references unknown item <id> · Supersede event type does not match item <id> · Supersede by a non-author of item <id> · Supersede edge events cannot change endpoints · Tag assignment references an unknown representation |
The logs verify but their content is inconsistent. |
Meta anchors. Every meta event records how many base events its author
had verified (base_seq) and the hash of the last one (base_head). If the
base is later rewritten or truncated, the base alone still verifies but every
annotation anchored past the change fails. Base events never mention the
meta log, so an archive shipped without meta/ (and without seals/) is
valid on its own.
Some conditions only log a WARNING: unreferenced blobs, a seal of an empty
archive, a seal repeating the previous one, and (with full verification)
SPEC.md / test_vectors.json copies that differ from this package’s.
Seals and the blob store
A seal records the base tip and meta tip (event count and hash of the
last event) and blob_root, an RFC 6962 Merkle root of every blob those
events reference; it is signed and chained to the previous seal. The seal
hash commits to everything in the archive at that moment. Publish it (a web
page, an email, a printed book); anyone with a copy can check it with
ik verify --expect <hash>. No seal can prove it is the latest one:
freshness needs that outside reference.
Every seal, not only the latest, is checked on every open: chain and
signature, tips present in the logs, counts never decreasing, blob_root
recomputed. Writing after a seal is normal and keeps it valid; seal again for
a new checkpoint. seals/ commits to both logs: ship it only together with
events/ and meta/.
Unreferenced blobs are files under blobs/sha256/ that no event uses —
the leftover of an interrupted write, or a file added by hand. They are not
part of the graph and no seal covers them. Opening warns
(<n> unreferenced blob(s) in <dir> … : <names>), sealing is refused until
they are deleted, and they are always safe to delete (ik fsck and
Archive.unreferenced_blobs() list them). A referenced blob that is
missing or altered always fails with File corrupted or missing!.
Verification levels and the trust model
Most of the cost of opening an archive is checking the Ed25519 signature of every event and re-hashing every blob. Measured on 10,000 events:
Level (--verify, verify=) |
What it does | 10k events |
|---|---|---|
full |
Checks everything; ignores the cache, then refreshes it. | ≈ 11 s |
incremental (default) |
Recomputes every hash link, anchor, transcription and seal, but skips the signature checks and blob re-hashing that a trusted source already vouches for. | ≈ 1 s once cached |
none |
Parses only (debugging, recovering a damaged archive); the archive is read-only. | ≈ 0.3 s |
verify, fsck, seal, ingest, export and manifest always run a
full check first. At incremental, a damaged archive reports exactly the
error full would.
Checks may be skipped on exactly two grounds:
- Your machine checked it before. After a successful check, the reader records “the signatures of events 1…N were valid and event N hashed to H” per log, plus the size and modification time of each verified blob. Next time it recomputes the chain anyway (cheap); if event N still hashes to H, events 1…N are byte-for-byte those already checked. Anything else, and the log is checked in full.
- A seal hash you obtained separately (
--trust-seal HASH,trusted_seal=), e.g. from the author’s website. That seal must be in the seal log and the recomputed chains must reach its tips; only the signatures it covers are skipped.
Never because of a seal found in the archive. Any key may seal. If
Mallory alters Alice’s event 500, re-links the events after it and seals the
result with his own key, a full check still fails (Data tampered or bad
author!): he cannot sign as Alice. Trusting seals found in the archive would
skip exactly that check, so IndieK always checks them and never trusts them.
With Alice’s real seal hash, --trust-seal rejects Mallory’s copy outright.
The cache lives outside the archive, in your user cache directory
(~/Library/Caches/indiek/ on macOS, $XDG_CACHE_HOME/indiek/ or
~/.cache/indiek/ on Linux, %LOCALAPPDATA%\indiek\Cache\ on Windows;
override with IK_CACHE_DIR), so nothing shipped with an archive can make
you skip a check. One small state.json per archive holds hashes, counts and
file sizes/times, never content; deleting it is always safe.
Caveats: the separate route for a trusted seal must be authenticated;
anyone who can write to your cache directory already controls your account;
and since the cache cannot see bit rot that keeps a file’s size and time,
every blob is re-hashed whenever it is read, and ik verify / ik fsck
re-hash them all.
Known limitations
- One writer per history. Each log is a single chain. Two copies extended
independently cannot be merged (
Chain broken!), and there is no lock against two local processes writing at the same moment. - Clock changes. Events are filed by their UTC date; writing after the system clock moved back across midnight breaks the chain.
local_filepaths are absolute paths of the writer’s machine.- CLI
--media-typeis ignored for managed nodes (detected from the file name, ortext/plain). PDF/A content can only be added from the SDK (media_type=MediaType.APPLICATION_PDF_A). exportignores supersedes (it shows original content), may keep an edge whose endpoint was filtered out, labels onlytext/plaincontent, and--tagsfails with aKeyErroron an archive whose base log holds atranscriptionevent.ingeststores a subfolder’s path as a text node, stops at the first hidden or unsupported file (e.g..DS_Store), keeps the trailing newline in edge text, and crashes on an edge line with fewer than two commas.manifestlists only the current author inauthors.- Reading content goes through
archive.index.store.blobs. - Schema checks on read are partial: see
SPEC.md§20. - Speed. A
fullcheck of 10,000 events takes about 11 s.