Agents · 2026-05-16 · By RedDB team · 4 min read

RedDB as Claude Code's memory backend — beyond CLAUDE.md

Swap the static MEMORY.md file for a queryable, embeddable memory layer. A SessionStart hook reads top-K memories into context, a /remember slash command writes them, and RedDB stores rows, vectors, and audit log in one transaction.

The problem with CLAUDE.md

Every CLI agent ends up reinventing the same hack: a markdown file at the project root that gets shoved into the system prompt on every turn. Claude Code calls it CLAUDE.md (plus a per-project MEMORY.md). Cursor has .cursorrules. Codex has AGENTS.md. The shape is the same and the failure modes are the same:

  • It bloats context. Every turn re-reads the whole file. By month three the file is 12 KB of half-stale rules nobody trims.
  • It is not queryable. “What did the user say about retries last month?” requires grep, not a query.
  • It is single-scope. One file per project. Cross-project memory (your shell preferences, your code-review tone) has to be duplicated or symlinked.
  • It is not auditable. Who wrote which line? When? Why? git blame if you remembered to commit it.

A database fixes all four. The interesting question is how to wire it in without forking the CLI.

The shape of the fix

Three pieces:

  1. A schema for memory rows — kind, scope, body, embedding, timestamp.
  2. A SessionStart hook that runs once when the agent boots and injects the top-K relevant memories as context.
  3. A /remember slash command that writes a memory row from inside a session.

All three talk to the same RedDB instance. The transactional boundary means a write from /remember is durable before the slash command returns, and the next SessionStart will see it.

Schema

A single table — call it memory — with five columns plus a vector index.

CREATE TABLE memory (
  id          TEXT PRIMARY KEY,           -- ulid
  kind        TEXT NOT NULL,              -- user | feedback | project | reference
  scope       TEXT NOT NULL,              -- global | <project-path> | <session-id>
  body        TEXT NOT NULL,              -- markdown, what the model reads
  embedding   VECTOR(1024) NOT NULL,      -- cohere-embed-v4 or similar
  created_at  TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX memory_scope_idx ON memory (scope, created_at DESC);
CREATE INDEX memory_embedding_idx ON memory USING hnsw (embedding vector_cosine_ops);

kind mirrors the categories Anthropic’s own memory skill uses (user / feedback / project / reference). scope lets a single instance serve global preferences alongside per-project facts. The HNSW index is what makes “top-K relevant” cheap.

The SessionStart hook

Claude Code fires a SessionStart event when a session boots. A hook is any shell command that returns JSON on stdout — Claude reads it and merges the result into the conversation as a system-reminder.

Drop this in ~/.claude/hooks/session-start-memory.sh:

#!/usr/bin/env bash
set -euo pipefail

SCOPE_GLOBAL="global"
SCOPE_PROJECT="$(pwd)"
TOP_K="${REDDB_MEMORY_TOP_K:-12}"

# CWD + last user message gives us a query embedding seed.
# At SessionStart there is no user message yet, so we seed with
# the project path. /remember will re-rank against real queries later.
QUERY="agent session start in ${SCOPE_PROJECT}"

curl -sS \
  --fail \
  --max-time 2 \
  -H "Authorization: Bearer ${REDDB_TOKEN}" \
  -H "Content-Type: application/json" \
  "${REDDB_URL}/memory/recall" \
  -d "$(jq -nc \
        --arg q "$QUERY" \
        --arg g "$SCOPE_GLOBAL" \
        --arg p "$SCOPE_PROJECT" \
        --argjson k "$TOP_K" \
        '{query:$q, scopes:[$g,$p], top_k:$k}')" \
  | jq '{
      hookSpecificOutput: {
        hookEventName: "SessionStart",
        additionalContext: ( .rows
          | map("- [\(.kind)] \(.body)")
          | join("\n")
          | "## Recalled memory\n\n\(.)"
        )
      }
    }'

Register it in ~/.claude/settings.json:

{
  "hooks": {
    "SessionStart": [
      {
        "matcher": "startup",
        "hooks": [
          { "type": "command", "command": "~/.claude/hooks/session-start-memory.sh" }
        ]
      }
    ]
  }
}

Two details worth flagging:

  • --max-time 2. If RedDB is unreachable the hook fails fast and the session boots without memory rather than hanging.
  • The output uses Claude Code’s hookSpecificOutput.additionalContext shape, which is appended as a system-reminder. This is the only way to inject text into the context window from a hook without invoking the model.

The /remember slash command

Slash commands in Claude Code are markdown files under ~/.claude/commands/. The frontmatter declares argument hints; the body is a prompt for the model. Here we use the ! prefix to run a shell command inline.

~/.claude/commands/remember.md:

---
description: Persist a memory to RedDB
argument-hint: <kind> <body...>
---

Persist this as a `$1` memory in RedDB, scoped to the current project.

!curl -sS \
  --fail \
  -H "Authorization: Bearer $REDDB_TOKEN" \
  -H "Content-Type: application/json" \
  "$REDDB_URL/memory/write" \
  -d "$(jq -nc \
        --arg kind "$1" \
        --arg scope "$(pwd)" \
        --arg body "${@:2}" \
        '{kind:$kind, scope:$scope, body:$body}')"

Confirm in one line. Do not summarise the body back.

Usage from inside a session:

/remember feedback Don't mock the database in integration tests — we got
burned last quarter when mocked tests passed and the prod migration failed.

The shell call writes the row; RedDB computes the embedding server-side (cheaper than embedding in the hook), commits, and returns the row id. The model gets the JSON response in its tool result and acknowledges.

The retrieval query

The /memory/recall endpoint is a thin wrapper over a single SQL query. It is worth showing because the shape of the query is the whole point of using a database:

WITH q AS (
  SELECT embed($1) AS v
)
SELECT id, kind, scope, body, created_at,
       1 - (embedding <=> q.v) AS score
FROM memory, q
WHERE scope = ANY($2::text[])
ORDER BY embedding <=> q.v
LIMIT $3;

One statement does scope filter + vector ranking + score. Compare to the markdown-file world, where filtering across global + project scope means concatenating two files in the right order and hoping the model picks the relevant lines out.

Why this matters

Three things you get for free that a flat file cannot give:

  • Scope composition. Global preferences + per-project facts + per-session scratch, ranked together, deduplicated by id.
  • Decay. A created_at column plus a half-life term in the ranking (score - 0.01 * age_in_days) lets stale memories fall off the top-K without anyone curating the file.
  • Audit. Every memory has an id and a timestamp. A second table memory_audit (id, action, actor, ts) gives you a git blame for facts the agent acted on.

None of this is exotic. It is the database treating agent context as just another query workload. The CLI changes are 80 lines of bash and JSON.

What is next in this pillar

This post is the anchor. The follow-ups make the pieces concrete:

  • A walkthrough of building a remember-this skill end-to-end in 30 minutes.
  • Slash commands with memory — /remember, /forget, /recall — and the queries underneath.
  • Hooks that mutate persistent state, made transactional so a half-finished hook doesn’t leave you with a phantom row.
  • An MCP server that exposes the same memory.* surface to every MCP-compatible CLI: one DB, all the agent tools.

If you want to be told when those land, grab the Atom feed.

© 2026 RedDB.io. AGPL-3.0 self-host · Managed Cloud.