Session
Persistent memory layer for Claude Code — Obsidian vault health, connection discovery, brain dump, session notes, email bridge. Five lightweight skills, CLI-first, any taxonomy.
npx -y skills add jojoprison/mnemo --skill sessionAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 6 stars6 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Use when significant work wraps up — a feature shipped, a bug fixed, a research thread finished, before the user steps away — and also mid-task as a checkpoint before a long run risks context compaction (it updates the same session note + handoff, never spawns a duplicate). Also on 'записать сессию', 'сохрани сессию', 'отложи сессию в память', 'сессию в обсидиан', 'закругляйся с сессией', 'session note', 'handoff', or similar. Writes a human-readable summary plus a handoff so the next session picks up where you left off.
SKILL.md
17.2 KB, as published. Nobody here has run it
mn:session — Session Notes to Obsidian
Invocation marker (both runtimes): begin your reply with the exact line
🧠 mn:session (mnemo) → running— the user-visible confirmation that this skill actually loaded. Emit it once per invocation, before any other output.
Portable paths
Resolve <mnemo-root> once to the absolute plugin root before reading bundled files or running bundled scripts. In Claude Code, use ${CLAUDE_PLUGIN_ROOT}; in Codex, derive it from this loaded SKILL.md path (skill directory → skills/ → plugin root). Replace <mnemo-root> with that quoted absolute path in every command — never execute the placeholder literally and never hunt versioned cache directories.
When another mnemo skill must run, use the runtime-native path: Claude Code invokes mn:<skill> through its Skill tool; Codex reads <mnemo-root>/skills/<skill>/SKILL.md completely and follows it with the prepared input. For user-facing explicit syntax, render /mn:<skill> in Claude Code and $mnemo:<skill> in Codex.
Create a human-readable session summary note in Obsidian after significant work.
Prerequisites & config
Obsidian must be open. Config at ~/.mnemo/config.json — reads vault, taxonomy, taxonomy_roles, links_section, and handoff_note. Before resolving session or hub destinations, require exactly the five role keys fact, insight, source, session, and moc; require every target to exist in taxonomy; and require session → session plus moc → moc. The deterministic legacy Zettelkasten fallback is allowed; any other missing/invalid map offers the runtime-native setup skill instead of guessing. Schema in <mnemo-root>/references/config-schema.md.
Tool-routing (bundled atomic writer for writes, bundled CLI adapter for reads/search) is in <mnemo-root>/references/tool-routing.md. Frontmatter template is in <mnemo-root>/assets/session-template.md.
When to Trigger
- After completing a feature / PR / fix
- After significant research session
- Manually via the current runtime's explicit form (
/mn:sessionin Claude Code,$mnemo:sessionin Codex) - Do NOT trigger for trivial tasks (typo fix, one-liner). Research / exploration / personal-curiosity sessions are NOT trivial — they get a note even with zero code. When unsure, create: a session note is cheap, a lost session is not.
Workflow
Step 1: Summarize Current Session
Mid-task checkpoint vs end-of-session note. If this is a mid-task checkpoint (a long run risks compaction and you're saving progress, not wrapping up) and a note for this same session already exists — match by the session_id you'd write in frontmatter (or the filename derived earlier this session) — update that note in place through the bundled exact-replace/insert writer plus refresh the handoff (Step 5). Do not create a second note for the same session — that fragments the record. Only a genuinely new session or a distinct topic gets a new note.
Analyze the conversation: what was done, key decisions, commits/PRs created, findings.
Thoroughness by routing (standing default). A good session note is thorough by routing material to its right home, not by swelling this one note: the narrative + arc + decisions-in-context live here; business-logic / pains / how-the-user-thinks route to save's typed principle / pain / stance atoms; connections go to connect; unfinished threads stay here as - [ ] items in this note's own pending section (the handoff gets only a pointer to it). Full contract: <mnemo-root>/references/depth-contract.md. Depth = structure, not volume — never fold "capture everything" into the narrative (that is the blob anti-pattern).
Ground the summary in facts — don't rely on conversation memory alone (a note that claims "shipped X" when git shows no such commit is worse than no note). Before writing "what was done", cross-check against reality: git log --oneline -15 + git status --short for real commits/changes, and — when the script is reachable — the session's actual tool/skill activity:
python3 "<mnemo-root>/scripts/session-scan.py" 2>/dev/null | head -30
Reconcile claimed outcomes with these before persisting — the same grounding the canonical review skill performs, applied to the note this session skill writes directly.
Derive a planned filename: {session_prefix}{YYYY-MM-DD} {short descriptive topic}. Topic should be specific enough to disambiguate from other sessions the same day (include PR number, Linear ticket, branch name, or primary keyword).
Naming: the topic must NOT contain #, ., or / — they break wikilinks (#→heading anchor) or the CLI (.→truncation). Write PR 387, not PR #387. See <mnemo-root>/references/tool-routing.md (naming rules).
Step 2: Duplicate Check (two-level, parallel)
Run Level 1 and Level 2 in parallel — single assistant message with two shell tool uses. Use the bundled shell-free helper with quoted JSON heredocs. ~185ms total instead of ~370ms sequential.
Level 1 — exact filename:
python3 "<mnemo-root>/scripts/safe-read.py" read <<'JSON'
{"file":"{planned-filename}","vault":"{vault}"}
JSON
If the read returns content → a note with this EXACT filename already exists. Do NOT silently skip and do NOT auto-overwrite. Offer append / overwrite / rename, leading with append/continuation. For continuation use vault-write.py insert with a unique copied anchor, or guarded append with the exact current tail/hash. Never call create for an existing note. Overwrite only if the user is clearly regenerating the same session, and implement it as an exact optimistic replacement rather than a blind file write.
Level 2 — related same-day sessions (informational only):
python3 "<mnemo-root>/scripts/safe-read.py" search <<'JSON'
{"query":"{session_prefix}{YYYY-MM-DD}","vault":"{vault}"}
JSON
These are NOT duplicates — same day, different topics. Doing many sessions in one day is normal: each distinct topic gets its OWN note. A Level-2 same-day match is context, NOT a reason to skip creation or to assume the note already exists — that mistake silently loses sessions. Show the list to the user so they can:
- Decide if this session should be merged into an existing one
- Remember to cross-link related sessions via
## Связи/## Links
Do not block creation on Level 2 matches — they're context, not conflicts.
Step 3: Create Session Note (same writer in both runtimes)
After the exact role-map validation in Prerequisites, resolve taxonomy_roles.session and taxonomy_roles.moc to existing config.taxonomy entries and use their configured prefixes/tags. Legacy Zettelkasten without a role map uses the documented deterministic fallback; any other missing/invalid map must go through setup rather than guessing.
First, read the template (provides the exact structure to follow):
cat "<mnemo-root>/assets/session-template.md"
Then create the note, filling every template placeholder with grounded current-session context. In particular, {session_type} and {session_tag} come from the taxonomy entry mapped by taxonomy_roles.session; {mapped_moc_note} uses the prefix from the entry mapped by taxonomy_roles.moc; {session_id} is runtime-neutral; and {topic_tags} contains only validated YAML-safe tag values:
python3 "<mnemo-root>/scripts/vault-write.py" <<'JSON'
{"action":"create","vault":"{vault}","note":"{planned filename}","content":"{one JSON-escaped Markdown string with the filled template}"}
JSON
Where {session_prefix} comes from the taxonomy entry mapped by taxonomy_roles.session, {links_section} from config.links_section, and the session id comes from CLAUDE_SESSION_ID in Claude Code or CODEX_THREAD_ID in Codex (CODEX_SESSION_ID remains a legacy fallback; use an empty string if none is available).
The helper receives Markdown through JSON stdin, never shell syntax or Obsidian CLI argv. It verifies vault containment and performs an exclusive atomic create, so backticks, $(...), quotes, and code blocks remain inert. On conflict, re-read and use the duplicate flow; never overwrite.
Codex memory ownership: ${CODEX_HOME:-~/.codex}/memories/ is Codex-generated state. Never create, edit, append, or use it as a session-note fallback. If the vault is unavailable, report the failed durable write and retain the summary in the current response so the user can retry; do not create a shadow copy.
Step 4: Verify MOC Link
Read MOC through the shell-free CLI adapter:
python3 "<mnemo-root>/scripts/safe-read.py" read <<'JSON'
{"file":"{MOC name}","vault":"{vault}"}
JSON
Check if the new session note is listed.
If missing — insert through the same optimistic writer:
python3 "<mnemo-root>/scripts/vault-write.py" <<'JSON'
{"action":"insert","vault":"{vault}","note":"{mapped moc note}","anchor":"{unique stable anchor copied from read}","position":"after","content":"\n- [[{session note name}]] — session context"}
JSON
If the anchor is missing/non-unique or the file changes during publication, re-read and retry. Never use inline CLI content.
Step 5: Update Session Handoff
🛑 Unfinished work belongs to its own session note, not to the handoff. Write every open tail into this session's ## Next steps / pending section (Step 4) — that is its home. The handoff gets a pointer, not a copy.
Why this is the contract and not a preference: when tails were written into the shared handoff instead, only 9% of the live vault's fresh open items existed in their own session note (34% for older ones). The handoff became the sole home of forward state, which is exactly how it reached 805 KiB — and why it could then be neither read nor shrunk. A pointer line is bounded by the number of sessions; a copied tail is bounded by nothing.
Upsert this session's pointer line — the handoff index:
python3 "<mnemo-root>/scripts/vault-write.py" <<'JSON'
{"action":"handoff-index-upsert","vault":"{vault}","note":"{handoff_note}","session_note":"{this session's note name}","date":"{YYYY-MM-DD}","project":"{project}","open_count":{count of open items in the session note},"max_kb":{handoff.maxKB},"keep_days":{handoff.keepDays}}
JSON
It writes one line — - 2026-07-25 · mnemo · open 3 · [[Session — …]] — and is idempotent on the session link: a mid-task checkpoint refreshes its own line instead of appending a twin. Count open_count from the session note you just wrote; do not estimate it. Omit max_kb/keep_days when the config has no handoff section; the defaults (56 KiB, 31 days) apply.
The index holds a calendar window, not a line count. Pointers older than handoff.keepDays are dropped — the session note they point to keeps the detail, so nothing is lost by eviction. handoff.maxKB is a backstop underneath that: if a month is busier than the window fits, the oldest pointers give way and the file says so in one > _overflow: line rather than quietly holding a shorter window. Per-line length (handoff.maxLineBytes, 200) is enforced on every line, not only the new one, and always in bytes — a Cyrillic line is ~2 B/char. The session link is never truncated; only the project label gives way.
If the handoff note does not exist, create it via vault-write.py create with the same structure as setup Step 6, then upsert.
Legacy block format. A handoff still holding ## YYYY-MM-DD blocks keeps them: the upsert adds its index line without touching them, and the size-guard below still rotates closed blocks. Migrating the blocks is a deliberate, separately-run operation — never a side effect of writing a session note.
Size-guard — keep the handoff thin (run every session):
The handoff is a LIVE index, not an archive. After updating it, run the archival helper so closed history doesn't accumulate into a note nothing can read (it no-ops when the note is at/under handoff.maxKB):
python3 "<mnemo-root>/scripts/vault-write.py" <<'JSON'
{"action":"archive-handoff","vault":"{vault}","note":"{handoff_note}","max_kb":{handoff.maxKB},"keep_days":{handoff.keepDays}}
JSON
Replace the placeholders with the configured integer values, or omit both keys when the config has no handoff section.
Keeps HOT: entries with an open - [ ] + the last keepDays. Moves CLOSED older entries verbatim to {handoff_note} Archive (cold, not read at session start; a unique .bak-<date> is written for undo). Archive-first publication and retry deduplication prevent loss across partial failures; the handoff/archive replacements use the same optimistic atomic writer as every other vault edit. Their durable detail already lives in the linked session notes. If the vault path can't be resolved (Obsidian absent), skip and report it.
Step 6: Orphan Check
python3 "<mnemo-root>/scripts/safe-read.py" orphans <<'JSON'
{"vault":"{vault}"}
JSON
If the newly created note appears in orphans, it means no ## Связи links or the MOC didn't get updated.
⚠️ obsidian orphans caches & lags writes 1-5s — a freshly written note may show as orphan falsely. If it appears right after creation, wait 2-3s and re-run, or verify authoritatively via obsidian eval (metadataCache.resolvedLinks/unresolvedLinks). See <mnemo-root>/references/gotchas.md.
Step 7: Confirm — with an own-note self-check
Before the summary, self-verify this note's own artifact (not a cross-skill audit — that is review --full Step 9): reusing Steps 2 / 4 / 6, reconfirm it is not a duplicate of today's session, its ## Связи carries the mapped MOC link, it is non-orphan, and any atom-worthy material was delegated to save rather than folded in. Report residual own-note gaps.
Output summary:
- Note name
- MOC updated (yes/no)
- Handoff updated (yes/no)
- Orphan status (clean / flagged)
- Own-note self-check (clean / gaps: {list})
Rules
vault-write.pyfor every vault write — one shell-free, atomic path in both runtimes- CLI for read/search/index — faster, indexed, unique functions
- No inline
obsidian create content="..."with markdown — banned: zsh expands backticks /$(...)in the body (real incident: nearly ranmake deploy-back) - Two-level duplicate check — exact-read + same-day-search
- Include session_id in frontmatter — disambiguates same-day sessions
- No session notes for trivial work — but "trivial" = mechanical one-liners only (typo, single rename). A research / exploration / curiosity session counts as significant even with zero code; default to creating.
- Branch field optional — research sessions don't have branches
- Handoff = thin live index, not an archive — targeted optimistic replacement, not blind append. Named ceiling: when it exceeds
handoff.maxKB(default 40KB),vault-write.py archive-handoff(Step 5) rotates CLOSED blocks older thanhandoff.keepDaysinto{handoff_note} Archive(cold); open- [ ]+ recent stay hot. 🛑 The guard is necessary but not sufficient, by its own invariant: a block with even one open- [ ]is never archived, so on a real vault the archiver can be a no-op while the file is 20× over the ceiling (measured: 805 KiB, 863 open items, 0 blocks eligible). Size is bounded by the format of what gets written here; the guard only rotates what is already closed.healthStep 7.6 reports what the guard cannot move and why. - Links section is mandatory — at least one MOC link, else the note orphans (invisible to graph navigation)
- Ghost notes generously — wrap projects, technologies, people in
[[wikilinks]] - Thorough by routing, not volume — the note stays a narrative; atom-worthy material (decisions, business-logic, pains, how-the-user-thinks) goes to
save, links toconnect, unfinished work to handoff. Full contract in<mnemo-root>/references/depth-contract.md - Own-note self-check only — Step 7 verifies this note's own artifact (dup / MOC / orphan / delegation); the cross-skill palace audit belongs to
review --full, not here
Gotchas
Common failures (IPC hung, shell injection) in <mnemo-root>/references/gotchas.md. Skill-specific rules:
- Writer contract:
noteis vault-relative (.mdoptional);createfails if the target exists;replacerequires exactly oneold_str;insertrequires exactly one anchor; guarded append requires an expected tail/hash. - Always check duplicate before creating — prevents clobbering same-day work. Two-level check in Step 2.
- Exact matching means exact whitespace — copy the anchor/old section verbatim from read output.