Per-project memory palace, episodic memory across Claude Code sessions, an issue tracker, and command hooks — all backed by redb. Single Rust binary, one optional embedding-model download, no background daemon.
- Recall palace — per-project structured memory with rooms, drawers, 4-layer retrieval ladder (identity → Do-Not-Repeat → wake-up → room-filtered → hybrid search), local embeddings via
fastembed+all-MiniLM-L6-v2 - Hybrid search — semantic (cosine) + lexical (BM25) fused via RRF; wins over either alone on both exact identifiers and synonyms
- Episodic memory — indexes Claude Code session transcripts for full-text search across past sessions, with cross-references between sessions and the files they touched
- Issue tracker —
ndx issue add | list | close | …builds on top of drawers in the reserved_issues_room; status, milestone, and closed-at live as drawer metadata - Command hooks — PreToolUse on Bash injects CLI syntax hints, filters noisy output, logs events, and auto-injects wake-up context once per Claude session; PreToolUse on Read flags repeated reads of the same
(path, mtime)so Claude works from existing context instead of re-reading; PreCompact re-injects wake-up before context compaction; SessionStart/SessionEnd auto-mine the palace - File listing —
ndx listandndx findwalk the project tree (gitignore-aware), with optional token estimates per file. Content search is delegated to ripgrep / Claude'sGreptool — no in-binary trigram cache. - Cross-referencing — bridges session memory and recall palace ("which sessions touched this file?", "which drawers came from this commit?")
- Subagent-friendly — pure CLI interface works from any context, including Claude Code subagents and team members
- Claude-curated quality — slash commands (
/ndx-chore,/ndx-recall-classify,-score,-dedupe,-contradict,-summarize) delegate judgment work to Claude instead of brittle heuristics
Direct-access embedded storage; no daemon, no socket:
CLI (ndx recall *, ndx issue *, ndx xref drawer/drawer-session/git)
└─ direct access ──► {project}/.ndx/recall.redb
├─ drawers + BM25 index + embeddings
├─ rooms + links + metadata
├─ file/session/commit cross-references
└─ wake-up injection state
CLI (ndx memory *, ndx xref file/session, ndx scan)
└─ direct access ──► ~/.ndx/memory.redb (sessions, events, agents)
CLI (ndx list / find)
└─ walkdir over the project tree (gitignore-aware) — no index, no cache
The embedding model (all-MiniLM-L6-v2, ~90 MiB) downloads on first semantic operation and caches in ~/.ndx/models/.
curl -fsSL https://raw.githubusercontent.com/siy/ndx/master/install.sh | bashDownloads a prebuilt binary from GitHub Releases (macOS ARM64/x86_64, Linux x86_64/aarch64). Falls back to building from source if no prebuilt is available. Installs to ~/.local/bin/ndx, downloads 289 command manifests, registers the PreToolUse + PreCompact hooks, and installs 7 slash commands. Restart Claude Code after install.
Requires Rust 1.70+.
git clone https://github.com/siy/ndx.git
cd ndx
cargo build --release
cp target/release/ndx ~/.local/bin/
ndx installcd /path/to/your/project
ndx init # adds .ndx/ to .gitignore, appends ## ndx section to CLAUDE.md
ndx recall init # creates the recall palace (.ndx/recall.redb)Slash commands (/ndx, /ndx-chore, /ndx-recall-*) live globally in ~/.claude/commands/ after ndx install and are visible from every project Claude Code opens — ndx init does not copy them per-project. If you have an older project with stale per-project copies (from before this changed), run ndx init --clean-up to remove them; git-tracked copies are preserved with an explicit git rm instruction.
The recall palace is a per-project structured memory that stores decisions, rationale, architecture, and context — everything that disappears when a session ends. Here's the recommended lifecycle:
After ndx init, Claude Code sees the /ndx slash command which documents the full CLI surface. This is the discovery mechanism — Claude reads it and knows what ndx can do. From that point on, Claude can proactively suggest ndx commands, use recall search to find context, and run the maintenance skills without manual prompting. The tight integration flows from this one file.
# Derive drawers from past Claude Code sessions about this project
ndx recall mine --from-memory
# Optionally mine specific high-value files (not the whole tree)
ndx recall mine --project --path CHANGELOG.md
ndx recall mine --project --path docs/architecture.mdTip: Don't mine the entire repo blindly. Code files and large doc trees produce thousands of paragraph fragments that just duplicate the content. Mine session memory for decisions/rationale, and cherry-pick the files that capture why, not what.
/ndx-recall-classify # assign rooms to unclassified drawers
/ndx-recall-score # set importance (1-10) on default-5 drawers
Claude reads each drawer's text, proposes a room (e.g. architecture, decisions, people, tools), and scores importance. You review. Expect to delete noise drawers (assistant narration, markdown separators) during classification.
ndx recall search "why did we choose JWT" # hybrid semantic + lexical
ndx recall wake # L0 identity + L1 top drawers (for prompt injection)
ndx recall get --room decisions --limit 10 # all decisions, ranked by importanceThe PreToolUse hook automatically injects wake-up context (L0 + Do-Not-Repeat + L1) on the first Bash command of each Claude session — no manual wake needed. The PreCompact hook re-injects the same wake-up block whenever Claude compacts its context (manual /compact or automatic at the context limit), so palace context survives compaction intact.
Do-Not-Repeat channel — drawers in the reserved _do_not_repeat_ room render above L1 in every wake-up regardless of importance, capped by [wakeup] dnr_cap in identity.toml (default 20). Use it for corrections and hard rules ("never use X here", "deployment must always Y"). When the count exceeds the cap, an overflow line points at /ndx-chore to consolidate. ndx recall status shows the active rule count.
/ndx-recall-dedupe # merge near-duplicates after large mines
/ndx-recall-contradict # flag stale vs current claims
/ndx-recall-summarize # generate per-room summaries
/ndx-recall-handover # Claude reflects on what it learned, saves as memories
This closes the loop: each session leaves the next one smarter. Mining, classification, and scoring compound — the palace gets more useful the more you use it.
ndx list and ndx find walk the project tree using the ignore crate, respecting .gitignore. No daemon, no on-disk index, no cache. For content search, use ripgrep / Claude's Grep tool — they're faster and more capable than any cache ndx could maintain, and removing that surface keeps ndx focused on memory.
ndx list # list project files
ndx list --path src/ --pattern "*.rs" # filter by prefix and glob
ndx list --sort modified # sort by modification time
ndx list --tokens # append rough token-cost column (size / per-extension ratio)
ndx list --json # JSON: path, size, modified, tokens — always includes tokens
ndx find "**/*.toml" # find files matching glob
ndx find "src/**/*.rs" --sort modified
ndx find "**/*.rs" --tokens # token cost per matched file
ndx find "**/*" --json # structured output for scripts and Claude
ndx status # memory statisticsToken estimates are size_bytes / ratio_for_extension, deliberately rough — useful for "which of these files is cheapest to read", not exact tokenization. Code (.rs, .py, .go, …) tokenizes at ~3.0; prose (.md, .txt) at ~3.8; whitespace-heavy (.json, .yaml, .toml) at ~4.5; default 3.5.
Direct access to the global memory database — no daemon needed.
ndx memory search "database migration" # search session transcripts
ndx memory events "docker" # search command event log
ndx memory list # recent sessions
ndx memory list --project /path/to/project # filter by project
ndx memory stats # session/event/agent counts
ndx memory session <session-id> # full session details
ndx memory context # recent project context
ndx memory subagents "search query" # search subagent transcripts
ndx memory tree <session-id> # session + subagent treeAll memory commands accept --limit N.
Per-project structured memory ({project}/.ndx/recall.redb). Drawers are atomic memory units grouped into rooms, retrievable via a 4-layer ladder plus hybrid semantic + lexical search. Direct access, no daemon.
ndx recall init # create the palace
ndx recall status [--json] # counts, schema, embedding model, last mine
ndx recall reembed [--force] # backfill embeddings (downloads model if needed)ndx recall mine --from-memory [--since 2026-01-01] # derive from global session memory
ndx recall mine --from-chroma <path> # import from a mempalace ChromaDB
ndx recall mine --project [--path <dir>] # walk the project, paragraph-chunk text filesAll mine modes are idempotent via BLAKE3 content-hash dedup; re-running yields added: 0, deduped: N.
ndx recall wake [--force] # L0 identity + L1 top drawers → stdout (wake-up text)
ndx recall get --room <name> [--limit N] # L2 metadata retrieval
ndx recall search "query" [flags] # L3 hybrid (default), --semantic, --lexical
ndx recall search "query" --room decisions --limit 5L3 defaults to hybrid search: fastembed cosine similarity (top-50) fused with Okapi BM25 (k1 = 1.2, b = 0.75, top-50) via Reciprocal Rank Fusion (k=60). Semantic catches synonyms; lexical catches exact identifiers. Neither alone is sufficient.
ndx recall drawer list [--room X] [--limit N] [--pending <op>] [--json]
ndx recall drawer show --id N [--json]
ndx recall drawer add "text" [--room X] [--importance N] [--source-file F]
ndx recall drawer update --id N [--room X] [--importance N] [--text "..."]
ndx recall drawer rm --id N # full cascade across all indexes
ndx recall drawer link --from A --to B --kind <references|contradicts|supersedes|derived_from>
ndx recall drawer unlink --from A --to B [--kind <kind>]ndx recall room add <name> [--title T] [--description D]
ndx recall room list | show <name> | rename <old> <new> | rm <name>
ndx recall identity show [--merged] # render merged global + per-project identity.toml
ndx recall identity edit [--project] # $EDITOR on the identity file (creates template)The palace stores everything raw. Quality is curated via five slash commands that delegate judgment to Claude Code and round-trip through ndx recall drawer update|link|rm --json:
| Command | Purpose |
|---|---|
/ndx-recall-classify |
assign rooms to unclassified drawers |
/ndx-recall-score |
set meaningful importance on default-5 drawers |
/ndx-recall-dedupe |
merge near-duplicates (cluster by content-hash prefix) |
/ndx-recall-contradict |
flag contradictions and link via LinkKind::Contradicts |
/ndx-recall-summarize |
generate per-room summary drawers in the reserved _summary_ room |
Each skill fetches a batch via ndx recall drawer list --pending <op> --limit N --json, decides what to do, and writes back with individual update commands.
ndx xref file src/main.rs # find sessions that touched this file
ndx xref session <session-id> # list files touched by a session
ndx xref drawer src/auth.rs # find palace drawers referencing a file
ndx xref drawer-session <session-id> # drawers derived from a session
ndx xref git <commit> # drawers referencing files changed in a commit (cached)ndx project show # current project resolved from cwd's project.toml
ndx project list [--json] # distinct project tags + drawer counts
ndx project rename <old> <new> # rewrite every drawer's tag in one txn
ndx project move-drawer <id> --to <name> # one-off retag for a single drawer
ndx project move-drawer <id> --global # promote a drawer to cross-project visibility
ndx project rm <name> --yes # cascade-delete every drawer in a projectndx serve exposes the global palace over HTTP for remote agents — build bots, dashboards, browse-only operators that need palace access without local filesystem access. Authentication is bearer-token; scopes are r (read-only) or rw (full).
# Token file at ~/.ndx/tokens.toml (or pass --auth-tokens FILE)
cat > ~/.ndx/tokens.toml <<EOF
[[token]]
value = "your-bearer-token-string"
scope = "rw"
label = "build-bot"
EOF
ndx serve --listen 127.0.0.1:7531 # default
ndx serve --listen 0.0.0.0:7531 # remote-accessible (use a TLS reverse proxy)Endpoints (read; require any valid token):
GET /palace/status— schema, drawer/room/link counts, embedding modelGET /drawers[?room=&project=&limit=&offset=&all_projects=]— list drawersGET /drawers/:id— single drawerGET /drawers/:id/history— audit logGET /search?q=&mode=hybrid|lexical|semantic&room=&project=&limit=GET /wake?project=— L0+DnR+L1 wake-up textGET /issues[?status=open|closed|all&milestone=&project=]GET /projects— distinct project tags + drawer counts
Endpoints (write; require scope = "rw"):
POST /drawers— body{text, room, importance, source_file?, project?, global?}PATCH /drawers/:id— body{room?, importance?, text?}DELETE /drawers/:idPOST /drawers/:id/rollback— body{to_version: N}POST /issues— body{title, body?, milestone?, importance?, source_file?, link_drawers?, project?}PATCH /issues/:id— body{milestone?, importance?}POST /issues/:id/close— body{fix?, commit?, link_drawer?}POST /issues/:id/reopenDELETE /issues/:id
Single-process invariant: redb's file lock prevents concurrent ndx serve or CLI access to the same ~/.ndx/palace.redb. Run one server; route CLI usage through it via curl, or run the CLI on the same host while the server is stopped.
CLI subcommands route to the HTTP gateway (instead of opening the local palace) when a remote endpoint is resolved. Resolution precedence:
--remote URL --token TOKflags on the invocation$NDX_REMOTEand$NDX_TOKENenvironment variables~/.ndx/remote.toml:url = "http://palace.example.com:7531" token = "your-bearer-token"
Output is JSON when remote (the gateway's wire format) and human-formatted when local. Covered: recall status, recall search, recall wake, recall drawer list/show/history/add/update/rm/rollback, issue list/add/close/reopen/rm, project list.
Every drawer mutation (Create / Update / MetadataPatch / ProjectMove / Delete) writes a row to the per-drawer audit log inside the same redb transaction, capturing the pre-image plus the actor + timestamp. Actor is auto-resolved from $NDX_ACTOR → [actor] name in identity.toml → $USER/$LOGNAME/$USERNAME.
ndx recall drawer history --id N # walk versions oldest-first
ndx recall drawer history --id N --json # structured form for scripts
ndx recall drawer rollback --id N --to-version V # restore the pre-image stored at version V
# (rollback itself records a new history entry)A per-project issue log built on top of drawers. Issues live in the reserved _issues_ room; their structured state — status (open/closed), closed_at, milestone — is stored under namespaced issue.* keys in the drawer's metadata map, so tags and other future attributes can be added without schema migrations.
ndx issue add "fix the timeout" --body "happens on staging" --milestone v0.9.0 --importance 8
ndx issue list # default: open
ndx issue list --status closed --milestone v0.9.0 # filter by status and milestone
ndx issue list --status all
ndx issue show <id>
ndx issue update <id> --milestone v0.10.0 # bulk: change milestone or importance
ndx issue close <id> --fix "raised timeout to 30s" --commit abc1234 [--link-drawer N]
ndx issue reopen <id> # status → open; close trailer kept as history
ndx issue rm <id>
ndx issue milestones # group by milestone, show open/closed countsClosing an issue stamps issue.closed_at and appends a structured trailer (**Closed YYYY-MM-DD** — fix. Commit: sha.) to the drawer text — fully searchable via BM25/semantic, and ndx xref git <sha> picks up the commit reference. The optional --link-drawer records a derived_from edge from the issue to a rationale drawer (typically the closing session's most recent), preserving the fix narrative. Drawer machinery (search, links, importance, mining, classify/score) all keep working unchanged on issues.
ndx recall status shows Open issues: N.
ndx scan # scan memory (sessions, events, agents)
ndx install # download manifests, register hooks, install global skills
ndx init [path] [--clean-up] # wire ndx into a project (CLAUDE.md, .gitignore)
# --clean-up removes pre-existing project-local skill copiesndx registers four Claude Code hooks on ndx install: PreToolUse on Bash (manifest lookup + wake-up), PreToolUse on Read (repeated-read detection), PreCompact (re-inject wake-up before context compaction), SessionStart and SessionEnd (auto-mining).
PreToolUse Bash — Phase A — Syntax injection: Before execution, injects CLI syntax hints (key flags, preferred invocations) from YAML manifests into the agent's context.
PreToolUse Bash — Phase B — Output filtering: Pipes output through noise filters (regex patterns) and truncation (max lines), reducing context window usage.
PreToolUse Bash — Phase C — Event logging: Records every command directly to the memory database for cross-session search.
PreToolUse Read — Repeated-read detection: Captures the file's mtime at hook time and counts past Read events for the same (session, path, mtime). When the upcoming Read would be the third of identical content (no Edit or Write bumped mtime between reads), emits an additionalContext line: ndx: this session has read <path> N times — work from existing context instead of re-reading. An external or Claude-driven edit changes mtime and resets the count automatically.
ndx uses the same YAML manifest format as kcp-commands. Manifests are resolved with a three-tier lookup:
.kcp/commands/<key>.yaml— project-local (check into repo)~/.kcp/commands/<key>.yaml— user-level customizations~/.ndx/commands/<key>.yaml— bundled (downloaded byndx install)
- The recall palace (
{project}/.ndx/recall.redb) holds drawers, BM25 postings, embeddings, and links in redb. Direct access from the CLI; no daemon mediates. - The global memory database (
~/.ndx/memory.redb) indexes Claude Code session transcripts from~/.claude/projects/.ndx scaningests new transcripts; the SessionEnd hook does the same incrementally. ndx listandndx findwalk the project tree on each invocation via theignorecrate (gitignore-aware). No on-disk file index. For content search, use ripgrep / Claude'sGreptool.- The hook subcommand resolves manifests and responds in <20ms.
| Database | Location | Contents |
|---|---|---|
| Recall palace | {project}/.ndx/recall.redb |
Drawers, rooms, links, embeddings, BM25 index, xrefs, wake state |
| Per-project identity | {project}/.ndx/identity.toml |
Optional per-project identity override (TOML) |
| Global memory | ~/.ndx/memory.redb |
Sessions, events, agents, cross-references |
| Global identity | ~/.ndx/identity.toml |
Base identity file (TOML), merged with project override |
| Embedding model | ~/.ndx/models/ |
Cached all-MiniLM-L6-v2 ONNX model (~90 MiB, downloaded on first use) |
| Manifests | ~/.ndx/commands/*.yaml |
Command syntax and filter definitions |
MIT. See LICENSE.
ndx's episodic memory, command manifest, and recall palace features are inspired by and compatible with:
-
kcp-commands — Command syntax injection and output filtering for AI coding agents. Created by Cantara. ndx uses the same YAML manifest format and downloads the same bundled manifests (289 commands covering git, docker, kubectl, cloud CLIs, build tools, and more).
-
kcp-memory — Episodic memory daemon for AI coding sessions. Created by Cantara. ndx implements equivalent session transcript parsing and CLI interfaces in Rust.
-
Knowledge Context Protocol — The KCP specification that defines the manifest format and integration patterns.
-
mempalace — The structured memory palace concept (wings, rooms, drawers, 4-layer retrieval ladder, raw verbatim storage) that inspired ndx's
recallsubsystem. Created by milla-jovovich & Ben Sigman. ndx re-implements the useful ideas in Rust on top ofredbandfastembed, deliberately omitting mempalace's AAAK compression layer and MCP server. The 4-layer retrieval ladder, the importance-weighted taxonomy, and theall-MiniLM-L6-v2embedding choice (for benchmark parity) come directly from mempalace.
kcp-commands and kcp-memory are licensed under Apache 2.0. mempalace is licensed under MIT. ndx is an independent Rust implementation of the same concepts and protocols. The YAML manifest files downloaded by ndx install are redistributed from kcp-commands under their original Apache 2.0 license.