A local, automatic memory system for Cursor, Claude Code, and OpenCode. It captures your chats, edits, and shell activity, distills them into durable markdown "memory bank" files, and injects a bounded context pack into every new session, so the agent remembers your project's decisions, stakeholders, conventions, and open action items.
All three tools share one store per project: something learned in an OpenCode session shows up in the next Cursor or Claude Code session on the same project.
Works for coding and non-coding workspaces (product/eng/people management). Everything runs locally; nothing is uploaded anywhere.
- Three tools, one memory.
./install.shfinds Cursor, Claude Code, and OpenCode and wires each one it finds. Every tool's events go through onememory hookentry point, so capture, learning, and recall work the same everywhere. See Supported tools. - One home. The engine and your data live in
~/.silly-memory(override withSILLY_MEMORY_HOME). Each project is marked by.silly-memory/memory-id. - Memory tools for the agent. Skills let every tool search memory, list
action items, and store a fact by running the
memorycommand, with no MCP server to approve../install.sh --mcpadds a local MCP server with the same three operations (memory_recall,memory_tasks,memory_add). See Memory tools. - You control what is kept. Text inside
<private>…</private>is never stored. A prompt that starts with "remember that …" is stored as a fact at full confidence. - Continuity. The context pack has a "Recent sessions" section, a
compaction keeps a handoff note so Claude Code and OpenCode pick up where they
left off, and
memrecall --allsearches every project. - A learning loop. Corrections, reinforcement, and contradictions adjust confidence scores; recall is hybrid FTS5 + embeddings; old entries decay, and you approve what gets pruned.
./install.sh wires every tool it finds: a tool counts as found when its config
folder exists (~/.cursor, ~/.claude, ~/.config/opencode) or its command
(cursor, claude, opencode) is on PATH. Pick them yourself with
--tools:
./install.sh --tools claude-code # only Claude Code
./install.sh --tools cursor,opencode # any subset of cursor, claude-code, opencodeThe wired tools are recorded in ~/.silly-memory/config.json ("tools"); the
doctor, the rule renderer, and the uninstaller read that list. Every file below
is recorded in ~/.silly-memory/.installed-artifacts.json, so a reinstall
updates only what it owns and the uninstaller removes only what it installed.
Shared settings files (~/.cursor/hooks.json, ~/.claude/settings.json) keep
every entry that is not silly-memory's. If one of them is not valid JSON, the
installer stops before changing anything.
The MCP server is off unless you install with --mcp, and it is never
registered globally (~/.claude.json and ~/.cursor/mcp.json are not touched).
With --mcp, each project gets it in its own config at its first session; see
Memory tools.
After installing, check the wiring with memdoctor: its tools line reads
<tool> wired for each tool, and its mcp line reads off or, with --mcp,
counts the projects whose config carries the server.
What gets installed:
~/.cursor/hooks/memory-hook.sh, plus its entries for all eight Cursor hook events in~/.cursor/hooks.json~/.cursor/rules/memory-recall.mdc(the deep-recall rule) and the memory skills in~/.cursor/skills/
Each project gets a generated rule, .cursor/rules/_memory-context.mdc, that
carries its context pack. Reload: restart Cursor. With --mcp, each project
also gets .cursor/mcp.json with the silly-memory server: Cursor asks once
per project to approve it (or run cursor-agent mcp enable silly-memory in the
project), and the server binds to the project through the folder Cursor reports
as its workspace root.
What gets installed:
~/.claude/hooks/silly-memory-hook.sh, plus handlers forSessionStart,UserPromptSubmit,PostToolUse(edits andBash),Stop,PreCompact, andSessionEndunder thehookskey of~/.claude/settings.json. No other key of that file is touched.~/.claude/rules/memory-recall.mdand the memory skills in~/.claude/skills/
Each project gets .claude/rules/_memory-context.md with its context pack, and
SessionStart adds a short note to the session. With --mcp, the project also
gets .mcp.json with the silly-memory server, approved in the project's
.claude/settings.local.json (enabledMcpjsonServers) so Claude Code does not
ask; the server binds to the project through CLAUDE_PROJECT_DIR. The hooks run
synchronously (10 s timeout, 30 s for SessionEnd), so the last turn of a
session is captured before Claude Code exits. Reload: start a new Claude Code
session.
OpenCode v2 is supported (built and tested against v2.0.18); OpenCode v1 is not.
What gets installed:
- the plugin
~/.config/opencode/plugins/silly-memory.js(one file, no dependencies; honorsXDG_CONFIG_HOME) - the memory skills in
~/.config/opencode/skills/
The plugin sends OpenCode's session events to memory hook --tool opencode and
injects the project's context pack and the recall rule into the session context
(OpenCode has no project rule file). With --mcp, each project gets
opencode.json with the silly-memory server (an existing opencode.json
keeps its other settings; a project configured through opencode.jsonc is left
alone). Reload: OpenCode reloads plugins on its own; if it does not, run
opencode service restart. The plugin is a local file, so opencode plugin list does not show it. To see it working, run a session with --print-logs:
the log shows loading plugin for silly-memory.js and, with --mcp from the
project's second session on, mcp connected server=silly-memory tools=3.
Install from a git clone or an extracted release tarball:
git clone <repo-url> silly-memory # or: tar -xzf silly-memory.tar.gz
cd silly-memory
./install.shThe installer prints each of its 11 steps with timings, then lists the tools it wired and how to reload each one.
One-liner bootstrap on a fresh machine:
./bootstrap.sh # default offline; --from must be a local path
./bootstrap.sh --allow-network # opt in to network clone
./bootstrap.sh --from <url-or-path> --branch <ref> --target <dir>The default installer is fully offline. MEMORY_ALLOW_NETWORK=0 (the default)
blocks all outbound socket creation during install. The repo ships preseeded
SHA-pinned all-MiniLM-L6-v2 weights (~91 MB) at
memory/weights/all-MiniLM-L6-v2/. Install copies them to
~/.silly-memory/_embeddings/ and verify_weights checks every SHA-256 on
every backend load, so the embedding model is never downloaded over the
network.
Shipped weights remove the model download, not the embedding libraries.
Dense semantic recall additionally requires one of the optional backends to be
importable at runtime — torch + sentence_transformers, or fastembed. If
neither is installed, the embedding backend resolves to noop and recall runs
FTS5-only (still fully functional, just lexical rather than semantic). Note a
cosmetic quirk: a fresh offline install may log noop during backend
selection because that step runs before the weights are copied into the cache;
at runtime, with the weights cached and a backend lib present, recall uses
sentence-transformers/fastembed.
./install.sh # offline; copies shipped weights, no model download
MEMORY_SKIP_MODEL_DOWNLOAD=1 ./install.sh # hermetic CI/test: skip the weight copy entirelyTo refresh the shipped weights, see
memory/weights/all-MiniLM-L6-v2/README.md.
If you'd rather refetch weights at install time, opt in explicitly:
MEMORY_ALLOW_NETWORK=1 ./install.shOnly MEMORY_ALLOW_NETWORK=1 unblocks the weight download path. Any other
value (including unset) preserves the offline default.
./install.sh --help
./install.sh --tools claude-code,opencode # wire only these tools
./install.sh --mcp # also give each project the MCP server (off by default)
SILLY_MEMORY_HOME=/path/to/home ./install.sh # install into another homeinstall.sh has no dry-run mode and refuses unknown options and unknown tool
names (exit 2, before anything is written). To preview an install, run it
against a throwaway home: HOME=$(mktemp -d) ./install.sh.
upgrade.sh --dry-run and uninstall.sh --dry-run do exist.
With a custom SILLY_MEMORY_HOME, ~/.zshrc exports it for your shell. The hook shims find the engine through
SILLY_MEMORY_HOME, so an app you start outside that shell needs the variable
set too.
Choose the recall backend at install time:
MEMORY_EMBEDDING_BACKEND=sentence-transformers— default, highest quality.MEMORY_EMBEDDING_BACKEND=fastembed— lighter ONNX runtime fallback.MEMORY_EMBEDDING_BACKEND=noop— disable embeddings; FTS5-only recall../install.sh --no-torch— skip thesentence-transformers/torchbackend (usefastembedornoop).
Install-time env vars:
MEMORY_ALLOW_NETWORK=0(default) — block all network access during install.MEMORY_ALLOW_NETWORK=1— allow Hugging Face weight download.MEMORY_SKIP_MODEL_DOWNLOAD=1— first-class supported flag: skip embedding weight download entirely. Embeddings fall back to the noop backend untilMEMORY_ALLOW_NETWORK=1is set or weights are preseeded intomemory/weights/all-MiniLM-L6-v2/. Use this on machines that will never have network access or that are happy with FTS5-only recall.
- Reload each wired tool (see Supported tools).
source ~/.zshrc(or open a new terminal).- Run
memhelpto see the commands, andmemdoctorto check the wiring.
The installer is safe to re-run for updates: it refreshes the engine but never
deletes your memory stores and keeps your config.json. A recall rule or skill
it installed is updated only if you have not changed it. A rule or skill you
edited, or a file of the same name that was never silly-memory's, is left alone
and reported. Hook entries are merged; other entries in the same files stay.
The agent has three memory operations: search memory, list action items, and
store a fact. Two skills, installed into every wired tool, give it all three
with no MCP server: query-memory and add-memory run the memory command.
| Operation | What the skill runs | MCP tool (with --mcp) |
|---|---|---|
| Search | memory recall "<keywords>" (this project plus global memory); add --all for every project |
memory_recall (scope: workspace or all) |
| Action items | memory tasks, with --status, --tag, --owner, --all |
memory_tasks |
| Store a fact | memory add "<fact>", or memory add - to read it from stdin; --scope auto|workspace|global |
memory_add |
The command and the tool do the same work; memory add and memory_add share
one code path (secret redaction, <private> removal, routing by category, the
duplicate check, score 1.0, and the index and rule refresh). Every command finds
the project from the folder it runs in, so a subfolder uses the repository's
store. The recall rule tells the agent to use the MCP tools when they are
connected and the commands otherwise. You can run the same commands yourself:
memrecall, memtasks, and memadd.
Skills are the default for two reasons. A skill costs only its short description until the agent uses it, while an MCP server sends all its tool schemas with every request. And some teams may not use an MCP server until it is approved, while a skill is instructions plus a local command. A skill's commands still go through the tool's normal command approval.
./install.sh --mcp # turn it on; reruns and upgrades keep the setting
./install.sh --no-mcp # turn it off and remove it from every tracked projectThe setting is "mcp" in ~/.silly-memory/config.json. With it on, each tool
connects to a local MCP server, ~/.silly-memory/bin/memory-mcp, over stdio (no
port, no network). It offers exactly three tools:
| Tool | Arguments | What it does |
|---|---|---|
memory_recall |
query, limit (default 10, up to 50), scope: workspace (default) or all |
Searches memory. workspace covers this project plus global memory; all searches every project's store. |
memory_tasks |
status: open (default) or all, tag, owner, all (every project) |
Lists action items. |
memory_add |
text, scope: auto (default), workspace, or global |
Stores one fact at full confidence and refreshes the project's rules right away. |
Tool output is capped at 4,000 characters.
Registration is per project; nothing is global. At a project's first
session, the session-start hook adds the silly-memory server to that project's
own config for each wired tool:
| Tool | Project file | Approval |
|---|---|---|
| Claude Code | .mcp.json |
pre-approved in .claude/settings.local.json (enabledMcpjsonServers) |
| Cursor | .cursor/mcp.json |
Cursor asks once per project (or cursor-agent mcp enable silly-memory) |
| OpenCode | opencode.json |
none needed |
The tools read these files when a session starts, so the memory tools are
available from a project's second session on. The entries are meant to be
committed: they hold no machine path. Each one runs
sh -c 'h="$SILLY_MEMORY_HOME"; …; exec python3 "$h/bin/memory-mcp" --client <tool>',
which finds the memory home when the server starts (SILLY_MEMORY_HOME, else
~/.silly-memory), so a teammate without silly-memory
just sees a server that does not start. Other settings and servers in these
files stay. A server named silly-memory that does not run memory-mcp is left
alone, and so is a file that is not valid JSON. .claude/settings.local.json is
listed in the generated .claude/.gitignore, because approvals are personal.
The server works out the project the same way for every tool: Cursor reports its
workspace root, Claude Code sets CLAUDE_PROJECT_DIR, and OpenCode starts the
server in the folder it was launched from. From there it finds the repository
root (the nearest .git, else the nearest .silly-memory/memory-id), so
starting a tool in repo/packages/api still uses repo's store. A linked Git
worktree keeps its own store.
To run the server by hand: python3 ~/.silly-memory/bin/memory-mcp --workspace <project>.
- Capture. Hooks (Cursor, Claude Code) and the plugin (OpenCode) record prompts, replies, file edits, and shell commands as events in the project's store, with the tool that produced them. Session end and compaction process the session and refresh every wired tool's project rule, so the next session, in any tool, starts with the updated pack.
- Private text. Anything between
<private>and</private>(case insensitive; an unclosed<private>runs to the end of the text) is removed before anything is stored or logged, ahead of the secret filters. - "remember that …". A prompt that starts with
remember thatis stored as an explicit fact with full confidence (score 1.0, tagexplicit) when the session's memory is next processed, even in a short session. The same fact is stored once, however often it is repeated.remember that time …or the phrase in the middle of a sentence is ignored. Usememadd(or the add-memory skill, ormemory_addwith--mcp) when the fact must be recallable immediately. - Recent sessions. The context pack has a "Recent sessions" section: the five newest sessions with date, tool, duration, the first prompt, and edit and shell counts. The last 200 sessions per project are kept.
- Compaction handoff. Before Claude Code or OpenCode compacts a session, the work state, open tasks, and the last five prompts of that session are written to a handoff note. After the compaction, that same session gets the note back (at most 9,000 characters) and the note is deleted. A note older than 24 hours is never restored. Cursor does not use handoff notes.
- Recall everywhere.
memrecall foosearches this project plus global memory;memrecall --all foosearches every project's store, with each hit labeled by project.--allalways searches every store; it never stops early to save time.
upgrade.sh upgrades the installation in place behind a verified, numbered
backup. If any step fails after the backup, it rolls back automatically: the
home and every tool setting the installer changed return to how they were.
./upgrade.sh # upgrade in placeWhich home is upgraded: SILLY_MEMORY_HOME if set, otherwise ~/.silly-memory.
For a custom home, run SILLY_MEMORY_HOME=/path ./upgrade.sh, and pass the same
variable to roll back.
The upgrade takes the lock, backs up the home and checks the copy's digest, runs
the installer over the home, runs memdoctor, and then writes the upgrade
marker.
./upgrade.sh --dry-run # print the steps in order, write nothing
./upgrade.sh --from-version VERSION # override installed-version detectionThe dry run takes no lock and creates no home, backup, or journal.
./upgrade.sh --rollback --confirm # restore the latest backup
./upgrade.sh --rollback --target N --confirm # restore backup number NRollback restores the home from a copy of the backup (the backup itself stays
usable), moves the home it replaces aside to <home>.discard-<time>, and puts
back each tool file and setting the upgrade changed: hook shims, recall rules,
skills, the OpenCode plugin, the zsh block, and the hook entries. Tool entries
the upgrade added are removed. Settings that are not silly-memory's are never
touched. If you edited one of silly-memory's own entries after the upgrade,
rollback stops before changing anything and names the file. A backup without a
journal is never restored.
Backups sit next to the home they copy: <home>.upgrade-backup-N-<time>/, for
example ~/.silly-memory.upgrade-backup-1-<time>/. The five newest are kept.
Each backup holds a .upgrade-transaction/ folder with the upgrade's journal;
it is never copied back into a restored home. memstatus prints the most recent
backup path under the post-upgrade banner.
After a successful upgrade, memstatus shows a one-time post-upgrade banner
summarizing the from/to versions and pointing at the latest backup directory.
The banner is suppressed automatically on the next memstatus invocation.
Every upgrade or rollback holds the lock ~/.silly-memory-upgrade.lock (a
second one waits up to SILLY_MEMORY_UPGRADE_LOCK_TIMEOUT seconds, default 30,
then exits 73). The lock is released by the system when the process dies, so a
killed or crashed upgrade never blocks the next one.
If an upgrade or rollback is interrupted (signal, power loss), the next run
finds its journal before doing anything else and finishes the rollback, even if
the home is absent at that moment. ./upgrade.sh then starts a fresh upgrade;
./upgrade.sh --rollback --confirm stops after the rollback. A home that failed
an upgrade is kept as <home>.failed-upgrade-<time>.
memexport bundles your entire memory home (~/.silly-memory) into a
single portable .tgz. It is both your backup and the way you move memory
to a new machine. Nothing leaves your disk unless you copy the bundle
yourself.
upgrade.shvsmemexport— different jobs.upgrade.shis a same-machine, in-place upgrade of the installed engine (see Upgrade).memexport+install.sh --import/memimportis the cross-machine move + backup path for your memory data. They are independent: upgrade in place withupgrade.sh; relocate or snapshot your data withmemexport.
memexport # → ~/silly-memory-export-<date>.tgz
memexport ~/backups/silly-memory.tgz # …or choose the pathThe bundle carries config.json, _global/, every <workspace-id>/ store, and
each project's workspace_root → store-id mapping — enough to restore or
relocate everything. Copy it to the new machine (USB, scp, etc.).
Install the system with the bundle. Any existing store is snapshotted first, automatically — nothing is silently overwritten:
./install.sh --import ~/silly-memory-export-<date>.tgzinstall.sh --import restores the bundle and relinks each project in place,
writing its .silly-memory/memory-id. It supplies --confirm-import YES-IMPORT
internally (safe because the backup is mandatory), so it never blocks on a
prompt.
If a project now lives at a different absolute path (e.g. ~/work/app →
~/dev/app), relink it explicitly so memws and recall find it. Pass the
required --confirm-import YES-IMPORT token yourself; --map-workspace OLD=NEW
is repeatable:
memimport ~/silly-memory-export-<date>.tgz \
--confirm-import YES-IMPORT --relink \
--map-workspace /old/path/app=/new/path/app--confirm-import YES-IMPORT is required to write and is never injected for
you on this direct path (only the install.sh --import wrapper supplies it after
taking the mandatory backup). memimport is the zsh shorthand for
python3 ~/.silly-memory/bin/memory import.
Your local-only profile.md never travels in a shared repo. On the new machine,
fill it in and sync it into the recognized global bank files, then verify:
$EDITOR ~/.silly-memory/_global/memory-bank/profile.md
memprofile-sync
memstatus # or memws — confirm the projects and stores are presentSee docs/private-setup.md for the full private-info
checklist.
uninstall.sh removes only install artifacts. User memory data is always
preserved.
./uninstall.sh --dry-run # list what would be removedThe dry-run prints every path that would be deleted or edited, for every tool, and exits without touching the filesystem.
./uninstall.sh --confirm # actually remove install artifacts--confirm is required; without it uninstall.sh stops after listing what it
would remove.
Before deleting anything, the uninstaller reads every shared settings file it
would edit. If one is not valid JSON (for example ~/.claude/settings.json), it
names the file and exits non-zero without removing anything.
From the memory home (SILLY_MEMORY_HOME, default ~/.silly-memory):
bin/,lib/,tests/,hooks/,cursor-extras/memory.zsh,install-zsh.sh,README.md,VERSION,.installed-artifacts.json,.first-run-seen,.upgraded-from.install.lock,.upgrade.lock
From each tool, only what silly-memory installed and you have not changed:
- Cursor: the hook shim, the recall rule, the skills, and the memory entries in
~/.cursor/hooks.json - Claude Code: the hook shim, the recall rule, the skills, and the memory
handlers in
~/.claude/settings.json - OpenCode: the plugin and the skills
- Every tracked project: the
silly-memoryserver in its.mcp.json,.cursor/mcp.json, andopencode.json, and Claude Code's approval of it in.claude/settings.local.json. A file left with nothing else in it is deleted.
Every workspace directory, _global/, and config.json are preserved, and so
is .silly-memory/memory-id in every project. A rule or skill you edited stays,
as do other hooks, other MCP servers, and an MCP server named silly-memory
that does not run silly-memory. User-data preservation is a hard invariant
enforced by memory/tests/test_uninstall_residue.py.
./uninstall.sh --confirm --remove-zsh-helper # also strip the silly-memory block from ~/.zshrc
./uninstall.sh --confirm --remove-backups # also remove <home>.upgrade-backup-*
./uninstall.sh --confirm --remove-cache # also remove ~/.silly-memory/_embeddings/These flags are additive and never affect user memory data.
After uninstall, the mem* helper commands disappear from new shells (existing
shells keep the cached functions until restart). Workspace memory directories
remain readable for manual inspection or migration; re-running ./install.sh
reattaches them without data loss.
memhelp # detailed description of every command
memstatus # dashboard of memory for the current workspace
memlearn-status # learning loop metrics (corrections, reinforcements, contradictions)
memtasks # open action items as a task list
memws # list all workspaces that have memory
memrecall foo # full-text search across this project's and global memory
memrecall --all foo # the same across every project
memadd we deploy from main # store a fact at full confidence right away
meminspect <id> # score, topic, and recency info for one entry
memwhy # explain why each entry in the context pack was included
memdelete <id> # delete one entry (requires --confirm)
memprune-review # interactive review of low-score decay candidates
memprofile-sync # sync your private profile.md into the global bank files
memdoctor # 10 health checks (exit 0 healthy / 1 warn / 2 error)
memtest # run the regression suite (isolated)- A tool does not seem to remember anything. Run
memdoctor. Thetoolsline names each wired tool and when it last started a session; anERRORthere means a hook, handler, or plugin is missing, and rerunning./install.sh(or./install.sh --tools <tool>) repairs it. - The memory tools (MCP) are missing in a tool. The server is off unless
you installed with
--mcp; without it the agent uses the add-memory and query-memory skills, andmemdoctor'smcpline readsoff. With--mcp, the tools load from a project's second session: the first one writes the project's config. Check that the project's.mcp.json,.cursor/mcp.json, oropencode.jsonhas thesilly-memoryserver (memdoctor'smcpline counts them). In Cursor, approve the server when asked, or runcursor-agent mcp enable silly-memory. In OpenCode, a--print-logssession logsmcp connected server=silly-memory. - A custom home is not used by a tool. The hook shims read
SILLY_MEMORY_HOME; start the tool from a shell that exports it. - The installer or uninstaller stops on a settings file. It names the file that is not valid JSON; fix or move it and rerun. Nothing was changed.
- macOS/Linux with
python3(stdlib only — no pip installs) bash,rsync- zsh for the
mem*helpers (bash users cansource ~/.silly-memory/memory.zshmanually) - any of Cursor, Claude Code, or OpenCode v2
Memory data lives only under ~/.silly-memory/<workspace-id>/ and
~/.silly-memory/_global/. It is never transmitted. Install and upgrade set
the memory home to 0700 so other local users cannot read it; memdoctor
warns if that drifts. Text inside <private>…</private> never reaches the
store.
The system enforces a zero-network invariant: by default no socket creation is
allowed from any memory-system module. This is asserted by
memory/tests/test_zero_network.py, which monkey-patches socket.socket and
fails the build if any code path attempts to open a connection. The MCP server
speaks only over stdin/stdout; it opens no port.
MEMORY_ALLOW_NETWORK=0 is the default. Under this default every backend,
helper, and CLI path treats network access as a hard error. Hot-path modules
must not import urllib.request, requests, or socket; this is enforced by
memory/tests/test_no_forbidden_imports.py.
Set MEMORY_ALLOW_NETWORK=1 only when you explicitly want network-dependent
operations to succeed (Hugging Face weight refresh, cursor-agent delegation).
The opt-in is process-scoped — it does not persist beyond the invoking shell
and never silently re-enables itself across upgrades.
The cursor-agent LLM backend is gated by the privacy module, not by a
dedicated env var. Specifically,
memory/lib/memory_system/backends/llm/cursor_agent_backend.py calls
privacy.network_allowed() (which reads MEMORY_ALLOW_NETWORK) from both
is_available() and _send_prompt(). Whenever network_allowed() returns
False — i.e., when MEMORY_ALLOW_NETWORK=0, the default — both methods raise
RuntimeError and the backend refuses to launch the cursor-agent CLI.
There is no separate enablement flag; gating is unified under
MEMORY_ALLOW_NETWORK.
memory/tests/test_no_forbidden_imports.py enforces the import bans on every
commit. openai, anthropic, transformers, and langchain are forbidden
anywhere in the source tree. urllib.request, requests, and socket are
forbidden in hot-path modules. The combination guarantees that no future patch
can quietly introduce a network dependency.
The repo ships preseeded SHA-pinned all-MiniLM-L6-v2 weights (~91MB) at
memory/weights/all-MiniLM-L6-v2/. Install copies them to
~/.silly-memory/_embeddings/ and verifies SHA-256 on every load via
weights_manifest.py. The default offline install (MEMORY_ALLOW_NETWORK=0)
downloads no model. Dense semantic recall still needs an embedding library
(torch + sentence_transformers, or fastembed) importable at runtime;
without one the backend is noop and recall is FTS5-only.
MEMORY_ALLOW_NETWORK=1 enables refresh-from-upstream when tampering is
detected. MEMORY_SKIP_MODEL_DOWNLOAD=1 is a hermetic CI/test toggle for
environments where embedding isn't exercised. The shipped weights are
Apache-2.0 licensed; see memory/weights/all-MiniLM-L6-v2/README.md.
Latency budgets are asserted by memory/tests/test_perf_*.py, which CI runs as
its own perf job.
Each measurement takes N = 30 samples after a warmup with fixed seeds, and
records p50, p95, p99, mean, and coefficient of variation (CoV) to the perf
results log:
$MEMORY_PERF_RESULTS if set, else
<system tempdir>/silly-memory-perf/perf-results.jsonl. p95 is the gating
percentile; p99 is informational. A few tests also bound CoV, noted below.
| Measurement | p95 budget | Test file | On overshoot |
|---|---|---|---|
| Hook, warm (in-process), per event — Cursor events and the Claude Code hooks that map to them | 50 ms; 80 ms sessionStart/preCompact; 100 ms sessionEnd |
test_perf_hook.py |
skip, records measured ms |
Hook, cold (fresh python3 memory hook, and hook --tool claude-code) |
same as warm | test_perf_hook.py |
skip, records measured ms |
memstatus warm / cold |
50 / 100 ms | test_perf_cli.py |
skip |
memshow bank warm / cold |
80 / 150 ms | test_perf_cli.py |
skip |
mempaths warm / cold |
40 / 80 ms | test_perf_cli.py |
skip |
memrecall warm / cold |
150 / 250 ms | test_perf_cli.py |
skip |
memdoctor warm / cold |
200 / 500 ms | test_perf_cli.py |
skip |
memory --help cold |
100 ms | test_perf_cli.py |
skip |
| Event append | 50 ms per append, CoV < 20% | test_perf_write.py |
fail |
| Observation write | 50 ms per cycle | test_perf_write.py |
fail |
| Distill 100 observations | 1500 ms per batch | test_perf_write.py |
fail |
| 4 concurrent writers | 100 ms per append, no lost events | test_perf_write.py |
fail |
| Embedding, short / medium text | 100 / 200 ms, CoV < 20% | test_perf_embedding.py |
fail; skipped without an embedding library |
| Search over 50k entries: hybrid / FTS5 / vector | 150 / 100 / 120 ms | test_perf_search.py |
fail; skipped without numpy |
Lock primitive: bash mkdir/rmdir x30 / Python flock x10 |
80 / 10 ms | test_perf_locks.py |
fail |
Each Claude Code hook measurement first checks that its event reached the store, so a hook that fails fast cannot pass as a fast one.
Every cold measurement pays Python interpreter start-up: about 220 ms for macOS system Python 3.9, and considerably more on a heavily loaded machine. That alone exceeds several cold budgets, so the hook and CLI tests record the measured latency and skip instead of failing. The write, lock, search, and embedding budgets fail outright.
silly-memory is licensed under the Apache License 2.0. Copyright 2026 Sillybit (sillybit.io); see NOTICE.
The shipped all-MiniLM-L6-v2 model weights are the Sentence-Transformers
project's, redistributed unmodified under the Apache License 2.0
(memory/weights/all-MiniLM-L6-v2/).
The agent skills in .agents/skills/ and agent/skills/ are Sillybit's
silly-skills and keep the license
stated at the end of each file (CC BY-ND 4.0).