elderfo/mehmory
Memory abstraction layer for Claude Code, Codex CLI and Pi: deterministic capture hooks plus model-driven skills over a markdown wiki in ~/.mehmory.
Changelog
All notable changes to this project are documented here.
The format follows Keep a Changelog, and this
project adheres to Semantic Versioning.
VERSION is the canonical version source; package.json, plugin.json, and
.claude-plugin/plugin.json mirror it and tests enforce that they match.
Unreleased
Added
- Pi is a third host.
pi install git:github.com/elderfo/mehmoryinstalls the repository as a Pi package: an extension that runs the same five hook bundles withpias the host, plus the six skills as/skill:<name>. The frame is injected with the first prompt,remember:and Stop capture read Pi's session files, and the Stop nudge arrives as a Pi message.mehmory init --host picreates the store and names the install;hosts.pi.enabledturns it off. Existingconfig.jsonfiles load unchanged and getpienabled. MEHMORY_ACTIVE_HOSTnames the one harness that runs mehmory. Set toclaude-code,codex,pi, ornone, every other harness's hooks skip capture and injection. The Pi extension sets it topiunless you exported a value, so a Claude Code that a Pi provider runs nested no longer captures the same conversation ashost=claude-code. Unset, empty, or unrecognized means no restriction./mehmory:rememberandmehmory inbox-txare not gated.mehmory statscounts suppressed hooks. Each harness line shows invocations skipped byhosts.<host>.enabledorMEHMORY_ACTIVE_HOST, and--jsoncarries them ashosts[].suppressed.
Fixed
-
Status counts only pages retrieval can read. Symlinked pages, symlinked page directories, unreadable files and non-file
.mdentries no longer inflate live or archived page counts. -
Decay keeps archives inside their scope. A destination resolving to exactly the scope's parent is refused by the same canonical containment rule used for wiki reads.
-
Decay no longer follows symlinked index or pages paths. A symlinked
index.mdorpages/is left untouched instead of rewriting or archiving files outside the store. -
Unreadable wiki directories are logged. Prompt matching, search and SessionStart now record informational
E_STORE_READinstead of failing silently. -
Pause gates remain lock-free under contention. A busy session cannot capture a paused
remember:prompt or hide its topic cache behind fresh defaults. -
Capture failures keep their operation error codes. Append and retirement marker errors report
E_APPEND_FAILED, not corrupt session state. -
Failed retirement markers do not claim pending work. SessionEnd keeps the completed capture count and reports
marker_failed, notdeferred, after final-delta handling has landed. -
Evergreen and ephemeral pages are not flagged stale. Retrieval and decay share one staleness rule, so exempt decay classes get no age label or ranking demotion.
-
SessionStart's memory frame fits its budget, framing included. Maintenance notices keep their separate 150-token allowance; claimed warnings are truncated, never dropped, and unselected ones wait for the next start.
-
Small injection budgets degrade in order. Routing stays on every populated session and goes first only when the budget is squeezed, then content; the session id that skills depend on is kept whenever its minimal frame fits.
-
Doctor's injection KPI follows config. It checks the configured frame budget plus the 150-token maintenance allowance instead of a fixed 950.
-
Unreadable store paths fail open. Retrieval, status and SessionStart probes skip them; doctor reports a repair for a
pagespath that is a file and continues its other checks. Read failures log informationalE_STORE_READinstead of an append failure. -
Memory pointers are absolute paths.
relevant:lines name directly readable project or global store files. -
Truncation never splits an emoji. Injection and search snippets keep surrogate pairs whole.
-
Prompt matching and search share scoring and skip symlinks. Golden-set recall is unchanged.
-
PascalCase secret names are redacted.
ClientSecretandSecretKeyno longer leak. -
Unseparated secret suffixes are caught. Names such as
csrftoken,myapikeyandsecretkeyare covered, and URL passwords containing commas or apostrophes are redacted. -
More PEM separators are covered. A private-key footer within 8192 characters confirms a body.
-
Long numeric secret values are redacted. Generic exemptions cap integer portions at 19 digits.
-
Unterminated escaped env quotes no longer leak.
PGPASSWORD=\"hunter2is fully redacted. -
URL passwords stop at quotes and commas when an
@still follows. Neighboring JSON fields and email text stay readable. -
Flattened and stringified private keys are redacted. Space-flattened bodies, literal
\r\n, trailing header whitespace, adjacent bodies, and variable header word spacing no longer bypass filtering. -
Capitalized secret values no longer receive type exemptions. Bare values such as
password=Hunterare redacted; generic exemptions are limited to lowercase keywords, numbers, and self-assignments. -
Ordinary words ending in key/token/secret stay readable. CamelCase matching now requires the actual capitalized suffix, so assignments to
monkey,turkey, andhotkeyare not mistaken for secrets. -
Escaped and ANSI-C quoted inline env values are fully redacted. Backslash-escaped double quotes and
$'...'no longer leave value fragments behind. -
Template-literal secret values are redacted. Backtick-quoted assignments include multiline bodies and escaped backticks.
-
Inline passphrases and PINs are covered.
PASSPHRASE,*_PASSPHRASE, and whole-word*_PINassignments are filtered before commands. -
URL passwords containing
@are fully redacted. Matching reaches the final userinfo separator without quadratic rescans. -
Privacy documentation clarifies assignment limits. Wide unquoted values apply to
=only,)terminates them, and placeholders like${PASSWORD}are redacted. -
JWT matching is bounded on repeated base64url prefixes. Every built-in pattern now has exhaustive 250 KiB adversarial timing coverage.
-
Inline environment secrets are caught after shell and JSON punctuation. Backticks, quotes, substitutions, semicolons, pipes, and parentheses no longer bypass the name boundary; backticks terminate bare values.
-
URL passwords may contain single slashes. Database and HTTP credentials with slash-bearing passwords are redacted without rescanning subsequent URLs.
-
More inline environment names are covered.
*_PASS,*_AUTH, andSECRET_KEY_BASEno longer leak before commands. -
CamelCase secret keys are filtered. JSON and JavaScript names ending in Key, Token, Password, Passwd, or Secret are covered.
-
Oversized memory pages retain useful injection content. Parts are safely bounded before filtering, and a sliced trailing token cannot be pulled into the frame by earlier redaction shrinking the content.
-
Unquoted equals assignments accept arbitrary value characters. Pipes, brackets, Unicode, and embedded equals signs no longer bypass lowercase secret matching.
-
Private-key header mentions preserve following prose. A prose-only header without a body does not consume the rest of the document.
-
Short Azure key examples remain readable.
AccountKey=andSharedAccessKey=require at least 16 value characters. -
Lowercase keywords and self-assignments remain readable. Bare language/type keywords and assignments such as
this.password = passwordare exempt. -
Privacy documentation names additional uncaught shapes. Alternate authorization schemes, command-line passwords, Go assignments, cookies, unsupported provider prefixes, and distill-truncated key prefixes are explicitly disclosed.
-
Secret filtering covers more credential shapes. Database/broker URLs, Anthropic/OpenAI project keys, fine-grained GitHub and numeric-ID Slack tokens, AWS temporary keys, additional private-key formats, and quoted JSON/YAML/env assignments are covered; GitHub token lengths are corrected.
-
Inline environment credentials are filtered before commands. Secret-named variables such as
PGPASSWORD,MYSQL_PWD, and Docker-eassignments no longer escape line-anchored matching. -
Truncated private keys redact through the end of input. Missing footers and differing private-key footer labels no longer leave key material readable after distillation.
-
URL and assignment matching avoid quadratic rescans. Empty-user Redis credentials are covered, and inputs over 256 KiB fail closed before pattern matching.
-
Additional provider credentials are covered. Stripe, Google, JWT, npm, GitLab, SendGrid, Slack webhooks, legacy/service-account/admin OpenAI keys, Azure key assignments, and Basic authorization headers have regression vectors.
-
Bearer redaction includes base64 punctuation and padding. Slash, plus, and equals signs no longer leave part of a matched bearer token behind.
-
Non-secret identifiers and values stay readable. AWS matching is case-sensitive and word-bounded; bare lowercase keywords, numbers, and Slack-prefix prose are not treated as credentials.
-
More assignment delimiters and escaped JSON quotes are recognized. Unquoted secrets before shell/query/code punctuation or tabs and quoted secrets in stringified JSON are covered.
-
Filter failures are observable without logging sensitive text. Informational
E_REDACT_FAILEDreports whole-input redaction, and troubleshooting indexes the partial-config-validation consequence. -
Custom secret patterns cannot replace built-ins. The built-in corpus lives only in the filter; configurable patterns default to an empty additive list.
-
Regex complexity checks count brace quantifiers. Overly complex custom patterns using
{n,m}are skipped rather than silently accepted. -
Unexpected filter errors fail closed. The whole input becomes
[REDACTED]without stopping the harness, and an informational error is logged. -
Invalid config keys no longer discard valid settings or project aliases. Validation defaults only invalid keys, reports their paths once, rejects array roots, and enforces meaningful numeric bounds.
-
Codex TOML scanning distinguishes array continuations from table headers. Root feature keys and table booleans after nested arrays are edited or refused without creating a duplicate table.
-
Codex setup preserves multiline TOML strings. Basic and literal string contents are never edited or read as settings; doctor reports unreadable or unsupported flags as indeterminate warnings instead of unset errors.
-
Codex dotted hooks keys are inserted after complete feature statements. Multiline array values no longer receive an invalid key inside their contents.
-
Codex uninstall leaves foreign-only hook registries untouched. Semantic no-ops preserve any JSON formatting and create no backup, reporting nothing to remove.
-
Codex setup refuses
[[features]]with a specific reason. An array of tables is never combined with a conflicting feature-flag table. -
Codex doctor and backup guidance matches actual behavior. It distinguishes unset flags from indeterminate warnings and explains backups beside symlink targets in dotfiles repositories.
-
Codex configuration symlinks survive setup and removal. Edits and private backups follow the real target, including dotfiles outside
$CODEX_HOME; dangling links are refused. -
Codex lock write failures are reported immediately. Genuine contention identifies the lock path for stale-lock cleanup instead of hiding permission failures behind a five-second wait.
-
Codex skill refusals write no configuration or backups. All shipped sources and destination symlink checks run before any edit.
-
Codex uninstall on a never-installed home is a no-op. It creates neither the home nor a hook registry, including when only skills need removing.
-
Codex configuration edits retain CRLF line endings. Appended TOML lines and rewritten hook registries use the existing newline convention.
-
Codex skill symlink checks have a filesystem-root bound. An unreachable skills boundary cannot cause an infinite walk.
-
Codex uninstall failures describe removal accurately. Write failures no longer imply that an installation was attempted.
-
Codex troubleshooting covers safe-edit refusals and cleanup failures. Operator guidance now includes unsupported TOML, missing bundles, lock contention, staging cleanup, and symlink targets.
-
Codex feature edits preserve valid TOML and comments. Spaced or commented headers, dotted keys, and simple boolean inline tables are edited in place; unsupported shapes fail before any Codex file is written.
-
Codex init creates the memory store. Codex-only users can onboard immediately after setup.
-
Codex uninstall never writes to the memory store. Install and uninstall share a lock under
$CODEX_HOME. -
Codex setup refuses missing hook bundles. It no longer registers commands pointing at nonexistent scripts.
-
Codex setup accepts symlinked homes and ancestors. Symlink checks cover only owned paths inside its skills tree in both directions.
-
Codex setup recognizes hook entries from every supported host. Pi-marked entries are replaced or removed instead of duplicated.
-
Codex configuration backups reflect the last modification. Each
.mehmory.bakcontains the immediately preceding contents, with private permissions, deliberately replacing the pristine pre-mehmory backup so recovery preserves intervening user edits. -
Codex uninstall keeps hooks and skills consistent on failure. Skill staging can be rolled back; deletion failures leave the integration uninstalled and identify leftover staging for cleanup.
-
Unchanged Codex hook entries no longer trigger a re-approval note. Already-approved hooks are mentioned only when their entries changed.
-
Inbox text round-trips exactly. Backslashes, line separators, comment terminators, and surrounding whitespace survive new serialization without reinterpretation or loss.
-
Legacy inbox backslashes have one-way escape semantics. Raw
\\and\rin old entries now decode as a backslash and carriage return; review backslash-heavy facts before integrating. The schema and upgrade guide document the entry-text escapes. -
Atomic writes replace symlinks and flush replacements. A planted destination symlink is replaced, never written through or used to create its target's parent. Writes flush the temporary file before rename and the directory afterward (best-effort), and clean up temporary files on failure.
-
Memory commits use recoverable timeouts. Git receives SIGTERM, with 500 ms for cheap probes and 10 s for other store operations. Timeouts log the manual remedy; real lock contention retries once then defers.
-
Git timeouts never delete another process's index lock. Git cleans up its own lock on SIGTERM; mehmory leaves all index locks untouched and logs the safe manual remedy.
-
Old index locks no longer defer silently forever. Locks older than 30 s trigger a rate-limited warning with the manual remedy, without deleting a possibly live lock.
-
Store history reads disable signature display. Ambient
log.showSignaturecannot trigger signature verification or contaminate the last-commit summary. -
Every store git call is isolated. Init, config, status, and history reads now share the commit runner's stripped repository environment, disabled hooks and fsmonitor, literal pathspec support, and C locale. Project identity retains the caller's repo environment with bounded reads. An unchanged tree remains a successful no-op.
-
Concurrent warnings are not overwritten or cleared accidentally. Immutable records replace read-modify-write storage, drains claim only published records, legacy warnings remain readable, and detail paths honor
MEHMORY_HOME. -
Warning drains recover safely. Corrupt claimed JSON is discarded instead of retried forever, failed claims fall back to read-only delivery, and claims left by crashes are reclaimed after a minute.
-
Append and warning directory failures stay fail-open. An unusable parent directory returns an append failure or skips warning storage instead of throwing into the harness.
-
Capture retries keep their delta. Cursors advance only after a successful inbox append or durable enqueue, and failed captures retain the Stop counter.
-
Swept live sessions resume without SessionStart. Later transcript activity restores the saved cursor and advances the generation without clearing a session pause; unchanged trailing hooks still cannot recreate state or replay old entries.
-
Fresh cursors cannot prove growth. Untouched transcripts stay retired after an idle sweep.
-
Real cursors require new bytes. Mtime bumps and truncation cannot resurrect swept sessions.
-
Skills read retired session origins. A named session's marker supplies project key and host.
-
SessionStart resume is deterministic. Retired pauses clear before injection and maintenance.
-
Pause/resume recovery names the next turn. Errors and skills no longer require SessionStart.
-
Failed Stop captures back off. The nudge fires only on the first crossing, followed by one immediate silent retry and at most one retry per threshold window. Persistent failure is logged once after the immediate retry.
-
Deferred tails retain their origin agent. Session state records the agent so another process's sweep cannot misattribute its entries.
-
Failed inbox appends report failure. Both transactional entry points return stderr and a non-zero exit, including partial failures that can be safely retried.
-
Pause and resume use locked transactions. Skills pass an explicit session id to new inbox-tx operations instead of guessing a state filename and hand-editing it.
-
Skills know their current session. SessionStart injects the id inside the memory frame; all six skills use its hashed state filename, leaving recency only as a legacy project hint.
-
Skill helper fallbacks name the installed bundle. Claude Code uses its plugin root, Codex uses the path registered in hooks.json, and Pi names its git package directory.
-
Finalized session errors explain recovery. Pause/resume report the marker before a missing state file, so an unchanged finalized session is not mistaken for an unknown id.
-
State-file documentation matches disk. Config, troubleshooting, and the session module describe SHA-256 filenames rather than raw session ids.
-
Page purges remove catalog summaries too. Matching normative
- [[slug]] — summarylines are previewed and removed, preventing deleted summaries from remaining in SessionStart's catalog. Freeform references and lines without the summary separator stay. -
Page purges reach archived and agent pages. Live and archived copies are removed together within the selected scope.
-
Purge validates all targets before export or mutation. Refused pages or indices leave catalog lines intact, and every export source is checked for store containment so symlinked
pages/orarchive/directories cannot leak outside-store content. -
Ambiguous agent pages can be selected explicitly.
purge <slug> --agent <name>restricts deletion to that agent, and ambiguity errors name the correct qualifier. -
Scope labels no longer determine deletion identity. A project aliased to
agent/<name>remains distinct from the agent scope with the same label. -
Empty catalog edits no longer clutter purge previews. An index without matching lines is omitted from text and JSON index edits.
-
purge --globalremoves the whole global scope. Its index, inbox, log and archive go too, without rewriting git history. -
Invalid purge session ids are usage errors. Ids shorter than eight characters or containing whitespace are rejected before confirmation with exit 1 and
E_USAGE. -
Partial inbox purge failures report their reach. Deleted and remaining entry counts disclose that no purge commit was made and suggest inspecting the store's git status.
-
Interrupted onboard runs retain progress. Byte-capped and append-failed runs can continue with
--resume; append failures return exit 3 instead of success. -
Onboard rejects zero byte caps.
--max-bytes 0returns a usage error instead of creating a run that cannot progress. Append-failure messages cover busy write locks as well as filesystem problems. -
Unexpected CLI exceptions use
E_INTERNAL. They no longer incorrectly report an append failure. -
Queue claims start their stale window at claim time. Jobs pending for hours are not immediately claimed twice.
-
Stale-lock reclamation is serialized. A reclaim guard protects the owner/file-identity recheck and unlink, including recovery of abandoned guards. Live and permission-protected owners receive retry delays; a five-minute age cap allows recovery from PID reuse but can supersede a longer-running live holder. Lock release retains the shared directory.
-
pnpm testbuilds only when the build is missing or stale. Vitest global setup builds whendist/cli.mjsis absent or older thansrc/, so a fresh checkout works and tests never run against stale code, while CI's explicit build is not repeated. -
Architecture lint rules handle Windows paths and checkout boundaries. All five rules normalize backslashes and checkout-relative paths; a checkout under
test/no longer exempts production code from the filesystem boundary. -
The no-exit rule catches process aliases and assignment destructuring. Static template-literal property keys can no longer bypass the core's exit/abort ban.
-
Module boundary rules check static template literals. Dynamic imports and
requirecalls using expression-free templates now enforce the filesystem and CLI boundaries. -
Suppressed hook calls record their real project. A hook skipped by
hosts.<host>.enabledorMEHMORY_ACTIVE_HOSTwrote its stats line underunknown, so plainmehmory statsin a project never showed it. It now reads stdin to resolve the project, still without capture or injection. -
A git install no longer fails without dev dependencies.
prepareranhusky install, which exited 127 undernpm install --omit=dev(how Pi installs a git package). It now tolerates husky being absent, and a dev checkout still installs the git hooks. -
Purge and inbox transactions stay inside the memory store. Page selectors, inbox paths, and snapshot tokens are validated before destructive or rewriting operations, and purge commits only its requested paths.
-
Interrupted capture jobs are recoverable. Stale queue claims are returned to the pending queue instead of being discarded, while malformed secret settings continue to use built-in redaction.
-
Codex setup and inbox rewrites fail safely. Configuration writes are preflighted and rolled back on failure, new Codex config files use private permissions, and inbox clears never rewrite without their project lock.
-
Remember facts are serialized safely. Fact text no longer gets interpolated into shell JSON.
-
A resumed session is finalized again instead of being written off forever. The finalization marker meant "this id is done", not "the transcript up to here is captured", so once a harness reused a session id on resume every later
SessionEndfor it was a no-op and the entire resumed run was never captured. Seen in the wild: a marker five days older than the same session's live state.SessionStartnow clears its own marker, and the marker carries the cursor so the resumed run reads on from where finalization stopped rather than re-distilling the whole transcript.Clearing the marker alone was not enough: the
log.mdidempotency tag is the other thing keyed by session id, so a resumed run's ending still read as a retry of the first and was skipped. Session state now carries a generation, and both the marker and that tag are keyed by id and generation. Generation 0 keeps the original tag spelling, so existinglog.mdcontent still matches. -
A session in the middle of a long turn is no longer finalized while it is alive. Idle detection read the state file's mtime, which only moves when a hook writes. A session waiting on a slow build or a long tool call fires no hooks, looked abandoned after 30 minutes, and was retired by the next session's start: state deleted, id marked done, everything it recorded afterwards dropped with no error anywhere.
A session is now eligible only once its transcript has gone quiet as well. That is the file which actually grows while a session works -- Claude Code appends as it goes, and a Codex rollout is written incrementally too.
Protect-only by design: a warm transcript defers the finalize to a later start, it never loses one. A rollout flushed after
SessionEnd(#43) waits out the window from its own mtime, and a transcript that has not landed at all still falls back to the state mtime, so it stays eligible rather than waiting forever for a file that is absent. -
Session-state writes are serialized.
updateSessionStatewas an unlocked read-modify-write, and hooks for one session really do overlap -- a Stop alongside a UserPromptSubmit, aSessionEndracing a trailing Stop. The later writer discarded the earlier one's field, which could roll an advanced cursor backwards into a re-distill. A per-session lock now covers read and write together. It gets much tighter retry bounds than the project lock, which busy-waits: this one is taken on every prompt. -
A project key that would escape the store is rejected at every read boundary.
project_keyis joined under<home>/projects/, and now that it is written on every hook it reaches that join routinely. It is re-validated where it is read back -- the session state file and the queued distill payload -- alongsidehostandagent, and anidentity.aliasesvalue is validated too, since an alias is hand-written config that never passed through the remote-key sanitizer. Containment is now a separate check from thehost/owner/reposhape check, so a one-segment alias such asmy-custom-keyremains valid. -
An alias that is not a string no longer takes down every hook for that project.
identity.aliasesis typedRecord<string, string>, butconfig.jsonis user JSON and nothing enforced the value type at runtime. A number reachedString.prototype.splitand threw out ofresolveProjectKey, whichrunHookcatches fail-open -- so the hook produced no capture, no injection and no error the user would see. -
A finalized session's state is no longer resurrected. A hook firing after finalization rebuilt
.state/<id>.jsonfrom scratch, cursor back at 0.finalizeSessionshort-circuits on the marker before it would delete state again, so the file lingered; if the marker aged out ofsweepSessionStatefirst -- it can, being the younger file -- the transcript was distilled a second time. The origin write now returns early for a finalized session, and the sweep keeps a marker as long as its state file is one the sweep could act on. An unparseable state file is invisible to both the sweep andlistPendingSessions, so pinning a marker behind it would strand both files rather than protect anything.
Changed
- Releases are cut by merging a VERSION bump. GitHub Actions creates the
vX.Y.Ztag, the GitHub Release (notes taken from the version's CHANGELOG section) and the npm publish once CI passes onmain. Tags are no longer pushed by hand, and a hand-pushed tag publishes nothing.
Removed
setCachedProjectKeyis gone from@elderfo/mehmory/core/session. It was the dead second writer forproject_keyand had no callers.rememberSessionOriginnow owns that field, and takes the project key as a required fourth argument -- a breaking change to the same subpath export for any consumer that called either function directly.
Added
.github/copilot-instructions.md, with a maintenance matrix (what to update when hooks, config keys, error codes, CLI commands, or skills change) and conventions mined from past PR review feedback, mainly around session-state locking..github/workflows/copilot-setup-steps.yml, so a Copilot coding agent session starts with dependencies installed and the project built, matching local setup.- README now links to
CONTRIBUTING.md.
0.4.0 - 2026-08-25
Added
-
Agent scopes: per-agent memory beside the shared project scope. A third scope,
agents/<name>/, holds what an agent is — its preferences, its style, what it has learned about itself — alongsideglobal/(facts about you) andprojects/<key>/(facts about the repo). An agent declares its name throughMEHMORY_AGENT, falling back to the newidentity.agentconfig key; instances that share a model can then accumulate distinct selves instead of pooling into one. Captured entries carry anagent=stamp, and/mehmory:integratefiles self-facts into that agent's scope. A named agent's own content is injected atSessionStartas a fourth share of the existinginjection.budget_tokens, which stays a hard cap — naming an agent does not raise it. Addressing agent scopes fromsearch,statsandpurgeis not part of this change.An agent that declares no name is unaffected: capture, recall, injection budget and on-disk layout are all unchanged, and no agent scope is created for it.
-
Portable Agent Plugins packaging. The root
plugin.jsonandskills/directory now expose the Agent Plugins v1.0.0 surface while preserving Claude Code's compatibility manifest and lifecycle hooks.
Changed
-
FORMAT_VERSIONis 3 andschema_versionis 2. Inbox entries gained an optionalagent=field; older entries are not rewritten and still parse. Downgrading is not symmetric — an older build cannot parse a line carryingagent=at all, so integrate before rolling back.SCHEMA.md's scope rule gained a third clause, sodoctornow reports drift against a customized copy; adopt the new rule to route agent facts. -
The Stop nudge no longer reads as a hook error on Claude Code. The once-per-threshold request to save learnings is carried as
hookSpecificOutput.additionalContextrather than{"decision": "block"}. Both block the turn identically — same re-invoke, samestop_hook_activeguard on the next Stop — but the transcript now showsStop hook feedback: …instead ofStop hook error: …, with no error toast. The reason text also dropped from ~700 to ~310 characters: the literalinbox-txcommand is now emitted only for Codex, where skill invocation is not a slash command. Codex keeps{"decision": "block"}, the only shape its Stop event accepts. -
Skill metadata is portable across clients. Skill
allowed-toolsvalues now use the Agent Skills space-separated format, and the generated hook chunks are tracked so clean marketplace checkouts contain every runtime bundle.
Fixed
- ACP sessions no longer lose their transcript to a finalize race. The Claude Agent SDK
(ACP) writes its rollout after
SessionEndfires, sofinalizeSessionwas retiring the session and capturing nothing when the file had not yet reached disk. It now defers a named-but-absent transcript, leaving the session pending so the next start's sweep captures it once the rollout lands; a transcript that never appears still retires after the sweep's idle window.
Security
- Skills no longer install npm packages implicitly. Missing
mehmorybinaries now stop with an explicit installation request instead of running an unpinned global install.
0.3.1 - 2026-08-07
The first release whose plugin installs actually work. Every 0.x release before this one
shipped a hooks.json pointing at bundles that were not in the installed tree, so the hooks
never ran — reinstall the plugin to pick this up.
Fixed
-
Every hook was dead in any plugin install.
hooks.jsonregistered five commands pointing athooks/*.mjsbundles that were not in the installed plugin, so all five hooks — SessionStart, UserPromptSubmit, Stop, PreCompact, SessionEnd — exited 1 withMODULE_NOT_FOUND. Nothing was captured, nothing was injected, and because the host reports that as a hook failure rather than a broken plugin, it looked like memory was simply empty.The bundles were gitignored on
main, andrelease.ymlforce-added them onto thev*tag to compensate. The tag was correct —v0.3.0carries all six files — and it never reached anyone: the plugin marketplace clones the repository's default branch, not the tag, so installs recordedmain's HEAD and got ahooks/directory containing onlyhooks.json.hooks/*.mjsis now committed onmain(A25 indocs/WORLD_MODEL.md) and the tag inherits it. Committed build output can go stale, so CI rebuilds and fails ifhooks/differs by a byte; the bundles are content-addressed and reproduce identically across machines, so that gate is deterministic. A test now asserts each bundle is tracked by git rather than merely present on disk — the previousexistsSynccheck was satisfied by any localpnpm build, which is why the outage shipped.The two distribution paths update at different moments. Plugin users reinstall to pick this up as soon as it is on
main— the marketplace installs from the default branch, so no tag is involved, which is the whole point of the change. The npm package still updates on thev0.3.1tag.dist/stays gitignored, because a marketplace install never puts themehmoryCLI onPATHregardless — that surface is npm's.
Security
-
A crafted
config.jsoncould poison every object in the process.JSON.parseturns__proto__into a real own enumerable property, and the config merge treated it as ordinary data —'__proto__' in targetis true through the prototype chain, so the recursion wrote straight intoObject.prototype. Since config is merged into defaults on every hook run, one poisoned file leaked a property onto every object mehmory touched. Prototype-reaching keys (__proto__,constructor,prototype) are now dropped, and the merge tests own-property membership instead of walking the chain. -
Inbox text ending a comment early.
serializeInboxEntryneutralized-->but not--!>, which HTML also accepts as a comment terminator, leaving one spelling live in text written into a markdown file. Both are escaped now, reversibly — a round-trip still returns the user's exact words.Both were surfaced by CodeQL against the newly committed bundles and fixed at the source rather than suppressed, so the pre-existing alerts on
src/schema/format.tsandsrc/core/config.tsclear too.
Removed
- The
build-tagrelease job, along with the workflow's write access to repository contents. It existed only to force-push bundles onto the tag; with them on the branch it has nothing left to do, andrelease.ymlno longer needs a token that can rewrite refs.
0.3.0 - 2026-08-04
This is the first release published to npmjs.org, and the first one installable without a token. It is also the first release since the repository became public.
Changed
-
The package is
mehmoryon npmjs, not@elderfo/mehmoryon GitHub Packages. GitHub Packages has no anonymous read, so the documented install told every reader to mint aread:packagestoken before they could try the tool — a private-repo arrangement that outlived the private repo. Installing is nownpm install -g mehmory, with no registry configuration and no token. Releases authenticate with anNPM_TOKENsecret instead of the workflow'sGITHUB_TOKEN, and the release-workflow test asserts the old registry and scope are absent rather than present, so a half-revert fails loudly.Anyone on the old package should replace it:
npm uninstall -g @elderfo/mehmory && npm install -g mehmory.@elderfo/mehmorystays published on GitHub Packages at 0.2.1 rather than being deleted — removing it would break existing installs, and the README documents the switch. The@mehmory/mehmoryname on npmjs is a reservation stub containing no code; it exists so the scope can't be claimed by someone else, and points atmehmory.
Added
- Contributor and security docs, now that the repository is public:
CONTRIBUTING.md,SECURITY.md(private vulnerability reporting, with the secret filter named as the security-relevant surface),CODE_OF_CONDUCT.md, issue forms, and a pull request template.
Security
- CI runs on fork pull requests, so
ci.ymldeclarespermissions: {}at the workflow level andcontents: readon the job instead of inheriting the repository default. Secret scanning, push protection, and Dependabot alerts are enabled on the repository.
0.2.1 - 2026-08-03
Fixed
- The Stop nudge asks for a silent save. The block reason told the model what to save but never how loudly. One session answered it mid-brainstorm by reciting all twelve saved entries and appending a "where we left off" recap, which buried the dialogue it interrupted. The reason now asks for the append and one short sentence — no list, no recap, no status summary.
- Capture now has a retention decision. The distill pattern list ended in a
user_messagecatch-all that matched every user turn unconditionally, so the keyword patterns above it only chose a label — nothing anywhere in the pipeline decided whether a turn was worth keeping. Menu picks and acknowledgements (A,yes,Ship it) were filed as memory; one store held seven consecutive one-letter entries. A turn now needs eight characters to reach the inbox. The floor deliberately errs toward keeping: it lets some multi-word ephemera through rather than risk discarding the user's own words, since a junk entry is visible and deletable at integrate time and a dropped fact leaves no signal. - Harness notification blocks are stripped like the other machine text.
<task-notification>and<system-reminder>joined the slash-command and bash envelopes in the noise filter. Without them a single agent-completion notice was filed verbatim, and a run that dispatched several subagents wrote a near-identical entry for each. - The noise filter no longer misses a differently-spelled tag, nor eats prose around one.
It matched only a bare lowercase tag, so
<Task-Notification>,<task-notification id="1">and a block truncated before its closing tag all passed through and were filed verbatim — machine text that later gets re-injected into a session. Matching is now case-insensitive, tolerates attributes, and discards an unterminated block. It is also anchored to the start of a line, so a turn that quotes a tag name inline while discussing it keeps its prose instead of having everything between the two mentions deleted.
0.2.0 - 2026-08-01
Added
- Codex CLI support, alongside Claude Code. mehmory is a two-harness memory layer now,
not a Claude-Code-only plugin:
mehmory init --host codex [--uninstall]wires the same four capture hooks and the six skills into$CODEX_HOME, reading and writinghooks.json/config.tomlas merge-only edits so entries owned by other tools are never touched. There is one plugin manifest (.claude-plugin/plugin.json+marketplace.json) serving both harnesses — Codex'splugin marketplace add/plugin addread the same files Claude Code does, so a second manifest was deliberately not created. A per-harnesshosts.claude-code.enabled/hosts.codex.enabledconfig toggle lets you adopt the Codex side gradually. Redaction andpurgereach content from either harness identically — there is one store, not one per harness. - Project website — a VitePress site under
site/, deployed to GitHub Pages on every push that touches it. Carries the pitch, a five-minute quickstart, and a "how it works" walkthrough of the three planes, the retrieval trade-off, and the context budget. Deep reference (docs/CLI.md,CONFIG.md,PRIVACY.md,TROUBLESHOOTING.md,UPGRADE.md,WORLD_MODEL.md) is linked from the site rather than duplicated into it, so there is one copy of every fact. Build it locally withpnpm docs:dev. The site is not part of the published package —filesinpackage.jsonis unchanged.
Fixed
- Slash-command and bash-mode turns are no longer filed as memory. Claude Code writes
them as ordinary
type: 'user'records with noisMetaflag, so distill treated a/reload-pluginsecho or a<local-command-stdout>block as something the user said — an inbox, and eventually wiki pages, built from command transcripts. The envelope blocks are now stripped in place:<command-args>survives, because/orchestrate <a whole project brief>puts real intent there, and so does prose the user typed after an echo in the same record.
Known limitations
- Whether Codex trusts a freshly written mehmory hook on first run is unverified on
Codex CLI 0.146.0 —
config.toml'strusted_hashmechanism was not measurable without a real user configuration. Seedocs/TROUBLESHOOTING.md. - The
PreCompacthook's payload on Codex is unverified; finalization relies on the next-session-start path instead of onPreCompactfiring. - The Stop nudge does not render under
codex exec(non-interactive) — capture still happens, but a non-interactive run has no follow-up turn to show the nudge in. mehmory onboardmines only~/.claude/projects/*/; there is no Codex-transcript equivalent to backfill from.
0.1.0 - 2026-08-01
First released version. Everything below shipped across the three build runs that
preceded it; 0.0.1 was scaffolding and was never published.
Added
- Markdown wiki memory store at
~/.mehmory(or$MEHMORY_HOME) — a git-backed directory of pages that a Claude Code session reads and writes with ordinary file operations. No embeddings, no MCP server, no external services. - Five Claude Code hooks —
SessionStartinjects relevant memory into the session,UserPromptSubmitmatches pages against the prompt,StopandSessionEndcapture what happened,PreCompactpreserves context before compaction. Every hook fails open: an error is logged toerrors.logand the session continues unaffected. - Model-driven skills —
integrateturns captured inbox entries into wiki pages,rememberrecords something deliberately mid-session,lintjudges whether pages are still true,onboard-sessionseeds a new project, andpause/resumebracket work you intend to come back to. mehmoryCLI —init,onboard,search,status,stats,doctor, andpurge, each with a--jsonenvelope for scripting.- Search across pages, archive, and log as a single scan, with a documented cap and warning rather than a silent truncation.
- Secret redaction on capture, applied before anything reaches the store.
- Decay classes — pages carry an optional
decayfield (evergreen,ephemeral, or the default) that governs how aggressively they age. Default pages are demoted after 60 days and archived after 90; ephemeral content is refreshed or deleted on every integrate pass rather than on a timer; evergreen pages are exempt from mechanical decay. schema_versiondrift warning indoctor, so a store written by an older plugin version surfaces an upgrade signal instead of failing quietly.- Documentation —
docs/CLI.md,docs/CONFIG.md,docs/PRIVACY.md,docs/TROUBLESHOOTING.md,docs/UPGRADE.md, anddocs/WORLD_MODEL.md, with tests that keep the CLI reference and the README quickstart honest against the built binary. - Publishing to GitHub Packages — tagging
v*builds the hook bundles into the tagged tree and publishes@elderfo/mehmoryto the owner's registry.
Known limitations
- Installing the CLI from GitHub Packages requires a GitHub token with
read:packages; the registry has no anonymous read, even for a public package. See the README. project.mdonly carries integrated content after the first/mehmory:integrate, so the session that already knows your project is the second one, not the first.- No release has been published before this one, so the tag-driven publish path runs against the live registry for the first time here.