Documentation
Secret vault
Where credentials live and how they are handed out.
On this page
One file, and it stays here
Every credential TDE holds — API keys, connector tokens, database connection strings, generated service passwords — lives in a single encrypted SQLite file:
~/.tde/secrets.dbThe file never leaves the machine, and neither does the key that opens it. Not to TDE's servers, not to another one of your devices, not into a backup TDE makes. What Sync replicates is a small allowlist of values, re-encrypted under a separate license-derived key — never this file and never its key.
What is inside
One table, one row per secret:
| Column | Holds |
|---|---|
name | The lookup name, unique — GITHUB_TOKEN, LINEAR_API_KEY:my-project |
ciphertext | The encrypted value, base64 |
nonce | A fresh random 96-bit nonce, base64 — one per write |
scope_kind, scope_value | global, project:<name> or connector:<name> |
created_at, updated_at | Timestamps; the update clock is the conflict resolver |
The cipher is AES-256-GCM-SIV with a 256-bit key. Every write generates a new nonce, so storing the same value twice produces different ciphertext. The nonce length is validated on read rather than assumed.
The key
The vault key is 32 random bytes. It is never derived from a constant, never stored in the database, and never transmitted. Where it is kept depends on what kind of machine you are on, and TDE decides that by probing rather than guessing.
| Machine | Backend | Where the key lives |
|---|---|---|
| Desktop with a working credential store | OsKeyring | The OS keychain, under service dev.tde.secret-vault |
| Headless VPS, container, no session bus | KeyFile | ~/.tde/vault.key, 32 bytes, mode 0600 |
On Linux the keyring is the Secret Service over D-Bus; on macOS it is the system
keychain. If neither answers — no D-Bus, no unlocked collection, a bare
container — TDE falls back to the key file. The file is created with
create_new and mode 0600 in one step, so it is never world-readable even
momentarily, and two racing processes cannot clobber each other's key. If it is
later found with a looser mode, TDE narrows it back to 0600 rather than
refusing to start.
Resolution order is strict, and the backend is pinned by the presence of the key file so it cannot drift between boots:
vault.keyexists → that is the key. The keyring is not even probed.- The keyring returns a key → use it.
- The keyring is empty but the vault has rows → hard error. Those rows are not recoverable and TDE will not pretend otherwise by minting a new key.
- The keyring is empty and the vault is empty → mint a key into the keyring.
- The keyring is unavailable and the vault has rows → recoverable error. TDE never silently re-keys.
Connectors store names, not values
~/.tde/connectors.json records which connectors exist, their endpoints, org
slugs and chat ids — and for credentials, only the name of a vault entry. The
in-memory field that holds resolved secrets is marked non-serializable, which is
the load-bearing detail: there is no code path that could write a token into that
file, because the serializer does not know the field exists.
Where secrets are not
The vault would be pointless if values leaked out the side. Four boundaries are enforced in code, each with tests pinning them:
Debug output. SecretEntry, SecretRecord, ConnectorConfig,
DatabaseProfile, the Slack broker authorization and the service secret material
all have hand-written Debug implementations that print [REDACTED]. A
connector's debug output lists the role keys it holds — token, bot_token —
and never their values.
Job payloads. A pipeline run snapshot is validated before it is persisted,
and a snapshot carrying an agent API key is rejected outright with
snapshot must never contain an agent API key. The field itself is marked
skip-serialize and skip-deserialize, so the rejection is a belt over a brace.
Stream events and process output. Agent runner output is scrubbed: every
value in the project environment is replaced with [REDACTED] in thinking text,
message text, tool names and details, usage records, permission requests and raw
stdout. Longest values are replaced first, so one credential that is a prefix of
another cannot leave a suffix behind.
Process arguments. Credentials that a third-party CLI needs are injected as environment variables, never as argv, so they do not show up in a process list.
Reloaded at execution time
Keys are not baked into work when it is scheduled. A durable job freezes its pipeline, model, prompt, budget and agent selection into an immutable payload — and then only the secrets are rehydrated from the freshly loaded scheduler at execution time. The same pattern holds elsewhere: each bot run opens the vault inside its worker, and each sync phase opens it per phase.
The practical consequence is the useful one: rotating a key takes effect on the next job without re-creating anything, and a job that has been queued for a day does not run with yesterday's revoked credential.
Replication is opt-in per name
Sync does not replicate the vault. It replicates an allowlist of exact names plus two prefixes, re-encrypted under a license-derived key:
ANTHROPIC_API_KEY GITHUB_TOKEN OPENAI_API_KEY
CURSOR_API_KEY GROQ_API_KEY OPENCODE_GO_API_KEY
DEEPSEEK_API_KEY LINEAR_API_KEY XAI_API_KEY
GEMINI_API_KEY MISTRAL_API_KEY
LINEAR_API_KEY:* STRIPE_API_KEY:* SLACK_TOKEN:triage:*Everything else stays local, and the exclusions are reasoned rather than
accidental: TELEGRAM_BOT_TOKEN because a bot's long-poll is single-consumer and
two machines would fight over it, POSTGRES_DSN:* and service credentials
because they point at loopback ports on one host, EMAIL_PASSWORD:* and
SENTRY_TOKEN because they need machine-local configuration to be usable at all.
The governing rule is that a replicated secret must be usable alone on the
receiving machine — otherwise the new machine looks configured and is not.
Reading and writing it
There is no tde secrets command. The vault is written through the surfaces that
own the credentials — Connectors, the
Database profile form, the
Store's service installer, the agent
configuration screen — and read by the code that needs them. The standalone
tde-control TUI has a Secrets tab for inspection.
On a headless machine where the vault cannot be opened at all, TDE degrades to an in-memory vault for the session rather than refusing to boot, so a broken keychain does not take the desktop down with it.