@ciel2142/qmemd
v0.8.2
Published
Durable memory engine — remember/recall/forget over markdown facts, backed by the qmd SDK
Maintainers
Readme
qmemd
Durable memory engine: remember, recall, and forget plain-markdown facts backed by the @tobilu/qmd SDK. qmemd opens its own dedicated qmd index whose only collection is the memory data directory.
What it is
Facts are stored as individual markdown files under $QMD_MEMORY_DIR/{user,feedback,project,reference}/. The engine provides BM25 lex search and optional vector (embedding) search over that collection, plus a session-snapshot recall for use in AI agent hooks.
There are four fact types:
user— durable user preferences or identity factsfeedback— guidance on how to work / correctionsproject— repo/system facts not in code or gitreference— reference pointers and discoveries (URLs, dashboards, gotchas)
Install (users)
Published to npm as @ciel2142/qmemd (the installed CLI command is qmemd). Requires Node ≥ 22 (Bun also works).
npm install -g @ciel2142/qmemd # global CLI (command: qmemd)
npx @ciel2142/qmemd recall "<topic>" # or run without installingnpm ships the CLI only.
npm install -gputs theqmemdcommand on your PATH, but thescripts/installers (install-claude-integration.sh,install-windows.ps1) are not in the npm package — they need a git checkout. To turn an npm-installedqmemdinto Claude Code's memory engine, install the Claude Code plugin (it bundles the hooks, commands, skill, and trigger rule), then do the one step the plugin can't: setautoMemoryEnabled: falsein~/.claude/settings.json.
Install (dev)
qmemd depends on @tobilu/qmd 2.8.3 from the npm registry, so a clean clone installs standalone.
# In this repo (Node ≥ 22 is the official runtime; Bun also works):
npm install # or: bun install — resolves @tobilu/[email protected] from the registry
npm run build # tsc -p tsconfig.json → dist/
# Put qmemd itself on PATH:
npm link # or: bun linkRebuild after
src/changes:bin/qmemdexecsdist/cli/qmemd.js, so a linked or globalqmemdreflects your edits only afternpm run build. To run straight from source without a build, usenpm run qmemd -- <verb>(tsx).
Note:
@tobilu/qmdis published to npm, sobun installresolves it like any other dependency — no localqmdcheckout orbun link @tobilu/qmdis required for qmemd. (The standaloneqmdCLI/MCP is a separate tool that may still run from a local fork checkout via its own globalbun link; that is independent of qmemd's dependency.) To develop qmemd against an unreleasedqmd, runbun link @tobilu/qmdagainst your checkout to temporarily override the registry version with a symlink.
Install (Windows, native)
Runs qmemd as your Claude Code memory engine on native Windows (PowerShell / cmd).
Needs a git checkout —
scripts\install-windows.ps1is not in the npm package. If you installed vianpm i -g @ciel2142/qmemd, use the plugin instead.
Prerequisites (the installer checks and stops with instructions if any are missing):
- Bun for Windows —
winget install Oven-sh.Bun(reopen the shell afterward). - git —
winget install Git.Git.
Note:
@tobilu/qmdis published to npm, sobun installresolves it like any other dependency — no localqmdcheckout orbun link @tobilu/qmdis required. (The standaloneqmdCLI/MCP is a separate tool that may still run from a local fork checkout via its own globalbun link; that is independent of qmemd.)
Install:
powershell -ExecutionPolicy Bypass -File scripts\install-windows.ps1
# keep Claude's built-in auto-memory:
powershell -ExecutionPolicy Bypass -File scripts\install-windows.ps1 -NoDisableMemoryThe installer: builds qmemd (bun install / bun run build), adds <repo>\bin
to your User PATH (so the qmemd.cmd shim
resolves — open a new shell, and fully restart Claude Code, to pick it up), and idempotently wires
settings.json (SessionStart qmemd recall --session, PreToolUse(Bash) qmemd
hook beacon, autoMemoryEnabled=false) plus the @import in CLAUDE.md. It
honors $env:CLAUDE_CONFIG_DIR (default %USERPROFILE%\.claude).
Then, in the new shell (so the qmemd shim resolves), register the MCP server
yourself (pick a scope):
claude mcp add qmemd -- qmemd mcpEmbeddings on Windows: the native qmd embedding model is unverified on Windows, but install never depends on it. Lex recall, the session snapshot, and the beacon are model-free and work regardless; only hybrid (semantic)
recallneeds the model, and it loads lazily on first use. If it fails to load, lex recall (qmemd recall --lex "<query>") still works.
Optional — idiomatic data dir:
setx QMD_MEMORY_DIR "%LOCALAPPDATA%\qmd-memory"(otherwise facts live under%USERPROFILE%\.local\share\qmd-memory).
Verify the install
qmemd --version # prints the version → the CLI is on your PATHNot found? The fix depends on how you installed:
npm i -g(Linux / macOS / Windows): npm's global bin dir is already on PATH from a standard Node install — no PATH edit, no new shell. Ifqmemdis still missing, that dir isn't on your PATH:npm prefix -gprints it (<prefix>/binon Linux/macOS,%APPDATA%\npmon Windows) — add it, or reinstall Node so its installer wires PATH.npx @ciel2142/qmemd <verb>always works with no PATH setup.- git checkout (
npm link/bun link, orinstall-windows.ps1): these add a directory to PATH, which only affects shells opened afterward — open a new shell, and fully restart Claude Code so its hook/MCP child processes inherit the new PATH.
PATH setup is a checkout-only concern.
npm i -g @ciel2142/qmemdinstalls into a directory already on PATH, so the new-shell / restart-Claude step does not apply — prefer it unless you are developing qmemd itself.
Env vars
| Variable | Default | Purpose |
|---|---|---|
| QMD_MEMORY_DIR | ~/.local/share/qmd-memory | Memory data directory (markdown facts live here) |
| QMEMD_DB | ~/.cache/qmemd/index.sqlite | Dedicated qmd index database |
| QMEMD_EMBED_MODEL | embeddinggemma-300M (Q8 GGUF) | Embedding model for hybrid recall — independent of qmd's own QMD_EMBED_MODEL; pinned to the index via a sidecar marker, mismatches warn at open |
| QMEMD_EMBED_TIMEOUT_MS | 6000 | Bound on the lazy embed barrier in hybrid recall; on timeout recall fails open to lexical search (flagged degraded) |
| QMEMD_HTTP_PORT | 8182 | Port for qmemd mcp --http / the installed service (CLI --port wins) |
| QMEMD_BEACON_EVERY | 40 | Re-fire cadence of the PreToolUse memory-presence beacon (every N Bash calls; a repo pivot always fires) |
| QMEMD_SESSION_BUDGET | 2000 | Byte cap on the session snapshot (recall --session and the MCP/REST session paths); invalid values fall back to the default |
| QMEMD_SESSION_PROJECT_LIMIT | 0 | Recent unpinned project/reference facts in the session snapshot. 0 (the default) makes the snapshot pinned-only; set 5 to restore the recency-sliced lanes. Invalid values fall back to the default |
| QMEMD_TTL_<TYPE> | project 90d · reference 180d · user/feedback durable | Per-type default review window applied when a fact has no explicit review_by (e.g. QMEMD_TTL_PROJECT=180d, or never); an unparseable value falls back to the built-in default. Surfaces via qmemd stale — never auto-expires |
| XDG_CACHE_HOME | ~/.cache | Base for the default QMEMD_DB plus beacon/daemon state under <cache>/qmemd/ |
Git-backed sync
If $QMD_MEMORY_DIR is a git repo with a configured upstream, qmemd keeps it in
sync — entirely best-effort and gated, never failing a write:
- One-time setup (manual):
git init,git remote add origin <url>, thengit push -u origin <branch>to set the upstream. No auto-init. - On
remember/forget: stage + commit the change, thengit push. - On session start (
qmemd recall --session, and the MCP/RESTsession:truepaths):git pull --ff-onlyfirst.
Gating: no .git → nothing runs; .git but no upstream → commit only; .git +
upstream → commit/push/pull. Every git call is bounded by a 5s timeout, so a slow
or offline remote adds at most ~5s and then proceeds locally. Divergent histories
make pull --ff-only a silent no-op (reconcile manually); a push rejected because
the local branch is behind is harmless — the next session's pull fast-forwards.
Backup & recovery
The markdown corpus under $QMD_MEMORY_DIR is the only source of truth; the
index at $QMEMD_DB is a derived cache. To restore on a new machine (or after a
lost/corrupt index): clone the memory repo to $QMD_MEMORY_DIR, then qmemd
reindex (rebuilds the lex index) and qmemd embed (rebuilds vectors). Nothing
else needs backing up — deleting $QMEMD_DB is always safe.
Verbs
qmemd remember "<fact>" [--type user|feedback|project|reference] [--tags a,b] [--platforms linux,macos] [--pin] [--as slug] [--replace slug] [--supersedes slug] [--source S] [--ttl 90d|--review-by YYYY-MM-DD] [--force]
qmemd recall "<query>" [--lex] [--type T] [--platform P|--all-platforms] [--limit N] [--min-score N] [--full|--skim] [--json]
qmemd recall --session # start-of-session snapshot (user + feedback + pinned facts, plus a coverage footer)
qmemd show <slug> # print one fact in full (frontmatter + body) (alias: qmemd get <slug>)
qmemd list [--type T] [--tag t] [--project p] [--platform P] [--json] # browse the corpus (model-free)
qmemd tags [--project p] [--json] # tag(count) overview for a project (model-free)
qmemd stale [--limit N] [--json] # facts due for review + never-reviewed backlog; lists only, never removes (model-free)
qmemd reviewed <slug> [--ttl <N>d|w|m|y|never] [--review-by YYYY-MM-DD] # re-verified & unchanged: reset the staleness clock (review_by forward-set, updated untouched; --ttl never = durable)
qmemd forget <slug>
qmemd reindex # rebuild the lex index from the memory dir (after out-of-band edits; model-free)
qmemd embed [--force] # (re-)embed the memory collection
qmemd status # show store status as JSON
qmemd doctor [--fix] [--json] # audit frontmatter integrity; --fix repairs mechanical issues (writes .bak, model-free)
qmemd mcp # start the stdio MCP server (--http for the daemon; install-service for a durable unit)remember warns on a near-duplicate instead of writing — use --replace <slug> to update in place, --supersedes <slug> to retire the old fact under this new one (hidden from recall, kept on disk + git), or --force to write a new entry anyway. Dedup runs in three tiers: exact slug, FTS (BM25), then a model-free token-set near-dup pass whose contradiction classifier tells a true paraphrase (blocked as a duplicate) from a likely update — a differing version/number, polarity flip, or antonym — which is surfaced as a conflict with a type-derived authority comparison for you to resolve; qmemd never auto-resolves.
remember --ttl <N>d|w|m|y (or --review-by YYYY-MM-DD) schedules a re-verify date for facts that age — rotating creds, version pins, temporary states. A fact with no explicit review_by still ages: it inherits a per-type default review window — project 90 days, reference 180 days, user/feedback durable (never surface) — tunable per type via QMEMD_TTL_<TYPE> (see Env vars). qmemd stale then lists facts due (an explicit review_by, or an inherited window, now past) plus the never-reviewed backlog (decay-prone but not yet due); an undatable fact surfaces as due today. It is review-only: nothing is hidden or deleted, and recall ignores review_by.
Resolve each surfaced entry with one of:
qmemd reviewed <slug>— you re-checked the fact and it is still correct: reset the staleness clock totoday + the type's window. It forward-setsreview_byonly, leavingupdatedhonest (content age stays truthful). Add--ttl <N>d|w|m|y|neveror--review-by YYYY-MM-DDto set the next date explicitly;--ttl never(→review_by: never) marks the fact permanently durable.qmemd remember --replace <slug>— the fact changed: edit it in place (re-arm the clock with a fresh--ttl). Omit--typeto keep the existing one; pass a different one to retype the fact — it moves to that folder in one commit. (--forcenever retypes: it keeps the existing folder.)qmemd remember --supersedes <slug>— retire it under a successor fact.qmemd forget <slug>— drop it.
# after re-verifying each fact is still accurate:
qmemd reviewed redpanda-ca-cert # clock → today + the type's default window
qmemd reviewed clickhouse-sandbox-password --ttl 180d # ...or set the next review 180 days out
qmemd reviewed deploybot-macmini-ssh --review-by 2026-12-31 # ...or pin an explicit next-review date
qmemd reviewed redpanda-acl-convention --ttl never # decided permanent: stop surfacing (review_by: never)Over MCP the reviewed tool mirrors the CLI — { slug, ttl?, reviewBy? }, same semantics (see MCP server).
recall returns a truncated body preview per hit (≤500 bytes; pass --full for the whole body). show <slug> prints one fact in full, and list browses the corpus by type/tag/project without loading the model.
recall reports completeness (qmemd-40h): when matches exceed --limit or hybrid hits fall below the relevance floor, a footer notes N more match (raise --limit) / M below the 0.575 relevance floor (--min-score 0 shows all) — N+ when the search pool saturated and the count is a lower bound. Humans see it in-band; --json keeps the array shape and prints the note on stderr; MCP/REST return moreMatches/belowFloor/saturated fields.
Hybrid recall applies a relevance floor (--min-score, default 0.575): hits the reranker scores below it are dropped. The floor is on the reranker score — the reranker's calibrated relevance judgement (≈0.5 for a neutral/irrelevant hit, ≈0.7+ for a genuinely relevant one) — not the displayed score, which is position-dominated (rank-1 carries a large RRF bonus regardless of relevance). Pass --min-score 0 to disable the floor, or a higher value to tighten. The floor is hybrid-only — --lex ignores it, because the lexical path runs no reranker.
A freshly remembered fact is lex-searchable immediately. Vector (semantic) recall embeds any not-yet-embedded facts on demand — the first hybrid recall after a write does the embedding inline, so there is no background daemon to run.
MCP server
qmemd mcp is a stdio MCP server by default. It exposes six tools: remember, recall, forget, reviewed, get, and list, with the same semantics as the CLI verbs (the MCP get tool ↔ the CLI show verb; remember takes the same supersedes/platforms/ttl/reviewBy parameters). recall carries a truncated body preview per hit; get returns one fact's full body by slug; list browses by type/tag/project; reviewed resets a fact's staleness clock (forward-sets review_by, accepting the same ttl/reviewBy).
Register it under mcpServers in your MCP client config (e.g. ~/.claude.json), using any server name (here qmemd):
{
"mcpServers": {
"qmemd": {
"command": "qmemd",
"args": ["mcp"]
}
}
}Install as a Claude Code plugin (recommended)
qmemd ships as a native Claude Code plugin that bundles the skill, both hooks
(session snapshot + beacon), and the /qmemd:* commands in one install. It is the
recommended path for most users: it wires the same SessionStart snapshot and beacon
hooks the scripts/ installers do, but works straight from an npm i -g install —
no git checkout required.
The plugin deliberately does not declare an MCP server. Register one yourself,
either the stdio server (MCP server above) or the shared HTTP daemon
(qmemd mcp install-service, see HTTP API) — one
server, one set of tools. A plugin-declared server would stand up a second
process alongside whichever one you already run, duplicating all six tools in the
model's context and letting the two drift to different versions.
Install the CLI. The hooks and the
/qmemd:*commands shell out to it (as does whichever MCP server you register), so theqmemdcommand must be on PATH:npm install -g @ciel2142/qmemdThe hooks fall back to
npx -y @ciel2142/qmemdwhen the CLI isn't on PATH, but a first-evernpxMCP cold-start can exceed the client's initialize timeout — a global install is the reliable path.Add the marketplace, then install the plugin (run these inside Claude Code):
/plugin marketplace add Ciel2142/qmemd /plugin install qmemd@qmemdDisable Claude's built-in auto-memory — the one step a plugin can't do, since it is an app-level setting a plugin may not change: set
"autoMemoryEnabled": falsein~/.claude/settings.jsonso it stops competing with qmemd.Restart Claude Code, then verify: the
/qmemd:*commands appear in the slash-command menu, and a fresh session injects the qmemd rule + therecall --sessionsnapshot at the top of its context (the SessionStart hook).qmemd statusconfirms the CLI from step 1 (there is no--versionflag).
Migrating from the bash installer? The plugin and
scripts/install-claude-integration.sh wire the same SessionStart + beacon hooks;
running both double-fires the beacon. Remove the bash-installer wiring first:
scripts/install-claude-integration.sh --uninstallConversely, don't run scripts/install-claude-integration.sh while the plugin is
enabled — it re-wires the same hooks and re-introduces the double-fire. Use the
plugin or the bash installer, not both.
For Codex and Cursor, copy-paste MCP + rule snippets live in
integrations/.
Integrate with Claude Code
Installed from npm? The
scripts/install-claude-integration.shbelow is not in the npm package (it needs a git checkout). From annpm i -ginstall, wire everything through the plugin instead — it automates the trigger rule and SessionStart hook, leaving onlyautoMemoryEnabled: falsefor you.
Registering the MCP server makes the remember/recall tools available, but two
more pieces turn qmemd into the agent's actual memory engine:
- A trigger rule — when to remember and recall —
@imported into your global~/.claude/CLAUDE.md. The rule lives in this repo atclaude/qmemd.md(theqmemd-memoryskill is the on-demand how-to; this rule is the always-on policy). - A SessionStart hook that injects the session snapshot
(
qmemd recall --session) at the start of every session. autoMemoryEnabled: falseso Claude's built-in auto-memory stops competing with qmemd.
An idempotent installer wires all three:
scripts/install-claude-integration.sh # wire it up
scripts/install-claude-integration.sh --no-disable-memory # ...but keep built-in auto-memoryIt edits ~/.claude/settings.json (or $CLAUDE_CONFIG_DIR) and appends one
@-import line to ~/.claude/CLAUDE.md — re-running never duplicates. It does
not register the MCP server (the -s scope is yours to pick); it prints the
claude mcp add command to run yourself. The @-import is a live link: the rule
updates when you git pull this repo, so keep the checkout in place.
Already have the rule inline? If you previously pasted the
# Memory (qmemd)block directly into~/.claude/CLAUDE.md, delete that inline copy after running the installer — the@-import now supplies it, and keeping both duplicates the rule in every session's context. The installer dedupes its own@-import line, but it can't detect a hand-pasted inline copy.
HTTP API (REST + MCP over HTTP)
By default qmemd mcp speaks stdio. Run it instead as a long-lived localhost
HTTP server to get a programmable REST API over your memory — for non-MCP
clients like scripts, cron jobs, curl, or a local web UI — alongside
MCP-over-HTTP for Claude sessions, all sharing one store. (A single process also
keeps the embedding model resident across sessions, but that is a minor effect:
hybrid recall is rare and the session-start snapshot is model-free — the REST
surface is the reason to run it.)
qmemd mcp install-service # write a systemd --user (Linux) / launchd (macOS) unit, then print the activate commands
qmemd mcp install-service --print # preview the unit + commands without writing anything
qmemd mcp uninstall-service # remove the generated unit files
qmemd mcp --http --daemon # dev-only: unsupervised background process (dies on SIGTERM, no restart)
qmemd mcp stop # stop the --daemon process
qmemd mcp token # print the daemon's auth token (mints it if the daemon hasn't started)
curl localhost:8182/health # {"status":"ok","uptime":N} — the one route that needs no tokenAuthentication
The daemon binds loopback and rejects non-loopback Host and cross-origin
Origin headers, but that only covers the browser threat model: any other
process on the machine sends a loopback Host and no Origin, so it could read the
whole corpus. Every route therefore requires a shared secret — the one exemption
is GET /health, which returns a hash of the memory root and no fact data, and
which the CLI must probe before it has any reason to authenticate.
The token is 32 random bytes, minted on first start into
${XDG_CACHE_HOME:-~/.cache}/qmemd/daemon-token with mode 0600. It never
appears in a unit file, an environment dump, or a URL. The qmemd CLI reads that
file itself, so warm-daemon delegation needs no configuration; other clients send
it as the x-qmemd-token header:
curl -H "x-qmemd-token: $(qmemd mcp token)" localhost:8182/listPoint Claude Code at it (replaces the stdio registration — use one or the other):
claude mcp add --transport http qmemd http://localhost:8182/mcp \
--header "x-qmemd-token: $(qmemd mcp token)"Without the header the daemon answers 401. A qmemd recall whose delegation is
rejected falls back to the local cold path rather than failing — the same
degradation as an unreachable daemon.
REST endpoints (localhost, JSON):
| Method | Path | Body / query |
|--------|-------------|--------------------------------------------------------------------|
| POST | /recall | {query, lexOnly?, minScore?, type?, limit?, full?, skim?, allPlatforms?, platform?} or {session:true, project?} |
| POST | /remember | {fact, type?, tags?, project?, pin?, source?, as?, replace?, supersedes?, force?, platforms?, ttl?, reviewBy?} |
| POST | /forget | {slug} |
| GET | /list | ?type=&tag=&project=&platform= |
| GET | /get | ?slug= |
The server binds localhost only with no auth — same single-user trust boundary
as the CLI. For a durable daemon that restarts on crash and survives reboot, run
qmemd mcp install-service: it generates a systemd user service (Linux) or a
launchd LaunchAgent (macOS) capturing the current environment, then prints the
systemctl --user enable --now / launchctl bootstrap commands to run. The bare
--daemon flag is dev-only and unsupervised.
Build & test
npm run build # tsc -p tsconfig.json → dist/
npm test # vitest run (model-free: lex/filesystem paths only)
npm run qmemd -- <verb> # run the CLI from source via tsx, e.g. npm run qmemd -- recall --session