Agent memory now strips credentials before it stores anything
Agents paste .env files, curl commands and connection strings into memory, and memory is built to travel. Kognite now removes credentials on write. How we measured false positives on real text, and what it still doesn't catch.
Kognite team6 min read
An agent with memory writes down whatever is in front of it. Sometimes that is a preference or a decision. Sometimes it is a .env file it just read, a curl command with the Authorization header still attached, or a connection string copied out of a stack trace. Until today, Kognite stored all of it as written. As of 24 September 2026 it doesn’t: credentials are removed before a memory is stored.
Why a secret in memory is worse than a secret in a log
A log line sits where it was written. A memory is built to travel. Once a key was stored, Kognite embedded it, returned it from any search it matched, and handed it to every agent that recalled that context, in every later session and every connected client. For episode_log, the conversation was also sent to the model that extracts facts from it. One pasted token ended up in all of those places.
So redaction runs at the only point where it can prevent all of that: before the content hash, before extraction, before the embedding and before the row is written. Both write paths do it. That covers memory_add and the REST POST /api/memories, where string values in metadata are checked too. It also covers episode_log and POST /api/memories/ingest, where every message is checked.
What gets removed, and what stays
# what an agent sends to memory_add
Deploy notes for acme: CI uses ghp_9fQ…Xk2 and SESSION_SECRET=4c7e…b19.
Staging DB is postgres://app:Pw8…[email protected]:5432/app
curl -H "Authorization: Bearer eyJhbGciOi…" https://api.acme.dev/health# what is stored, embedded and returned by search
Deploy notes for acme: CI uses [REDACTED:github_token] and SESSION_SECRET=[REDACTED:secret_assignment].
Staging DB is postgres://app:[REDACTED:url_password]@db.staging.internal:5432/app
curl -H "Authorization: Bearer [REDACTED:jwt]" https://api.acme.dev/healthThe secret goes, and the context around it stays. The label in SESSION_SECRET= stays. The user and host in a connection string stay. The Bearer scheme stays. The marker names the kind of credential, so a later agent still knows that a GitHub token was configured here, just not what it was. What is recognised:
- Provider keys with a fixed prefix. Anthropic, OpenAI and other
sk-keys, Stripe secret, restricted and webhook keys, GitHub and GitLab tokens, AWS access key IDs, Google API keys, Slack tokens and webhooks, npm, Hugging Face and Resend tokens, and Kognite’s own API keys and OAuth tokens. - Structural secrets. PEM private-key blocks, JWTs, the password in
scheme://user:password@host, and the token inAuthorization: Bearer|Basic|Token. - Labelled values.
API_KEY=…,"client_secret": "…",password: …, and a metadata key such asapi_key, but only when the value doesn’t read like an ordinary word.
When anything was removed, the write response tells the agent what, by type and count:
{
"memory_id": "e53ff7df-…",
"replayed": false,
"redacted": [
{ "type": "github_token", "count": 1 },
{ "type": "secret_assignment", "count": 1 }
]
}MCP clients get the same field, because the tool result is the API response.
The hard part is not catching secrets
A redactor that catches everything is easy to write, and it would quietly damage the memories people actually want. max_tokens: 100000000 is a number, not a token. token: process.env.X is code, and deleting it makes a stored snippet useless. SECRET_KEY=your-secret-key is a placeholder from a tutorial. A false positive removes part of a memory without telling anyone. For most people that costs more than the leak it prevents. So we tuned for precision first, and measured it on real text instead of on examples we thought up ourselves.
- Conversation: all 5,882 turns of the public LoCoMo dataset. 0 changed.
- Technical writing: 14,213 paragraphs of our own documentation and blog posts across four products. This is exactly where false positives live: env-var tables, setup guides, curl examples. The first pass changed 21. We read every one, and all 21 were wrong: code like
secretKey = config.secretKey, shell likeAUTH_SECRET=$(openssl rand -base64 32), or placeholders likeuser:password@host. Each became a rule and a test. The same corpus now changes 0. - Production: random fake credentials sent through both write paths of the live API, then the database read back directly. No raw value was stored and none came back from search. This test found one more bug: in
SESSION_SECRET=abc123. The user prefers…the full stop went with the secret, because dots are legal inside a value. Punctuation that ends a sentence now goes back after the marker.
What it does not do
- It doesn’t guess from randomness alone. A 32-character random string with no label and no known prefix is kept. So are commit hashes, UUIDs and content IDs, which look exactly the same. Entropy-only detection would flag all of them. If your own services use custom token formats, a label such as
MY_SERVICE_TOKEN=…is what gets them caught. - It doesn’t rewrite the past. Memories stored before 24 September 2026 are unchanged. If you think a key went into memory earlier, search for it and delete the memory with
memory_forget. Then rotate the key anyway: that is the only fix that doesn’t depend on where else it was copied. - It isn’t a secrets manager. It’s a guard against accidents. It doesn’t invite you to store credentials in memory on purpose.
Redaction is on for every plan and every write, with nothing to configure. If you’re connecting an agent for the first time, the quickstart takes a couple of minutes, and the tool reference lists what each tool returns.