You run the tools; the human holds the keys.
You are an agent, and a human asked you to set up or use kint. This page says what you can do for them, what must go back to them, and what never goes through the chat.
If you only read one line#
kint is not on PyPI yet (install it from the git tag), run it with PYTHONPATH unset, call memory_verify before you act on anything you recalled (push your own edits first), and never ask the human for the wallet signature, the vault passphrase or the recovery code in chat.
The install gotchas#
kint is not on PyPI yet, so an install by bare name (uv tool install kint, pip install kint) does not get this project. Install from the tag instead (Python 3.10 or newer):
uv tool install git+https://github.com/s0nderlabs/kint@v0.3.0That puts two binaries on PATH: kint, the CLI, and kint-server, the MCP server a harness launches.
If the user's shell sets PYTHONPATH, Python puts its entries ahead of the tool's own packages, so the binaries must run with it unset: env -u PYTHONPATH kint .... kint doctor notes it on its last row, python: (PYTHONPATH is set: run with env -u PYTHONPATH). kint setup keeps it out of every registration it writes: Claude Code gets -e PYTHONPATH=x -- /usr/bin/env -u PYTHONPATH <kint-server>, Codex gets PYTHONPATH = "" in its env table, and Hermes and OpenClaw launch the server through /usr/bin/env PYTHONPATH=.
Four more things trip agents up:
- One store, one server.
kint-serverserves Sibyl's eight tools itself. If Sibyl's own server is also registered, remove it (claude mcp remove -s user sibyl-memoryfor Claude Code);kint setup claudeprints that reminder. - User scope only.
kint setup clauderegisters at user scope. A project-scoped registration is done by hand; Harnesses has the line. - The tenant. kint uses
KINT_TENANT, elsetenant_idoraccount_idfrom Sibyl'scredentials.json, else Sibyl's default. Pin it per harness withkint setup <target> --env KINT_TENANT=<tenant>. The CLI's--tenantand--dbflags go before the command:kint --tenant kint-demo status. kint setupedits the user's harness configs. Say so before you run it. For Claude Code it removes any existing user-scopekintentry and adds it again. For Codex it copies~/.codex/config.tomltoconfig.toml.bak-<unix time>first, and does nothing if a[mcp_servers.kint]block is already there.
What you can do for the user#
- Install kint and run
kint setup <target>, where target isall,claude,codex,hermesoropenclaw, with--env KEY=VALUEas often as needed. - Create this machine's session key:
kint session-key create. It prints the new address. The key only signs transactions to the contract; it can never decrypt. - Print the payloads the human will sign:
kint canonical-payload --owner 0x...(the frozen EIP-712 message) andkint authorize payload --owner 0x...(the session-key authorization; it prints the next step on stderr). Neither payload is a secret. What the wallet returns when it signs the first one is. - Diagnose:
kint doctor(exits 1 when any row says FAIL),kint status,memory_status, and the log at~/.kint/kint.log. - Recall with Sibyl's tools, check with
memory_verify, read old versions withmemory_history, and push or pull withmemory_pushandmemory_pull. - Write the recovery-code file again on a connected machine:
kint recovery-code. Tell the human where it is. Do not open it.
On a machine joining a vault that already exists, kint join --owner 0x... with exactly one secret (--signature - piped from the wallet, --base-account which prompts for the passphrase, or --recovery-code-stdin) creates the session key, connects, restores and registers the harnesses, and writes nothing to the chain. The human supplies the secret in their own terminal; writes: off in its report means the session key still needs funding and authorizing. A failure prints join stopped at <step>: <message> (One command: kint join).
What must go back to the human#
These steps need a wallet, a secret or money. Hand the human the command and wait.
| step | why it is theirs | what they run |
|---|---|---|
| Sign once per machine (EOA owner) | the signature is the key to their memory | the canonical-payload and cast wallet sign pipe into kint connect --signature - |
| Type the vault passphrase (Base Account owner) | it derives the key | kint connect --owner 0x... --smart-account (it prompts), or with --passphrase-stdin |
| Fund the session key | it pays for every push | send a little ETH on Base to the address kint session-key create printed |
| Authorize the session key | only the owner can | kint authorize page (Base Account, in the browser), or payload, sign, then submit |
| Keep the recovery code | it opens the vault when the wallet and passphrase are gone | copy ~/.kint/RECOVERY-<space16>.txt somewhere that is not this machine |
| Rotate the data key | it needs every key that should keep working | kint rekey, in their terminal; there is no rekey tool |
Some details that change what you tell them:
- Fund before
kint authorize submit: that transaction is sent by the session key itself.kint doctorwarns until the balance is above 0.00002 ETH and the key is authorized. kint authorize payloadstamps a deadline 300 seconds out, so the human signs and submits within five minutes or prints a fresh payload.- An authorization runs for
--days(default 30). The contract'scanWriteis true only while the expiry is in the future, so after that pushes fail until they authorize again.memory_statusshowssession_key.expiry. kint authorize directandkint authorize burn-nonceread the owner's private key fromKINT_OWNER_KEY. That is the human's to set in their own shell, never yours to ask for.- On the machine that created the vault, push refuses while the recovery-code file is missing or empty. After a
kint rekey, the old code no longer opens the vault and a new file is written.
Never ask for a secret in chat#
memory_connect does accept two secrets. Called with passphrase (a Base Account owner) or recovery_code, it connects directly, and owner is required the first time (without one it answers OWNER_REQUIRED). It never accepts the derive signature: that travels on stdin through kint connect --signature - (or in a file that --signature-file reads and then deletes), never on argv.
Do not use the two it accepts. Anything a human types into an agent chat lands in the transcript, and a tool call's arguments are part of it. Call memory_connect with no arguments instead. If the machine is not connected, it returns connected: false and human_steps: three lines, each a label and then a command for the human's own terminal. The commands are:
kint canonical-payload --owner 0x... > kint-canonical.json && cast wallet sign --data --from-file kint-canonical.json --ledger | kint connect --owner 0x... --signature -
kint connect --owner 0x... --smart-account --passphrase-stdin
kint connect --owner 0x... --recovery-code-stdinThe first is for an EOA wallet, the second for a Base Account or smart wallet owner, the third for the day both are lost. The server looks for the enrolment and the cached key on every call, so once the human has connected, your next memory_status sees it without a restart. Then call memory_pull: the server only pulls on its own at startup.
Warning. The derive signature is not a login. The frozen EIP-712 message says so in its
purposefield: "kint-memory-v1: signing this reveals your memory encryption key. Only sign it in a kint terminal or page you opened yourself." A phished derive signature is a permanent key. Never ask the human to paste it, never read~/.kint/session.key, the vault cache or the recovery file into the conversation, and never read the macOS Keychain.
The tool-call recipe#
memory_statusfirst. Readconnected,data_key_cached,in_step,unanchored,held,chain_head(anerrorkey there means the chain could not be read) andsession_key(exists,authorized,balance_eth,expiry). Ifconnectedordata_key_cachedis false, go tomemory_connectwith no arguments. Ifheldis notnull, kint-server is refusing to push: tell the human itsmessage. The cached key expires afterKINT_KEY_TTL, 24 hours by default, and every successful pull and push renews it.memory_pullwhenin_stepis false. The chain head is not what this machine last saw anchored: another machine pushed, or this one has not pulled yet. With unanchored changes here, the pull answersFORK, which is the human's call.memory_verifyrefuses while the head is past this machine's mirror, so bring the mirror up to the head first.- Recall with Sibyl's tools.
memory_search,memory_recallandmemory_get_statework as they always did. Their results are what the store holds now, not what the chain vouches for. memory_verifybefore acting on a recalled row. Pass the query, not a row key: it runs the same searchmemory_searchruns (Sibyl'smulti_record_search, precision gates included) and checks the toplimithits (default 5, wherememory_searchdefaults to 10). Act only whendecisionisproceed, and only on the row thereasonnames.- Write with Sibyl's tools. kint anchors after the write has returned. While the server runs, a watcher pushes once the store has been quiet for
KINT_QUIET_SECONDS(default 300), or sooner when the change set is large. To verify a row you just changed,memory_pushfirst: an unpushed edit to an anchored row is refused asdrifted, and that refusal holds every kint-server push until the human runskint push. memory_pushwhen a task is done, so the next machine sees it now rather than after the quiet period.memory_push(snapshot=true)anchors one epoch that holds the whole state.memory_historyfor "what did this say before". Passtier(entity,stateorreference),key,categoryfor entities, and optionallyblock.
memory_status()
memory_search(query="release rule")
memory_verify(query="release rule") # act only on decision "proceed"
memory_remember(category="rules", name="release-gate", body={...})
memory_push()memory_history reads the decrypted epochs cached on this machine. On a machine whose restore stopped at a snapshot epoch, older versions appear only after memory_pull(full=true). Report each version's block as an upper bound: the row existed no later than block N. Never turn it into a wall-clock time.
Reading a memory_verify result#
ok true when the check ran; a refusal still has ok: true
decision "proceed" or "refuse"
reason one sentence: the row and the block it rests on, or why it refused
verdict Sibyl's search verdict: code, returned, recovery, tokens, explain
hits what the search returned: tier, key, category, snippet, rank, ts
checks one per hit: tier, key, category, status, local_leaf, anchored_leaf, anchored_seq,
anchored_block, anchored_tx, head_seq, head_block, rows_root, proof, proof_ok, reason
refusal_entity "kint_refusal/<name>" when a refusal was written back, else null
chain_head the head read for this check: seq, digest, block; null when not readok: false means the check itself failed (error holds the exception name), which is not a proceed either. Each check has one of four statuses:
| status | meaning |
|---|---|
verified | the stored text hashes to the leaf anchored on Base and the merkle proof against the anchored rows_root passes |
drifted | the stored text no longer matches the anchored leaf, or the proof failed; your own edit to an already anchored row reads this way until it is pushed |
unanchored | the row is new since the last anchored epoch; nothing on the chain vouches for it yet |
missing | the row the search returned is no longer in the store |
The decision is refuse when Sibyl's verdict is not ok (abstained_on, negation_abstain, gated, empty_store or no_match) or there are no hits, when this machine has no mirror (it never connected, pulled or pushed; the reason says "never pulled or pushed"), when its mirror is incomplete (a skipped epoch, or a root that does not equal the anchored one), when the chain head moved past the mirror (the reason starts "chain moved to seq N at block B, pull first"), when any hit is drifted or missing (even if another hit verified), or when every hit is unanchored. Otherwise it is proceed, and the reason names the top verified row, for example entity rules/release-gate verified against rows_root 390336ff71dbcb85 anchored at epoch 2, block 51081880. That epoch and block are the head this machine last saw; the epoch that last changed the row is in the check's anchored_seq and anchored_block. If higher-ranked hits were unanchored, the reason adds a NOTE naming them: act on the verified row and push before relying on the others.
What to do on refuse:
- Do not act on the row. Tell the human, and quote the reason: a drift names the epoch and block that anchored the last good value, and the head epoch and block.
- Match the fix to the reason. Unanchored rows:
memory_push, then verify again. "Never pulled or pushed", "pull again" or "chain moved ... pull first":memory_pull, then verify again. A verdict that is notok: there was nothing to act on, so rephrase the query (onabstained_on, drop the word inverdict.tokens[0]) or ask. - A drifted row is a human decision. If the change was theirs and intended,
kint pushin their terminal anchors it;memory_pushanswersHELDwhile the refused value is in the store. If it was not,kint pull --discard-localmoves the store aside and restores from the chain, taking every unanchored change into the backup with it; a plain pull reports "up to date" and leaves the drift in place. - The first
driftedormissinghit is written back as a Sibyl entity (categorykint_refusal, statusrefused, named<tier>-<category or none>-<key>-<unix time>) so the next fresh session sees it. While the row still holds the refused value, every kint-server push answersHELD; the entity itself is anchored by the first push that goes through. The other refusals write nothing. The tool always writes it;kint verify --no-writeis the CLI's way to skip it.
Anti-patterns to avoid#
- Do NOT treat
memory_searchormemory_recalloutput as checked. Onlymemory_verifydecides. - Do NOT push past a refusal or a
HELD.memory_pushis held on a refused value, andkint pushfrom a shell is not held: it anchors whatever the store holds, including a drifted row, so it is the human's decision. - Do NOT run
kint compact --over-skippedorkint rekey --over-skippedyourself. They anchor this machine's store as the whole state on top of an epoch that will never open: the owner's recovery step, after revoking the session key that wrote it. There is deliberately no tool argument for it. - Do NOT run
memory_pull(discard_local=true)orkint pull --discard-localwithout the human. It movesmemory.db(and its-waland-shmfiles) aside to<name>.kint-backup-<YYYYmmdd-HHMMSS>, so every unanchored change leaves the live store and survives only in that backup. - Do NOT suggest
kint pull --rebase. There is no such flag:kint pulltakes--owner,--discard-local,--force-scanand--full. - Do NOT rely on
force_scan.memory_pullandkint pullaccept it, and v0.3.0's pull never reads it. - Do NOT look for a rekey tool. There is deliberately none: the secrets that rotate the key are typed in a terminal.
- Do NOT tell the user reading costs gas. Restoring and reading never send a transaction.
- Do NOT start a second push while one runs. The tool answers
BUSY, and a second process waits up to 30 seconds for the head lock, then fails. - Do NOT promise a restore without a key. The wallet signature, a passphrase that has a wrap, or the recovery code opens the vault; kint has no reset.
- Do NOT set
KINT_KEY_TTL=sessionand expect a terminalkint connectto reach the server. Withsessionthe key lives only in the memory of the process that connected.
Common errors and fixes#
Tool errors come back as {"ok": false, "error": ...}, usually with a message, a hint, or both. The error is a code (NOT_CONNECTED) or, for anything else, the exception's class name (ConnectError).
| error | where | fix |
|---|---|---|
NOT_CONNECTED, hint call memory_connect | memory_push, memory_pull | memory_connect with no arguments; give the human the human_steps |
OWNER_REQUIRED | memory_connect with a secret | this machine has no owner yet; the human connects in their terminal with --owner |
KEY_EXPIRED: the data key on this machine has expired or is missing | push | the human runs kint connect again; nothing was dropped |
CHAIN_MOVED: ... another machine pushed., hint call memory_pull first | push | memory_pull; with unanchored changes here it answers FORK (below), which is the human's call |
BUSY, hint a push is already running | memory_push | wait, then memory_status |
HELD: refusing to anchor: ..., hint nothing was anchored and nothing was dropped; memory_verify still refuses the row | memory_push (the watcher and the exit push log the same) | stop and tell the human the message: they check the rows it names, then run kint push (anchor the value) or kint pull --discard-local (restore the anchored value, dropping every unanchored change) in their terminal |
PUSH_FAILED: session key 0x... is not authorized for owner 0x... | push | the human funds and authorizes it (kint authorize); also the message after the expiry passes |
PUSH_FAILED: refusing to push: this machine created the vault but its recovery code file is gone | push | kint recovery-code, the human copies it off the machine, then push |
PUSH_FAILED: refusing to push: this machine could not apply epoch(s) [...] on its last pull | push | memory_pull again; a wrong key needs kint connect. An epoch that will never open (a leaked session key wrote it) is the owner's to clear: they revoke the key, then run kint compact --over-skipped in their terminal |
PUSH_FAILED: row ... compresses to more than 90 KB on its own and cannot fit one epoch | push | shrink or split that row in Sibyl; nothing else is blocked |
PUSH_FAILED: the full state (N rows) compresses to ... snapshots are single-epoch | memory_push(snapshot=true) | push without snapshot; a restore just walks more epochs |
KeyError_: no session key on this machine | push | kint session-key create, then fund and authorize |
TimeoutError: another kint process holds the head lock for this space | push, pull | another push or pull is running on this machine; wait and retry |
FORK: the chain moved to seq N and this store has ... rows that were never anchored: a fork | pull | the human decides: discard_local moves the store aside and restores; the unanchored rows are then only in the backup |
FORK: the local store already holds N rows for tenant T and this machine has no record of what the chain last saw | pull | wrong tenant, or a store from before kint; the human moves it aside or uses --discard-local |
NOT_FRESH: cold start needs a second, independent RPC and KINT_RPC_URL_2 resolves to the same endpoint | pull | the human sets KINT_RPC_URL_2 to another provider, or accepts one RPC with KINT_ALLOW_SINGLE_RPC=1 |
NOT_FRESH: could not get a second opinion on the head from ... | pull | the second RPC did not answer on a cold start; the human fixes KINT_RPC_URL_2, or accepts one RPC with KINT_ALLOW_SINGLE_RPC=1 |
NOT_FRESH: two RPCs disagree on the head | pull | wait and retry; do not override it |
NOT_FRESH: ... stale or lying RPC, refusing | pull | the RPC served a head older than this machine already saw; use another endpoint |
PULL_FAILED: no data key on this machine | pull | memory_connect with no arguments |
ConnectError: this key (tag ...) does not open the vault anchored under 0x... | connect | wrong wallet, passphrase or tenant; the recovery code still opens it |
ConnectError: this recovery code does not belong to the vault anchored under this owner | connect | check the owner and the tenant |
KintCryptoError: recovery code checksum failed (typo?) | connect | the code was mistyped; the human types it again |
ChainUnreadable: cannot read the chain head | connect | an RPC problem, never a security verdict; retry or set KINT_RPC_URL |
kint: derive signature recovers to 0x..., expected 0x...; refusing to derive a key from it | CLI connect | the wallet that signed is not --owner, or the payload was for another owner |
kint: give the derive signature with --signature - (stdin) or --signature-file PATH; never on argv | CLI connect | pipe the signature in; never put it on the command line |
kint: --passphrase-stdin and --signature - cannot share stdin | CLI connect, rekey | use --passphrase-prompt or --signature-file |
kint: no owner known on this machine: pass --owner 0x... or run kint connect | CLI | pass --owner, or connect first |
When you script the CLI, the exit codes carry the same information. Any kint: error exits 2 unless noted. push, compact and rekey exit 3 for a missing data key or session key, 4 when the chain moved, 5 for any other push failure. pull exits 4 for a fork, 6 for a freshness refusal, and 5 for any other failed pull (a missing data key included). kint verify exits 0 on proceed and 1 on refuse, except with --json, which exits 0 either way: read decision. A head-lock timeout (TimeoutError) or a chain failure during a push (ChainError) is not caught by the CLI and ends in a Python traceback.
Where state lives#
Everything kint keeps is under KINT_HOME (default ~/.kint), mode 0700. Every state file is written 0600; kint.log is created with the default file mode. <space16> is the first 16 hex characters of the space id, keccak256("kint-space-v1" || tenant_id); memory_status returns the full id as space.
~/.kint/
├── session.key this machine's session key, a V3 scrypt keystore
├── session.key.old the previous key, after kint session-key rotate
├── local.secret the local passphrase, only when no Keychain is used
├── enrol-<space16>.json the enrolment: owner, tenant, space, key tags
├── vault-<space16>.aes the data key, encrypted at rest, with an expiry
├── wraps-<space16>.json the wrapped data keys, one per unlock path
├── RECOVERY-<space16>.txt the recovery code: copy it off the machine
├── mirror-<space16>.json what this machine last saw anchored: rows, leaves, anchored root
├── watermark-<space16>.json the highest head this machine has seen; never decreases
├── head-<space16>.lock one pusher or puller per space per machine
├── kint.log kint's log (stderr gets the same lines)
└── epochs/
└── <space16>/
├── 00000001.bin the ciphertext, as sent to Base
├── 00000001.json the decrypted plaintext, which memory_history reads
└── 00000001.meta.json seq, digest, prev, block, tx, bucket, rows_root, writerThe Sibyl store itself is at SIBYL_MEMORY_DB (default ~/.sibyl-memory/memory.db), and it is plaintext, as it always was. On macOS the local passphrase that protects session.key and the vault cache is kept in the Keychain item dev.kint-session-key unless KINT_SESSION_PASSPHRASE is set (a server) or KINT_NO_KEYCHAIN=1. The full list of variables is in Configuration.
Read Built on Sibyl Memory next.
Source: src/kint/server.py, src/kint/cli.py, src/kint/connect.py, src/kint/push.py, src/kint/pull.py, src/kint/keys.py, src/kint/verify.py, src/kint/paths.py.