> ## Documentation Index
> Fetch the complete documentation index at: https://chatcli.edilsonfreitas.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Encryption at Rest

> ChatCLI encrypts the conversations, memory, contexts and other data it keeps on disk by default, with a key held in the OS keychain on macOS, Linux and Windows. How the key works, how an older installation is migrated, and how to check, rotate or turn it off.

ChatCLI keeps your sessions, transcripts, prompt history, long-term memory, knowledge contexts and attachments on disk under `~/.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.

<Note>
  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.
</Note>

## What is encrypted

| Store | Location | How |
| - | - | - |
| Saved sessions, autosaves, MCP session mirrors | `~/.chatcli/sessions/` | whole file |
| Transcript journal | `~/.chatcli/transcripts/` | each line |
| Prompt history | `~/.chatcli/history` | each line |
| Long-term memory: facts, profile, topics, projects, episodes, knowledge graph, summaries, daily notes, pending queue | `~/.chatcli/memory/` | whole file |
| Memory vector index, knowledge-context vectors | `memory/vector_index.json`, `contexts/*.vec.json` | whole file |
| Knowledge contexts (`/context`) | `~/.chatcli/contexts/` | whole file |
| Compression archive (CCR) | `~/.chatcli/ccr/` | whole file |
| Cost snapshots | `~/.chatcli/costs/` | whole file |
| Attachments (images and PDFs from the web UI, channels, ACP) | `~/.chatcli/attachments/` | whole file |
| Park snapshots | the OS config directory (`chatcli/parked`) | whole file |
| Gateway tenant stores | `~/.chatcli/tenants/<tenant>/…` | as above, bound to the tenant |

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](#exports));
* the hash-chained audit trail (`CHATCLI_AUDIT_LOG_PATH`), which is sealed line by line only under a key from `CHATCLI_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.

| OS | Store | Notes |
| - | - | - |
| macOS | Keychain (login keychain), through the `security` tool | No prompt: the item belongs to the `security` tool |
| Linux | Secret Service (GNOME Keyring, KWallet) through `secret-tool` | Needs `libsecret-tools` and a desktop session (D-Bus) |
| Windows | Credential Manager | Built in |

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

Switching between them never changes the key, so nothing breaks and nothing is lost. Moving to `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:

```text theme={"system"}
/config security recovery-key
```

It prints the recovery key (only in an interactive terminal; it is never logged). Store it in a password manager, not in a file next to your data. On any machine, setting it as `CHATCLI_ENCRYPTION_KEY` opens every store sealed with the default key:

```bash theme={"system"}
export CHATCLI_ENCRYPTION_KEY="<recovery key>"
```

The same value is what a cron job, a container or an SSH session that cannot reach the keychain needs.

**A recovery file instead of the raw key.** If you prefer a file you can keep on a USB stick or a cloud drive:

```text theme={"system"}
/config security recovery-key --save ~/chatcli-recovery.json
```

It asks for a passphrase twice (at least 12 characters) and writes the recovery key sealed with it (Argon2id + AES-256-GCM). The file opens nothing without the passphrase, so keep the two in different places. On a new machine, or after a reinstall:

```text theme={"system"}
/config security recovery-key --restore ~/chatcli-recovery.json
```

It asks for the passphrase and moves your data onto this machine's keychain key; the key is never shown or typed.

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

```bash theme={"system"}
export CHATCLI_ENCRYPTION_KEY="$(openssl rand -base64 32)"
```

It is not a one-way choice. ChatCLI always has its own key in the keychain, separate from yours. `/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.

<Note>
  `chatcli server` never creates a key on its own; on Kubernetes the key lives in a Secret. See [Kubernetes](#kubernetes).
</Note>

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

| Path | Default | Your own key |
| - | - | - |
| Helm chart | The server creates `<release>-atrest-key` on its first start (a random key) and every replica reads it; the chart adds a Role to `get` that Secret and `create` secrets | `security.encryptionKeySecret.name` (recommended for production: External Secrets, Sealed Secrets, Vault) |
| Operator | The operator creates `<instance>-atrest-key`, owned by the Instance like the sessions PVC, and lets the server only `get` it | `spec.features.encryptionKeyRef` |

* A key you supply through `extraEnv` or an `envFrom` Secret always wins: the server reads its key Secret only when `CHATCLI_ENCRYPTION_KEY` is empty.
* Opt out with `security.encryptionAtRest: false` (chart) or `spec.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 unless `CHATCLI_ENCRYPTION_KEY` was set. The first start of a version with encryption on by default migrates your data:

1. The key is created and kept in the keychain.
2. An existing `~/.chatcli/.auth-key` file 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-keychain` moves it on request, after checking the file key and keeping the keychain's value as `auth-key.previous`.
3. 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.
4. When everything is sealed, ChatCLI records it and stops checking.

The background pass runs with the default keychain key only. With `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:

```text theme={"system"}
/config security reseal
```

<Note>
  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:

  ```bash theme={"system"}
  export CHATCLI_ENCRYPTION_KEY=off
  ```

  ```text theme={"system"}
  /config security reseal
  ```

  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.
</Note>

## Checking the status

`/config security` shows:

| Row | Meaning |
| - | - |
| Encryption at rest | `on — key derived from the secret in the OS keychain`, `on — key from CHATCLI_ENCRYPTION_KEY`, `on — key derived from <file>` (no keychain: weaker), `off`, or `unavailable` with the reason |
| Key fingerprint | First 8 hex characters of a hash of the key, to compare machines without showing the key |
| Local data sealed | `yes`, `in progress`, how many files are left for the next start, or `pending` |
| Locked stores | Memory stores sealed with a key this process does not have. They are kept read-only so nothing overwrites them |

**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:** set `CHATCLI_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:**

1. Set the new key in `CHATCLI_ENCRYPTION_KEY`, and the old one in `CHATCLI_ENCRYPTION_KEY_PREVIOUS` (a comma-separated list).
2. Run `/config security reseal`. Every store is rewritten with the new key.
3. When it reports no errors, unset `CHATCLI_ENCRYPTION_KEY_PREVIOUS`.

## Turning it off

```bash theme={"system"}
export CHATCLI_ENCRYPTION_KEY=off   # also: none, disabled, false, 0
```

New writes are plaintext. Files that were already sealed **keep opening** with the key from the keychain, so turning encryption off never locks you out of your own data. To also write the existing stores back in plaintext, run `/config security reseal` while it is off.

## Exports

Exports are files you take somewhere else, so they come out in plaintext (mode `0600`) and the command says so:

| Command | Output |
| - | - |
| `/session export` | Markdown or JSONL, plaintext |
| `/export` | Trajectory JSONL, plaintext |
| `/context export` | Context JSON, plaintext |
| `/cost export` | JSON or CSV, plaintext |
| `/memory export` | JSONL: plaintext with the default keychain key; sealed line by line with a key from `CHATCLI_ENCRYPTION_KEY`, which you can carry to the machine that imports it |

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](/help/troubleshooting#encrypted-data-protect-it-and-recover-it).

<CardGroup cols={2}>
  <Card title="Security overview" icon="shield-halved" href="/security/overview">
    Every protection layer and its status.
  </Card>

  <Card title="Session management" icon="floppy-disk" href="/context/session-management">
    Sessions, autosave and the transcript journal.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.