~/.chatcli. All of it is encrypted by default with AES-256-GCM. There is nothing to configure: on the first start ChatCLI creates a key, keeps it in the operating system’s keychain, and seals every store with it.
Someone who copies your ~/.chatcli directory, a backup of it, or a stolen disk image gets ciphertext. They do not get your conversations.
Encryption at rest protects data on disk. A process running as your user while ChatCLI is unlocked can still read what ChatCLI can read. It complements full-disk encryption (FileVault, LUKS, BitLocker) and does not replace it.
What is encrypted
Each payload is bound to its own path and tenant (authenticated data), so a file copied to another store or another tenant does not open there.
The credentials in
auth-profiles.json (OAuth and API keys) were already encrypted, with their own key. That key now lives in the keychain too.
Not encrypted, on purpose:
- the daily budget counter,
costs/daily-spend.json, which is read before any key is needed; - exports you ask for (
/session export,/memory export,/context export), which are files you take somewhere else (see Exports); - the hash-chained audit trail (
CHATCLI_AUDIT_LOG_PATH), which is sealed line by line only under a key fromCHATCLI_ENCRYPTION_KEY: it is a file operators ship to a log pipeline; - coder checkpoints, which are copies of the project files that are already on disk in plaintext;
- the scratch directory of an agent session, which is removed when the session ends.
Where the key lives
ChatCLI keeps one secret in the operating system’s credential store. The key that seals your data is derived from it (HKDF-SHA256), and so is a separate key for the credential store. Both come from the same secret but are cryptographically separate.
Where there is no keychain (a server, a container, Linux without
secret-tool, or CHATCLI_KEYCHAIN_BACKEND=file), the secret is the file ~/.chatcli/.auth-key, mode 0600. The data is still encrypted, but whoever copies that file together with the data can open it. /config security warns about this case. For stronger protection there, set CHATCLI_ENCRYPTION_KEY from a secret manager.
Once the key is in the keychain, a marker next to where the key file was (~/.chatcli/.auth-key.keychain) records it. From then on ChatCLI never creates a new key for this installation: a context that cannot reach the keychain (an SSH session, a container, a cron job with CHATCLI_KEYCHAIN_BACKEND=file) gets an error instead of a fresh key that would orphan your data, and a key file that shows up later (a restored backup) does not replace the keychain key.
CHATCLI_KEYCHAIN_BACKEND chooses the store:
auto(default): the keychain where one is available, the file otherwise.keychain: the keychain always.file: the file always.
file after the key went to the keychain writes the same key to ~/.chatcli/.auth-key (the keychain item is kept), and moving back reads it from the keychain again.
Keep a recovery key
The REPL reminds you of this once, on the first start after your data is encrypted. The keychain holds the only copy of the default key. If it is lost — a reinstalled system, a reset keychain, a new machine — the encrypted stores cannot be opened. Keep a copy outside the machine once:CHATCLI_ENCRYPTION_KEY opens every store sealed with the default key:
Choosing the key yourself
CHATCLI_ENCRYPTION_KEY takes precedence over the keychain. Use it when the key must come from outside the machine:
- servers and the gateway in production;
- Kubernetes, where the operator injects it from a Secret (
spec.features.encryptionKeyRef); - CI;
- sharing encrypted stores between machines.
/config security use-keychain opens every file with your key and writes it again with the keychain key (it does not copy your key into the keychain); after that, remove the variable. ChatCLI encrypts with one key at a time (yours while the variable is set, the keychain key otherwise), but when a file does not open with that key it tries the other one, so a file encrypted with either key keeps opening.
A secret of 32 characters or more is used directly. A shorter one is treated as a passphrase and stretched with Argon2id. Keep it in a secret manager: data sealed with it does not open without it.
chatcli server never creates a key on its own; on Kubernetes the key lives in a Secret. See Kubernetes.Kubernetes
chatcli server never invents a key on its own: a key created inside a container would be lost on the next restart while the stores live on a volume. On Kubernetes the key lives in a Secret instead, and encryption at rest is on by default on both deployment paths.
- A key you supply through
extraEnvor anenvFromSecret always wins: the server reads its key Secret only whenCHATCLI_ENCRYPTION_KEYis empty. - Opt out with
security.encryptionAtRest: false(chart) orspec.features.encryptionAtRest: false(operator). - A deleted key Secret is never replaced behind your back. If the Secret is missing but the server’s stores already hold encrypted data, the server does not create a new key and does not start; the operator, which records that it created the Secret (annotation
platform.chatcli.io/atrest-key-created), stops the reconcile with an error instead of recreating it. Restore the Secret from your backup to recover. To start over and discard the encrypted data, delete the stores (and, with the operator, remove the annotation). - Back the key Secret up together with the PVC data (Velero, or your secret manager).
Upgrading from an older version
Older versions wrote everything in plaintext unlessCHATCLI_ENCRYPTION_KEY was set. The first start of a version with encryption on by default migrates your data:
- The key is created and kept in the keychain.
- An existing
~/.chatcli/.auth-keyfile is moved into the keychain when the keychain holds no key yet. The file is removed only after the keychain returns the same key. If the keychain already holds a different key (another installation may depend on it), nothing moves automatically: the file keeps being used and the keychain is left untouched./config security use-keychainmoves it on request, after checking the file key and keeping the keychain’s value asauth-key.previous. - Every store still in plaintext is sealed in the background. Each file is replaced atomically, so an interrupted pass loses nothing and the next start resumes it.
- Files modified in the last 10 minutes are left for the next start, because another ChatCLI window may still be writing them. A writer seals its own files on its next save anyway.
- Short-lived commands (
chatcli -p) migrate a share of the files per run.
- When everything is sealed, ChatCLI records it and stops checking.
CHATCLI_ENCRYPTION_KEY set (a server, the operator, or you opted in earlier), nothing is rewritten behind your back: each store is sealed on its next save, and /config security reseal seals the rest when you ask. The pass only touches the files each store reads sealed; an export, a note or a repository you keep in those directories is left alone, and each file keeps its modification time.
Your existing data keeps opening during and after the migration: a plaintext file is read as it is and sealed on its next save. To seal everything at once instead of waiting, run:
An older ChatCLI version cannot read the encrypted stores: it does not know where the key lives. To go back to one, first write the stores back in plaintext with the new version:With encryption off,
reseal decrypts every store instead of sealing it. Then install the older version and keep CHATCLI_ENCRYPTION_KEY=off while you use the new one.Checking the status
/config security shows:
Unavailable means the key could not be read: a locked macOS keychain in an SSH session, or Linux with
secret-tool but no desktop session. ChatCLI then writes new data in plaintext and cannot open sealed files until the key is reachable again. It never generates a new key in that state, because a new key would orphan the data sealed with the old one. Unlock the keychain, or set CHATCLI_KEYCHAIN_BACKEND=file on machines that never have a desktop session.
Rotating the key
From the default keychain key to a key of your own: setCHATCLI_ENCRYPTION_KEY and run /config security reseal. The keychain key still opens what it sealed, and the reseal moves every store to the new key.
From a key of your own back to the keychain key: with CHATCLI_ENCRYPTION_KEY still set, run /config security use-keychain. It re-encrypts every store with the keychain key (your key is not copied anywhere); then remove the variable from your environment. If you forget to remove it, nothing breaks: new data is encrypted with your key again, and files encrypted with either key keep opening.
From one CHATCLI_ENCRYPTION_KEY to another:
- Set the new key in
CHATCLI_ENCRYPTION_KEY, and the old one inCHATCLI_ENCRYPTION_KEY_PREVIOUS(a comma-separated list). - Run
/config security reseal. Every store is rewritten with the new key. - When it reports no errors, unset
CHATCLI_ENCRYPTION_KEY_PREVIOUS.
Turning it off
/config security reseal while it is off.
Exports
Exports are files you take somewhere else, so they come out in plaintext (mode0600) and the command says so:
A plaintext export imports anywhere. Keep it as carefully as the data it holds, and delete it when you no longer need it.
Related
For what to do when something goes wrong — a locked keychain, a new machine, a cron job, a downgrade — see Encrypted data: protect it and recover it.Security overview
Every protection layer and its status.
Session management
Sessions, autosave and the transcript journal.