From obsidian-brain
Interactively saves curated insights from the current Claude Code session to the Obsidian vault. Use when: (1) /compress command to save session insights, (2) /compress <topic> to extract a specific topic, (3) user wants to capture decisions, patterns, solutions, or error fixes from the current session.
How this skill is triggered — by the user, by Claude, or both
Slash command
/obsidian-brain:compressThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Analyze the current conversation, extract valuable insights, and save them as structured notes in the Obsidian vault. Supports both interactive multi-insight selection and targeted single-topic extraction.
Analyze the current conversation, extract valuable insights, and save them as structured notes in the Obsidian vault. Supports both interactive multi-insight selection and targeted single-topic extraction.
Tools needed: Bash, Write, Read, Edit
Follow these steps exactly. Do not skip steps or reorder them.
Run:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from obsidian_utils import load_config
c = load_config()
if not c.get("vault_path"):
print("ERROR: vault_path not configured", file=sys.stderr)
sys.exit(1)
print("VAULT=" + c["vault_path"])
print("SESS=" + c.get("sessions_folder", "claude-sessions"))
print("INS=" + c.get("insights_folder", "claude-insights"))
'
Parse each output line as KEY=VALUE, splitting on the first =.
If the output is empty or errors, tell the user:
Config not found. Please run
/obsidian-setupfirst to configure your Obsidian vault.
Stop here if config is missing.
Run:
test -d "$VAULT_PATH/$INSIGHTS_FOLDER" && test -w "$VAULT_PATH/$INSIGHTS_FOLDER" && echo "OK" || echo "FAIL"
If FAIL, tell the user:
The insights folder
$VAULT_PATH/$INSIGHTS_FOLDERdoes not exist or is not writable. Run/obsidian-setupto fix this.
Stop here if FAIL.
Check if the user provided a topic argument after /compress.
/compress rate limiting strategy): Go to Step 3.5./compress): Go to Step 4B.Run a single Python call to search the vault index for existing notes matching the topic:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os, json
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
try:
from vault_index import ensure_index, search_vault
from obsidian_utils import load_config
# Pure predicate: top rank must pass absolute-strength gate AND |top|-|#2| delta gate.
# MIN_RANK_DELTA tuned against scripts/compress_rank_gap_corpus.json (issue #45).
from compress_guard import is_high_confidence_match
c = load_config()
vp = c["vault_path"]
folders = [c.get("sessions_folder", "claude-sessions"), c.get("insights_folder", "claude-insights")]
db = ensure_index(vp, folders)
results = search_vault(db, sys.argv[1], note_type="claude-insight", limit=3)
results += search_vault(db, sys.argv[1], note_type="claude-decision", limit=3)
# Sort combined results by rank (most negative = best match)
results.sort(key=lambda r: r["rank"])
if is_high_confidence_match(results):
top = results[0]
print(json.dumps({"match": True, "path": top["path"], "title": top["title"], "date": top["date"], "tags": top["tags"], "rank": top["rank"]}))
else:
print(json.dumps({"match": False}))
except ImportError as e:
print(f"Warning: plugin hooks are out of date or missing ({e}) — run /dev-test install", file=sys.stderr)
print(json.dumps({"match": False}))
except Exception as e:
print(f"Warning: could not search vault index: {e}", file=sys.stderr)
print(json.dumps({"match": False}))
' "$TOPIC"
Parse the JSON output. If the script exits non-zero or the output cannot be parsed as JSON, treat it as {"match": false} and proceed silently (log a note: "Could not search vault index; creating new note.").
If match is true, store the path field as MATCH_PATH and the title field as MATCH_TITLE. Format tags by splitting on commas and joining with , . If tags is empty or null, display "no tags".
If match is false: No existing note found. Proceed silently to Step 4A (create new note).
If match is true: Present the match to the user:
Found an existing note on this topic: "" (<date>, <tags as comma-separated list>)
Would you like to update this note or create new?
Wait for the user's response:
Analyze the current conversation for content related to the user's specified topic. Draft a note that includes:
Skip to Step 5.
This step is reached when the user chose "update" in Step 3.5. The matched note path is $MATCH_PATH.
Use the Read tool to read the full contents of $MATCH_PATH. Note the existing frontmatter tags and whether a last_updated field is already present.
Analyze the current conversation for content related to the topic. Draft a dated update section:
## Update (YYYY-MM-DD)
<New content about this topic from today's session. Include:
- New findings, corrections, or extensions to the original insight
- Code snippets or commands if relevant
- Context on why this update was triggered>
Where YYYY-MM-DD is today's date.
Important: Do NOT rewrite or duplicate existing content. The update section captures only what is NEW from this session.
Present ONLY the new update section (not the full existing note):
Update section to append to "< existing note title>":
(show the drafted
## Update (YYYY-MM-DD)section)Preview above. Would you like to:
- save — append this update
- edit content — tell me what to change
- cancel — discard this update
Wait for the user's response. Apply edits and re-show if requested. Repeat until the user says save or cancel.
If cancel, stop here.
Use the Edit tool to append the update section to the note body.
Insertion point: Scan the note from the bottom for these trailing metadata patterns: _(Summary source: ...)_, ## Tool Usage, ## Conversation (raw), ## Session Metadata, ## Files Touched. If any are found, insert the update section on a new line immediately BEFORE the first trailing section. If none are found, append at the very end of the file.
Use the Edit tool with the first line of the trailing section (or the last line of the file) as old_string, and prepend the update section + a blank line before it.
Verify: After the Edit, use the Read tool to confirm the ## Update (YYYY-MM-DD) heading is present in the note. If it is not, tell the user: "Failed to append update section — file may have unexpected structure. Please edit manually at $MATCH_PATH." Do NOT proceed to 4A-update.5 if the append failed.
Use the Edit tool to update the frontmatter of the existing note:
last_updated field: If last_updated: already exists in the frontmatter, replace its value with today's date. If it does not exist, add last_updated: YYYY-MM-DD after the date: line.
New topic tags: Generate 1-3 topic tags from the update content (same logic as Step 5). For each new tag, check if it already exists in the tags: list. Only append tags that are NOT already present. Add new tags at the end of the tags list, before the closing ---.
Do NOT change: date, source_session, source_session_note, or type fields. These record the original creation context.
Run:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from vault_index import ensure_index
from obsidian_utils import load_config
c = load_config()
vp = c["vault_path"]
folders = [c.get("sessions_folder", "claude-sessions"), c.get("insights_folder", "claude-insights")]
try:
ensure_index(vp, folders)
print("OK")
except Exception as e:
print(f"WARN: re-sync failed (non-fatal): {e}")
'
Print:
Note updated!
- File:
$MATCH_PATH- Added section: "Update (YYYY-MM-DD)"
- New tags:
<list of newly added tags>(or "none")
Skip to Step 10 (offer follow-up). Do NOT proceed through Steps 5-9 (those are the create-new flow).
First, check for claudeception output using layered detection:
Layer 1 — High-confidence structured markers (check first):
Scan the current conversation for these patterns. If found, extract the skill/knowledge name and a one-line summary:
MANDATORY SKILL EVALUATION REQUIRED banner (from the claudeception activator hook)Result: PASS or Result: FAIL (from the claudeception skill validator)~/.claude/skills/*/SKILL.md or .claude/skills/*/SKILL.mdIf any Layer 1 markers are found, create a candidate for each and label it [from claudeception].
Layer 2 — Broad phrase scanning (fallback, only if Layer 1 found nothing):
Scan the conversation for these phrases:
/claudeception invocationIf any Layer 2 phrases are found, create a candidate for each and label it [possibly from claudeception].
Then, perform standard insight discovery:
Analyze the full conversation and identify 3-5 additional candidate insights (beyond any claudeception candidates). Each candidate should be one of these types:
Present all candidates as a numbered list, with claudeception candidates first:
Insights found in this session:
- [from claudeception] [Discovery] Rate limiter pattern — extracted as reusable skill
- [possibly from claudeception] [Pattern] Retry with exponential backoff — identified across 3 sessions
- [Decision] Chose Redis for session store — trade-off analysis
- [Solution] Fixed CORS issue with Safari — root cause in preflight handling
Which would you like to save? (e.g.
1,3orall)
If no claudeception output was detected, present only the standard candidates (same as before — no labels).
When the user says all, all candidates (including claudeception ones) are saved. When the user picks specific numbers, only those are saved — standard selection behavior.
Wait for the user to pick. For each selected insight, draft the note content and continue to Step 5. Process selected insights one at a time.
Based on the note content, generate 1-3 topic tags. Tags should be lowercase, hyphenated, and specific. Examples:
claude/topic/rate-limitingclaude/topic/react-hooksclaude/topic/git-workflowclaude/topic/api-designPresent the full note to the user including frontmatter:
---
type: claude-insight
date: YYYY-MM-DD
created_at: <ISO-8601-UTC>
source_session: <current-session-id>
source_session_note: "[[<session-note-filename>]]"
project: <project-name>
tags:
- claude/insight
- claude/project/<project-name>
- claude/topic/<auto-generated-topic-1>
- claude/topic/<auto-generated-topic-2>
---
# <Title>
<Note body>
Where:
YYYY-MM-DD is today's date
<ISO-8601-UTC> is the current UTC timestamp at second precision. Get it via:
python3 -c 'from datetime import datetime, timezone; print(datetime.now(timezone.utc).isoformat(timespec="seconds"))'
Example: 2026-04-24T18:42:11+00:00
<current-session-id> and <session-note-filename> are derived together. Get session context via the shared helper:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
python3 -c '
import sys, os
import glob; sys.path.insert(0, max(glob.glob(os.path.expanduser("~/.claude/plugins/cache/*/obsidian-brain/*/hooks")), default="hooks"))
from obsidian_utils import load_config, get_session_context
c = load_config()
ctx = get_session_context(c["vault_path"], c.get("sessions_folder", "claude-sessions"))
print("SID=" + ctx["session_id"] + " HASH=" + ctx["hash"] + " PROJECT=" + ctx["project"] + " SESSION_NOTE=" + ctx["session_note_name"])
'
Parse the output to get SESSION_ID, HASH, PROJECT, and SESSION_NOTE. Use these for the frontmatter fields.
Important: If SESSION_ID is unknown, use unknown for source_session and omit source_session_note entirely.
<project-name> is the PROJECT value from get_session_context() (lowercased, hyphenated basename of cwd)
The source_session_note field creates an Obsidian backlink from the insight to its source session, enabling bidirectional navigation in the graph view
Ask the user:
Preview above. Would you like to:
- save as-is
- edit tags — add or remove tags
- edit content — tell me what to change
- cancel — discard this note
Wait for the user's response. Apply any requested edits and show the updated preview. Repeat until the user says save or cancel.
If cancel, stop here (or move to the next selected insight if processing multiple from Step 4B).
Construct the filename from these parts:
YYYY-MM-DD (today)date +%s | md5 | cut -c29-32 (macOS) or date +%s | md5sum | cut -c1-4 (Linux). Do NOT use tail -c 4 — it counts the trailing newline as a byte and returns only 3 visible characters.Final filename: YYYY-MM-DD-<slug>-<hash>.md
Example: 2026-04-04-rate-limiting-with-redis-a3f2.md
Run:
mkdir -p "$VAULT_PATH/$INSIGHTS_FOLDER"
Then use the Write tool to write the full note (frontmatter + body) to:
$VAULT_PATH/$INSIGHTS_FOLDER/YYYY-MM-DD-<slug>-<hash>.md
Then set permissions:
chmod 644 "$VAULT_PATH/$INSIGHTS_FOLDER/YYYY-MM-DD-<slug>-<hash>.md"
Print:
Insight saved!
- File:
$VAULT_PATH/$INSIGHTS_FOLDER/<filename>- Tags:
claude/insight,claude/project/<name>,claude/topic/<topic1>, ...- Open in Obsidian to view and link to other notes.
If processing multiple insights from Step 4B, repeat Steps 5-9 for each remaining selected insight.
After all insights are saved, ask:
Anything else to capture from this session? You can run
/compressagain or/compress <topic>to extract a specific topic (will offer to update if an existing note matches).
Searches MemPalace before answering questions about past work, people, projects, or prior decisions. Returns verbatim stored content instead of guessing from model memory.
Guides Payload CMS config (payload.config.ts), collections, fields, hooks, access control, APIs. Debugs validation errors, security, relationships, queries, transactions, hook behavior.
Implements vector databases with Pinecone, Weaviate, Qdrant, Milvus, pgvector for semantic search, RAG, recommendations, and similarity systems. Optimizes embeddings, indexing, and hybrid search.
npx claudepluginhub abhattacherjee/obsidian-brain --plugin obsidian-brain