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.0

That 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-server serves Sibyl's eight tools itself. If Sibyl's own server is also registered, remove it (claude mcp remove -s user sibyl-memory for Claude Code); kint setup claude prints that reminder.
  • User scope only. kint setup claude registers at user scope. A project-scoped registration is done by hand; Harnesses has the line.
  • The tenant. kint uses KINT_TENANT, else tenant_id or account_id from Sibyl's credentials.json, else Sibyl's default. Pin it per harness with kint setup <target> --env KINT_TENANT=<tenant>. The CLI's --tenant and --db flags go before the command: kint --tenant kint-demo status.
  • kint setup edits the user's harness configs. Say so before you run it. For Claude Code it removes any existing user-scope kint entry and adds it again. For Codex it copies ~/.codex/config.toml to config.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 is all, claude, codex, hermes or openclaw, with --env KEY=VALUE as 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) and kint 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 with memory_history, and push or pull with memory_push and memory_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.

stepwhy it is theirswhat they run
Sign once per machine (EOA owner)the signature is the key to their memorythe canonical-payload and cast wallet sign pipe into kint connect --signature -
Type the vault passphrase (Base Account owner)it derives the keykint connect --owner 0x... --smart-account (it prompts), or with --passphrase-stdin
Fund the session keyit pays for every pushsend a little ETH on Base to the address kint session-key create printed
Authorize the session keyonly the owner cankint authorize page (Base Account, in the browser), or payload, sign, then submit
Keep the recovery codeit opens the vault when the wallet and passphrase are gonecopy ~/.kint/RECOVERY-<space16>.txt somewhere that is not this machine
Rotate the data keyit needs every key that should keep workingkint 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 doctor warns until the balance is above 0.00002 ETH and the key is authorized.
  • kint authorize payload stamps 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's canWrite is true only while the expiry is in the future, so after that pushes fail until they authorize again. memory_status shows session_key.expiry.
  • kint authorize direct and kint authorize burn-nonce read the owner's private key from KINT_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-stdin

The 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 purpose field: "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#

  1. memory_status first. Read connected, data_key_cached, in_step, unanchored, held, chain_head (an error key there means the chain could not be read) and session_key (exists, authorized, balance_eth, expiry). If connected or data_key_cached is false, go to memory_connect with no arguments. If held is not null, kint-server is refusing to push: tell the human its message. The cached key expires after KINT_KEY_TTL, 24 hours by default, and every successful pull and push renews it.
  2. memory_pull when in_step is 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 answers FORK, which is the human's call. memory_verify refuses while the head is past this machine's mirror, so bring the mirror up to the head first.
  3. Recall with Sibyl's tools. memory_search, memory_recall and memory_get_state work as they always did. Their results are what the store holds now, not what the chain vouches for.
  4. memory_verify before acting on a recalled row. Pass the query, not a row key: it runs the same search memory_search runs (Sibyl's multi_record_search, precision gates included) and checks the top limit hits (default 5, where memory_search defaults to 10). Act only when decision is proceed, and only on the row the reason names.
  5. 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_push first: an unpushed edit to an anchored row is refused as drifted, and that refusal holds every kint-server push until the human runs kint push.
  6. memory_push when 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.
  7. memory_history for "what did this say before". Pass tier (entity, state or reference), key, category for entities, and optionally block.
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 read

ok: false means the check itself failed (error holds the exception name), which is not a proceed either. Each check has one of four statuses:

statusmeaning
verifiedthe stored text hashes to the leaf anchored on Base and the merkle proof against the anchored rows_root passes
driftedthe 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
unanchoredthe row is new since the last anchored epoch; nothing on the chain vouches for it yet
missingthe 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 not ok: there was nothing to act on, so rephrase the query (on abstained_on, drop the word in verdict.tokens[0]) or ask.
  • A drifted row is a human decision. If the change was theirs and intended, kint push in their terminal anchors it; memory_push answers HELD while the refused value is in the store. If it was not, kint pull --discard-local moves 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 drifted or missing hit is written back as a Sibyl entity (category kint_refusal, status refused, 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 answers HELD; 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-write is the CLI's way to skip it.

Anti-patterns to avoid#

  • Do NOT treat memory_search or memory_recall output as checked. Only memory_verify decides.
  • Do NOT push past a refusal or a HELD. memory_push is held on a refused value, and kint push from 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-skipped or kint rekey --over-skipped yourself. 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) or kint pull --discard-local without the human. It moves memory.db (and its -wal and -shm files) 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 pull takes --owner, --discard-local, --force-scan and --full.
  • Do NOT rely on force_scan. memory_pull and kint pull accept 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=session and expect a terminal kint connect to reach the server. With session the 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).

errorwherefix
NOT_CONNECTED, hint call memory_connectmemory_push, memory_pullmemory_connect with no arguments; give the human the human_steps
OWNER_REQUIREDmemory_connect with a secretthis 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 missingpushthe human runs kint connect again; nothing was dropped
CHAIN_MOVED: ... another machine pushed., hint call memory_pull firstpushmemory_pull; with unanchored changes here it answers FORK (below), which is the human's call
BUSY, hint a push is already runningmemory_pushwait, then memory_status
HELD: refusing to anchor: ..., hint nothing was anchored and nothing was dropped; memory_verify still refuses the rowmemory_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...pushthe 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 gonepushkint 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 pullpushmemory_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 epochpushshrink or split that row in Sibyl; nothing else is blocked
PUSH_FAILED: the full state (N rows) compresses to ... snapshots are single-epochmemory_push(snapshot=true)push without snapshot; a restore just walks more epochs
KeyError_: no session key on this machinepushkint session-key create, then fund and authorize
TimeoutError: another kint process holds the head lock for this spacepush, pullanother 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 forkpullthe 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 sawpullwrong 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 endpointpullthe 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 ...pullthe 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 headpullwait and retry; do not override it
NOT_FRESH: ... stale or lying RPC, refusingpullthe RPC served a head older than this machine already saw; use another endpoint
PULL_FAILED: no data key on this machinepullmemory_connect with no arguments
ConnectError: this key (tag ...) does not open the vault anchored under 0x...connectwrong wallet, passphrase or tenant; the recovery code still opens it
ConnectError: this recovery code does not belong to the vault anchored under this ownerconnectcheck the owner and the tenant
KintCryptoError: recovery code checksum failed (typo?)connectthe code was mistyped; the human types it again
ChainUnreadable: cannot read the chain headconnectan 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 itCLI connectthe 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 argvCLI connectpipe the signature in; never put it on the command line
kint: --passphrase-stdin and --signature - cannot share stdinCLI connect, rekeyuse --passphrase-prompt or --signature-file
kint: no owner known on this machine: pass --owner 0x... or run kint connectCLIpass --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, writer

The 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.

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.