AI coding agents regularly need real credentials — an API token to call GitHub as you, a key to hit your billing provider, a token to deploy. The path of least resistance is to paste the secret into the chat, but then the value lives in the model’s context, the transcript, and your provider’s logs forever. A secrets vault backed by an encrypted Turso database lets an agent use a secret without ever seeing its value: the secret is injected into a child process, the command runs, and the output is scrubbed before the agent sees it.
This guide covers the schema, the encrypted connection, and the queries for a local secrets vault that an agent can use but never read.
Schema
The vault uses two tables: one for the secrets themselves, one for an append-only audit log.
CREATE TABLE IF NOT EXISTS secrets (
name TEXT PRIMARY KEY,
value TEXT NOT NULL,
provider TEXT,
account TEXT,
environment TEXT,
access TEXT,
tags TEXT NOT NULL DEFAULT '',
description TEXT,
created_at TEXT NOT NULL,
last_used_at TEXT
);
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
ts TEXT NOT NULL,
secrets TEXT NOT NULL,
command TEXT NOT NULL,
cwd TEXT NOT NULL,
exit_code INTEGER,
outcome TEXT NOT NULL
);
How it fits together
secrets stores each credential keyed by name. The value column holds the secret itself and is only ever read to inject into a child process, never returned to the agent. The metadata columns (provider, account, environment, access, tags) describe a secret without revealing it, so an agent can reason about “the production database token” by its attributes.
audit_log is an append-only record of every use: which secrets were injected, the command, the working directory, the exit code, and the outcome (ran or denied).
Connecting
The vault is a single encrypted database file. Turso encrypts every page at rest when you pass a key at connection time, and multiprocess_wal lets several processes (a couple of agent sessions plus a CLI) open the same file safely:
import { connect } from "@tursodatabase/database";
const db = await connect("vault.db", {
encryption: { cipher: "aes256gcm", hexkey },
experimental: ["multiprocess_wal"],
});
await db.exec(SCHEMA); // The CREATE TABLE statements above
hexkey is a 64-character hex string. Where it comes from is up to you. You can derive it from a passphrase with scrypt (and never store it), unwrap it from the OS keychain, or fetch it from a cloud KMS. Turso only needs the key at connect() time; the storage layer is identical regardless of the source.
Storing a secret
Upsert by name so rotating a value keeps the existing metadata:
INSERT INTO secrets (name, value, provider, account, environment, access, tags, description, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(name) DO UPDATE SET
value = excluded.value,
provider = COALESCE(excluded.provider, secrets.provider),
account = COALESCE(excluded.account, secrets.account),
environment = COALESCE(excluded.environment, secrets.environment),
access = COALESCE(excluded.access, secrets.access),
description = COALESCE(excluded.description, secrets.description);
Listing without exposing values
The query an agent is allowed to run simply never selects the value column:
SELECT name, provider, account, environment, access, tags, description, created_at, last_used_at
FROM secrets ORDER BY name;
This returns secret names and attributes — enough for an agent to pick the right credential — while the value never leaves the database.
Using a secret
To actually use a secret, read its value, inject it into a child process as an environment variable, run the command, and scrub the value out of the output before returning it:
SELECT value FROM secrets WHERE name = ?;
The injection and scrubbing happen in application code, not the database:
const { value } = await db.prepare(
"SELECT value FROM secrets WHERE name = ?"
).get([name]);
const result = await runCommand(command, {
env: { ...process.env, [name]: value },
});
// Replace every occurrence of the value before the agent sees the output
const safe = result.stdout.replaceAll(value, "***");
Scrubbing captured output is not a sandbox. A command that can read a secret can always leak it on purpose (write it to a file, send it over the network). What the vault guarantees is narrower and still useful: the model never receives the value, and a careless echo $TOKEN gets redacted. The secret only ever lives in the encrypted file and the short-lived process that used it.
Auditing every use
Record each access as a row, and update the secret’s last_used_at:
INSERT INTO audit_log (ts, secrets, command, cwd, exit_code, outcome)
VALUES (?, ?, ?, ?, ?, ?);
UPDATE secrets SET last_used_at = ? WHERE name = ?;
“Who used the production key, when, and for what” is then an ordinary query:
SELECT ts, secrets, command, cwd, exit_code, outcome
FROM audit_log ORDER BY id DESC LIMIT ?;
Key design points
- Encrypted at rest, for free. Turso handles whole-database encryption (AES-256-GCM, or one of the faster AEGIS ciphers). The vault never touches a cipher or manages page encryption — it derives a key and hands it to
connect().
- No coordinating daemon.
multiprocess_wal lets each agent session and CLI invocation open the same encrypted file directly, so the database is the shared state instead of a long-running server you have to keep alive.
- The value never appears in a list. Listing is a
SELECT that omits the value column; the agent sees names and attributes only.
- Fail closed. An unknown secret name aborts before the command runs, rather than executing with a missing credential.
- Auditing is just a table. Every use is an
INSERT; reporting on it is a SELECT you can export or alert on.
Example
keymaxxer is an MCP server that implements this pattern as an encrypted secrets vault for AI coding agents.