An AI assistant that remembers things about your business is genuinely useful. "Our fiscal year starts in February." "Flag any refund over $500." Teach it once and every future answer improves.
It's also a new attack surface. If an assistant learns, anyone who can influence what it reads can try to teach it lies. Researchers call this memory poisoning: plant a false "fact" today and the assistant repeats it as truth for months — no malware, no breach, just a note taken at the wrong moment. Most writing on this topic points at the problem. This post is what OpenNeko actually ships against it, with the code.
Every memory carries a cryptographic seal
When OpenNeko stores a memory, it computes an HMAC — a keyed hash — over the
fields that define what the memory is and who it belongs to, using a
per-organization key derived from the deployment secret
(packages/llm/src/work/memory.ts):
/**
* SEC6: HMAC over the row's identity-bearing fields with a per-org key
* derived from the deployment secret. A row whose stored hash no longer
* matches was modified outside this store (DB tampering / memory
* poisoning) — readers drop it from agent context.
*/
function memoryIntegrityHmac(row: {
orgId: string; userId: string | null; kind: string;
scope: string; scopeId: string | null; text: string;
}): string | null {
const key = deriveSigningSecret(`work-memory:${row.orgId}`);
return createHmac("sha256", key)
.update([row.userId ?? "", row.kind, row.scope,
row.scopeId ?? "", row.text].join("\u0000"))
.digest("hex");
}(Even the separator is chosen: fields join on a null byte, so no crafted text value can shift a field boundary and produce a colliding hash.)
Note what's inside the hash: not just the text, but the org, the user layer, the kind, and the scope. Editing a memory's content breaks the seal — and so does re-pointing an existing memory at a different scope or user. You can't take a legitimate note and quietly move it somewhere more influential.
On every read, the seal is verified before the memory reaches the agent:
/** SEC6: keep tampered rows out of anything an agent reads. */
function dropTampered(memories: WorkMemory[]): WorkMemory[] {
const ok: WorkMemory[] = [];
for (const memory of memories) {
if (memory.integrityOk) ok.push(memory);
else console.warn(
`[work-memory] integrity check FAILED for memory ${memory.id} ` +
`(org ${memory.orgId}) — excluded from agent context`,
);
}
return ok;
}A row edited behind the store's back — SQL console, a bug, an intruder with database access — simply stops existing as far as the agent is concerned, and screams in the logs. The threat this closes is specific: an attacker with write access to the database still can't steer the agent through its memory, because they don't have the deployment secret the HMAC key derives from. Tampering downgrades from "silent influence" to "detected deletion."
Learned facts expire; durable facts don't
Poisoned facts are loud; stale facts are quiet and nearly as damaging. "Normal" for January order volume isn't normal in July. So TTLs are kind-aware:
/** Thread notes are short-lived; durable kinds never expire by default. */
const THREAD_NOTE_TTL_DAYS = 30;
const PENDING_MEMORY_TTL_DAYS = 14;Working notes an agent takes during a conversation age out in 30 days. Memories the agent proposed but no human has confirmed sit in a pending state that expires in 14. Durable kinds — business rules, metric definitions, company context — persist, because a human ratified them. The lifecycle encodes a hierarchy of trust: what the agent observed decays; what you confirmed endures.
Every workspace gets an invisible git repo
Everything the assistant learns or is configured with — skills, workflows,
memory — is snapshotted into an actual git repository per organization,
committed automatically by the system
(packages/llm/src/config-vcs/index.ts):
/**
* config-vcs: invisible auto-versioning. One git repo per org whose
* working tree IS the org agents dir; only the config artifacts
* (skills/, workflows/, memory/) are tracked. Commits are
* system-authored — no personal identity in git. Failures are the
* caller's to swallow: a versioning hiccup must never fail the
* artifact write itself.
*/Durable memories are serialized on a nightly sweep to
memory/<kind>/<id>.md — markdown with frontmatter, deliberately without
embeddings and without personal-layer rows, so private data stays
deletable as a unit:
---
id: 3f2a…
kind: business_rule
pinned: true
updated_at: 2026-08-02T04:00:00.000Z
---
Refunds above $500 require a second approval.The snapshot tree is rewritten wholesale each sweep, so deletions and archived memories actually disappear from the current state (while remaining in history — that's the point of a VCS). Users never see git; they see "browse what changed, restore any earlier state." Two constraints from the comments worth stealing for any system like this:
- The safety net must never drop you. A snapshot failure cannot fail the underlying write.
- No personal identity in commit authorship. The history records what changed, not whom to blame.
The action log is a hash chain
Memory is what the agent believes; the audit log is what it did. That log
is an append-only per-org chain where every record binds the hash of the one
before it (db/migrations/0046_audit_chain.sql):
-- Each link binds the previous link's hash, so any retroactive edit or
-- deletion breaks every later link; verifyAuditChain recomputes the
-- chain and reports the first break.
CREATE TABLE audit_chain (
id bigserial PRIMARY KEY,
org_id text NOT NULL REFERENCES organization(id) ON DELETE CASCADE,
seq integer NOT NULL,
entity_kind text NOT NULL,
entity_id text NOT NULL,
event text NOT NULL,
payload jsonb NOT NULL DEFAULT '{}'::jsonb,
payload_hash text NOT NULL,
prev_hash text NOT NULL,
chain_hash text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX audit_chain_org_seq_unique ON audit_chain (org_id, seq);It covers the governance surface — action-request transitions, action
executions, run lifecycle. Alter or delete any row and every later
chain_hash stops verifying; verifyAuditChain recomputes the chain and
reports the first break. The unique (org_id, seq) index means you can't
even insert into the middle quietly. Same construction that makes a
blockchain tamper-evident — minus the coins — and the payload rides along as
JSONB so the whole thing exports to your SIEM or your auditor.
The paranoia is the product
OpenNeko's pitch is that the memory of how your business runs — the promises, exceptions, baselines, and decisions — is an asset that should live on your infrastructure, not inside whichever model vendor you rent this quarter. But a memory worth owning is worth defending on three fronts: integrity (the seal), freshness (the TTLs), and history (the git repo and the chain). Miss any one and your "asset" is a liability with a nice UI.
All of it is inspectable: github.com/open-neko/openneko. The positioning it serves is rent the intelligence, own the context — this is what "own" means when you take it seriously.
