Releases: liza-studio/skillmem
Release list
0.11.0 — the trust boundary, closed on every channel; 40 review rounds
Two independent reviewers (one on the Claude side, one on the GPT side) read the
whole codebase for the first time, then re-read every fix, forty rounds deep,
each round hunting for what the previous round's fixes broke. The first pass
found its P1s not in the recap hook everyone had been staring at but in the
parts nobody had reviewed end to end: the HTTP server, body files, packs.
Trust boundary
skillmem trustand--untrustrefuse to run without a terminal, and
init --claude-codeadds"Bash(skillmem trust*)"topermissions.deny.
Both are safeguards against an agent running the command, not an owner
authentication: a process with write access to the database can still set
the columns. README says which is which.- Unapproved memory is framed by one renderer wherever a body, snippet or
history entry reaches a model — MCPmem_get/mem_search/mem_recall,
HTTP/get//search//recall, CLIrecall(text and JSON), hooks — with
the title (and a history entry's old title) inside the frame; a previous
version is framed whatever the current one's approval — approval belongs to
the words that were approved. Listings (mem_list,/list) carryorigin
and atrustedflag with raw titles.mem_getno longer returns a raw body next
to atrust_warningkey. - HTTP
/writeon an existing slug demands the same permission/update
does: resubmitting a public rule's exact text as private used to reassign
its author and visibility and keep the owner's approval./learnrequires
write_publicfor public skills. A soft-deleted slug is not free for anyone. - CLI
write/learnfrom a process without a TTY recordorigin=agent, not
owner; HTTP writes recordorigin=agentinstead ofunknown; auto-memory
imports default toagent. - Pack import never overwrites a row that is not that pack's own; SKILL.md
symlinks and files outside the pack are ignored;git clone --; aggregate
caps (500 files / 4 MB); a removed pack reinstalls. Vault attachments must
resolve inside the vault.
Data integrity
- Body files written from now on are
<slug>__<hash>[-<db>]+<content32>.md:
namespaced per file-backed database (two databases under one home no longer
share a newly written file) and content-addressed with 128 bits (a new body
is a new file, so publish-before-commit is safe and an outer rollback cannot
leave a row pointing at someone else's text). Pre-0.11 files are not renamed
and stay shared if two databases referenced one. Orphans in a database's own
namespace are collected on the nightlydecayrun and HTTP/decay, under
the database write lock, with a 60 s grace; a non-lock SQLite error there
now propagates instead of reading as "nothing to do". kindis validated on write —[a-z0-9_-][a-z0-9_ -]{0,31}after
lower-casing, trimming and collapsing whitespace — and existing rows are
normalised once on the next open (a write) — case, runs of space, tab,
CR, LF, VT, FF and NBSP; a pre-0.11visibilityis lower-cased and space-trimmed, and one
outsidepublic/shared/privatebecomesprivate, so old rows stay
updatable. Filters are
case-insensitive; a filter nothing can match returns nothing. Export refuses
any path outside its destination (kind="../../x"used to write there).- Export keeps a per-database manifest and prunes the files it wrote last
time. Use one destination per database: two databases exporting the
samekind/sluginto one directory overwrite each other, and the first
0.11 export over a pre-release manifest adopts and prunes that whole list.
Strength, pin and the access/confirmed/failure counters are written to
frontmatter now, and a body is written verbatim, so a dump→restore keeps
exact slugs (a_banda-bno longer merge), LF bodies byte for byte (so
re-importing over an identical approved row keeps its approval — approval
itself never travels in a dump), counters, pins (when the dump records
them) and a recordedorigin—unknownincluded. Importing a plain
Obsidian note keeps what the row earned. - A same-text write through HTTP, MCP or the CLI applies only the metadata the
caller actually sent (library callers such as pack and vault imports keep
their file-is-authoritative behaviour — a migrated file still sets kind and
visibility; same-text provenance changes only when restoration is
explicitly enabled, i.e. a skillmem dump or a file carryingstrength:): a
retriedmem_writewithout akindno longer turns a trusted skill into a
note, a same-textmem_learnno longer flips a private skill public, and an
explicittopics: []really revokes a shared audience (the lexical index
follows). A write onto a soft-deleted slug is refused on every channel (it
used to say OK and stay invisible); a dump restore or a pack reinstall
revives it. An ordinary update keeps the strength the row earned (only a
restore — a skillmem dump, or a file carryingstrength:— sets it);restemindexes full
document bodies;reinforceis one relative UPDATE (concurrent confirmations
no longer lose each other). Known:reinforceis not idempotent — a
retried call counts as new evidence; evidence ids are a later release. - Decay: a fresh skill is measured from its creation, not from "never used";
one decay step per threshold (the threshold is at least one day), so a job
run twice does not compound; the
lifecycle sweep runs even when nothing decays (the CLI used to skip it on
those runs, so skills sitting at the floor were never archived). - History chain:
changed_atis clamped monotonic on every history write
(a clock stepped back no longer reads as tampering). A row stamped in the
future pins later stamps to it until real time catches up — by design; a
warning is logged once when the gap exceeds a day. init_schemano longer writes on every open (hooks stalled behind any writer).
Hooks
- The Stop→
skillmem migratehook is gone: it imported the alphabetically
first project's memory directory on every turn.init --claude-codewith
any hooks mode butnoneremoves an existing one (backup written) and
installs the deny rule;--hooks minimalmeans exactly that and nothing
else. Hand-written memory:skillmem migrate --source <dir>. - Recall context is budgeted per section before framing (a frame can no longer
be cut in half) and the seen-ledger lists exactly what was emitted (any slug
without whitespace or]). - Recap: publication fails closed without its lock; Stop recaps of one
session are serialised by a per-session lock, and the SessionEnd recap waits
up to 5 s for an in-flight Stop recap and then proceeds anyway (publication
is compare-and-swap, so neither can clobber the other); Claude Code's
synthetic string turns stay out of the summary.
CLI / MCP / scheduling
skillmem --db X init ...writesSKILLMEM_DB=X(absolute) into every
agent's MCP entry — Claude Code, Codex, Cursor, Windsurf, Gemini CLI,
opencode. Claude Code, Cursor, Windsurf, Gemini CLI and opencode update an
existing entry's database in place (JSON, rewritten atomically). Codex's
hand-written TOML is never edited in place: an existing entry keeps its
database andinitprints the one line to set by hand
(SKILLMEM_DB = "X"under[mcp_servers.skillmem.env]).uninstall
removes the Codex table only when the result provably equals the old file
minus that table; otherwise (including a file it cannot parse) it leaves
the file alone and says so inwarnings.uninstallalso removes a
skillmem hook from a settings.json group it shares with other hooks. Without--dban existing entry is left alone everywhere.
initanduninstallwrite config files atomically, through a symlink
to its target, with mode kept (JSON and TOML alike).--dbreaches scheduled jobs (re-run
schedule installafter upgrading).uninstall --purge-dbremoves the
DB, its-wal/-shm, its namespaced body files and the legacy-named
files it references (a second database referencing the same legacy file
loses it — split them first).- HTTP
/writeand/learnwithcheck_conflictsreported duplicate
candidates from every agent's records, private ones included — a 409 that
quotes another agent's private title is a read through the trust boundary.
Candidates are now the top BM25 matches among the records the writer may
read (the scan walks the BM25 order past hidden rows until it has scored
its five, so no number of hidden rows crowds out the writer's own
duplicate). MCP is one principal; unchanged. - A same-text write that changes
ttl_daysnow movesfreshness_untilwith
it, and over HTTP an explicitttl_days: nullclears both; before, the new
TTL was stored and the deadline never came. Re-sending the same TTL does
not renew; MCP and the CLI cannot clear a TTL (null is not sent, 0 is
refused) — 0.11.1. /updateandmem_updatemark the rewritten textorigin=agent: an
owner-authored row edited by an agent keptorigin=owner, so the owner
would re-trust words they never wrote (approval was already dropped).- HTTP
/search,/listand/recallused to cut the page before the
visibility filter, so another agent's records could crowd the caller's own
out of the answer (an empty 200 while/getfound the row); the filter
now runs inside the ranking — every match is ranked once and the first
limitrows the caller may read come back, bodies read only for those.
/getlists inlinks_inonly the backlink sources the caller may read —
a private record's slug used to show on the public record it linked to. import-vaultno longer follows a symlinked note out of the vault (the
rule attachments and packs already had):zshrc.md -> ~/.zshrcin a
cloned vault used to land the real file's text in the database.scrubis idempotent: a value already re...
v0.10.5 — a checked-in Docker build, and an honest serverInfo
- A Dockerfile, checked in. Catalogues build a container to score an MCP server, and a server whose inferred build fails is kept out of their search results. This one is explicit and verified on Ubuntu: the image builds at 297MB,
initializeanswers with the real version, andtools/listreturns all 9 tools over stdio. Memory lives in the/datavolume, so a restart keeps it.--build-arg EXTRAS='[semantic]'includes the vector path. - The MCP server reports its own version.
serverInfocarried the SDK's1.30.0, so a registry listing or a client's debug log showed a version this package has never had.
pip install -U skillmem
v0.10.4 — a clean install starts clean
Found while verifying a real pip install skillmem on Ubuntu: a brand-new database reported lexical_reindex_pending: true, which would send the nightly job on a pointless pass and makes skillmem doctor look alarming on a clean install. The flag is now set only when there is something stored to rebuild.
Together with 0.10.3 (the lexical index now keeps two-character tokens — db, py, js, ci — and the rebuild runs from the nightly job or skillmem reindex-lexical, never inside a hook) this closes the lexical-recall defect end to end.
Verified on Ubuntu from PyPI: install, fresh database, learn, recall by full path and by db.py with the embedder disabled, and the tool-recall hook emitting the untrusted frame.
pip install -U skillmem
v0.10.2 — a file path has to match lexically too
If you installed with a plain pip install skillmem (no semantic extra), recall was silently dead for Edit, Write and NotebookEdit. The FTS query was split on whitespace, so /work/analysis.ipynb became a single phrase token that matched nothing — and tool-recall passes the edited file's path as its query. The embedder masked it locally; CI, which installs without the extra, did not.
Queries are now tokenised the way the documents are, and the regression test runs with MEM_SEMANTIC=0 so the semantic layer cannot hide it again.
Also in 0.10.1: the README explains what 0.10.0 changed and how this differs from memory products built for conversational or user memory, with the measured retrieval numbers next to the claim; server.json, plugin.json and .claude-plugin/plugin.json were still pinned at 0.9.2, which would have pointed MCP Registry installers at a version that still contains the recursion fixed in 0.9.3.
pip install -U skillmem
v0.10.0 — provenance, and trust the owner grants
Memory now carries where it came from, and trust is something the owner grants.
The loop this closes was open: an external text — a README, a web page — reaches a transcript, a model distils it into a note, and the note comes back in the next session under a heading that reads like your own rules. A document could also talk an agent into saving a rule through mem_learn, and that rule then looked exactly like one a human wrote.
originon every memory (schema v10):owner,agent,imported,derived,unknown. Writers declare it; nothing guesses.- Trust is explicit.
trusted_atis set only by the owner —skillmem trust <slug>— and editing an approved memory's text drops the approval with it. A CLI write approves itself only from a TTY, because an agent can call the CLI through Bash as easily as a person can type it. - One frame, applied at read time, on every channel:
auto-recall,tool-recall,session-history,mem_recall,mem_get,cat, andinject(approved titles only; the rest reported as a count). Unapproved memory arrives inside a marked block that says it is data, not instructions, with its provenance on the line (origin=derived session=…,origin=imported pack=…). Written into the note body the frame would not survive — recall collapses newlines, snippets cut the middle, and a summary can carry its own closing backticks. - The summariser runs caged, or not at all:
--tools ""and--strict-mcp-configare mandatory, and a CLI that does not understand them getsskip:unsafe-cliinstead of a run without them. - Migration is additive, inside one transaction, after a copy of the database into
<data>/backups/pre-v10-*.db, with columns re-checked under the lock. 8901 rows in 0.45s here. Rows whose tags cannot be parsed keeporigin=unknown: unreadable provenance is not guessed and not approved. - Grandfathering, stated plainly: existing
ownerandagentrows are approved by the migration (trusted_by='migration-v10') — the alternative is that every rule you have relied on for months arrives unapproved the morning after an upgrade. Imported packs and transcript summaries are not: 7809 summaries stayed unapproved on the machine this was built on. export/importkeep provenance. A file may lower its own origin but never claimowner, and trust is never importable.
What this does not do: a frame makes the boundary legible; it does not guarantee a model ignores an instruction sitting inside data. The guarantee comes from the reader having no tools — which is why the summariser has none.
Reviewed by GPT-6 over four passes (it found the inject hole, the json_each failure that turned a pack into a trusted rule, and the unatomic migration) with each claim verified against a live install. Every fix has a test that is red without it: 212 passing.
pip install -U skillmem
v0.9.9 — compare-and-swap the recap, exclude kinds in the query
A third review pass reproduced two defects in code shipped this week.
- Publishing a recap is compare-and-swap now. The freshness check and the file replacement were two steps: a Stop that passed the check before SessionEnd wrote the final recap would replace it afterwards. The basis is re-read inside a short publish lock — milliseconds, not the model call — so the late writer sees the final note and stands down.
- Excluding session recaps from
searchhappened after the candidate pool, and that pool is capped at 50 per signal: a wall of recaps filled it and the search returned nothing while a matching skill sat just below (--kind skillfound it). Kinds are excluded inside the ranking query now —exclude_kindsonsearchandhybrid_rank_ids.
Both were reproduced before the fix, and each test is red without it.
pip install -U skillmem
v0.9.7 — a trust boundary, and a way to see what the hooks did
- Memory arrives with a trust boundary. Anything this machine did not author — an imported pack, or a note distilled from a transcript that may itself quote a web page — is injected under its own header saying plainly that it is data, not instructions. A stored instruction is still an instruction, and it used to land in the same block as the user's own rules.
skillmem searchhides session recaps by default (--notesbrings them back). They accumulate one per session and reached 90% of the words in one database, so an unfiltered search returned the diary instead of the rules.skillmem recap [TRANSCRIPT] [--force]writes a recap on demand — the Stop hook is rate-limited, and this is how you save a session's closing minutes without waiting. With no argument it picks the current project's newest transcript.skillmem hooks-statusshows what the hooks have actually been doing: runs, skips, failures, the last line per hook, and the state directory. Hooks swallow their own errors so they can never break a session — which also means one that silently stopped working looks exactly like one with nothing to do.- A manual recap no longer inherits SessionEnd's budget. Its first live run timed out at 45s: only the real SessionEnd event lives inside that 60s ceiling. A manual run gets the full timeout, and the SessionEnd one trims its input to 20KB, because a truncated recap beats one that times out.
pip install -U skillmem
v0.9.5 — four silent defects from the review
An outside review of the hook path (GPT-6 reading the code, a second model verifying each claim against the live install) turned up four defects. Every one of them failed quietly.
- Notebook edits get recall again.
tool-recallreadfile_path, but Claude Code sendsnotebook_pathfor NotebookEdit — the query came out empty and the hook returned nothing at all for every notebook edit. - Session notes are linked to their session again. The recap writes
metadata.source_session; the importer only kneworiginSessionId/sessionId, so imported session notes landed with a null session — 7793 of 7793 on the machine where this surfaced. - The MCP guard's count told the truth. It reported
len(actual)oflen(expected), so a config with extra servers could claim "12 of 10 expected connected" in the same breath as listing one as missing. - The per-session recall ledger moved to the private state dir. On Linux without
TMPDIRit sat in a world-writable/tmp, where a neighbour could pre-create the file and mute someone's recall; stale ledgers are now pruned after seven days instead of accumulating one file per session forever.
Every fix has a test that was checked by reverting the fix.
pip install -U skillmem
v0.9.4 — the recap costs what it should
0.9.3 stopped the Stop hook recursing. This release stops it being expensive, and closes what a full review of the recap path turned up.
Stop fires after every assistant turn, not when the session closes — so the recap ran per turn: a model call each time, and a near-identical note every few minutes.
- At most one call per session per
SKILLMEM_RECAP_MIN_INTERVAL(default 600s), and one note per session per day that a later recap rewrites. - The limit counts attempts, not successes — a failing model left no trace, so every following turn bought another call.
- A non-zero exit is no longer stored as memory. Any output over 100 characters (a usage-limit message, a stack trace) became a note and overwrote a good recap.
- SessionEnd gets its own recap, not rate-limited, so a session's closing turns still reach memory.
skillmem initwires it; existing installs pick it up by re-running init. - The parallel-slot semaphore is back (
SKILLMEM_RECAP_MAX_PARALLEL, default 2), lost in the rename. O_EXCL locks reclaimed by age, so SIGKILL cannot wedge a slot. - The child runs lean:
--strict-mcp-config(it booted every MCP server per call) and--no-session-persistence(it left a transcript on disk per call — a gigabyte on one machine). Dropped automatically on a CLI too old to know them. - Notes are written atomically; a crash mid-write left half a note behind.
- A typo in
SKILLMEM_RECAP_TIMEOUTno longer breaks every command —int()ran at import time. - Tests stopped writing into the developer's live state directory, where a stamp from one test silently debounced the next.
pip install -U skillmem
v0.9.3 — the recap no longer recaps itself
session-recap shells out to claude -p, and that child is a Claude Code session like any other: its own Stop hook fired the same recap, whose child fired it again. On one machine the chain went from 6 recaps a day to 4083 — 3901 ghost sessions, a gigabyte of transcripts and a burned subscription quota, while the garbage summaries fed themselves back into the next session's context.
The child now inherits SKILLMEM_NO_RECAP=1, the way it did before the rename. The old test only proved the flag works when set from outside, so the regression shipped green; the new one asserts the flag reaches the child.
Anyone on 0.9.0–0.9.2 should upgrade: pip install -U skillmem.
Also in this release (unreleased since 0.9.2): third-party skill pack imports (skillmem skills add), strength earned by outside evidence instead of self-report (schema v9), mem_pin for rules that must never fade, and init support for Cursor, Windsurf, Gemini CLI and opencode.