Install
$ agentstack add skill-abhattacherjee-claude-code-skills-recall ✓ scanned · ✓ verified — works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ✓ Network access No
- ● Filesystem access Used
- ● Shell / process execution Used
- ✓ Environment & secrets No
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
About
Recall — Load Project Context from Obsidian Vault
Searches the Obsidian vault for session notes and insights matching the current project, upgrades any unsummarized notes with AI summaries, and presents a concise context brief.
Tools needed: Bash, Grep, Read, Write
Procedure
Follow these steps exactly. Do not skip steps or reorder them.
Step 1 — Load config and derive project
Run a single call that loads config and derives the project name (saves one parent round):
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)
project = os.path.basename(os.getcwd()).lower().replace(" ", "-")
print("VAULT=" + c["vault_path"])
print("SESS=" + c.get("sessions_folder", "claude-sessions"))
print("INS=" + c.get("insights_folder", "claude-insights"))
print("PROJECT=" + project)
'
Parse each output line as KEY=VALUE, splitting on the first =.
If the user passed a project name argument (e.g. /recall my-project), override PROJECT with that value.
If the output is empty or errors, tell the user:
> Config not found. Run /obsidian-setup first to configure your Obsidian vault.
Stop here if config is missing.
Create the task manifest for the full /recall flow:
TaskCreate: subject="Find unsummarized notes", activeForm="Searching for unsummarized notes"
TaskCreate: subject="Summarize unsummarized notes", activeForm="Summarizing notes"
TaskCreate: subject="Present read-only context brief", activeForm="Building and presenting context brief"
Track the returned task IDs — you will update them as each step completes. Immediately set task #1 to in_progress via TaskUpdate.
Step 2 — Summarize unsummarized notes (deferred summarization, truncation-aware)
> ⚠️ THIS STEP IS MANDATORY. DO NOT SKIP IT. > > If Grep finds any file matching both status: auto-logged AND project: $PROJECT, you must produce an upgraded summary for every such file before proceeding to Step 3. "Skipping to save context" or "the other session covers it" is a bug, not an optimization — the user ran /recall specifically to get current-session context, and stale unsummarized notes are exactly what they asked you to fix. > > Visibility requirement: Before Step 3, emit a one-line status: Step 2: processing N unsummarized note(s) for $PROJECT (or Step 2: no unsummarized notes for $PROJECT if the intersection is empty). This makes the decision auditable in the tool trace.
Find unsummarized notes for this project in a single Python call (replaces multiple Grep rounds):
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 find_unsummarized_notes
print(find_unsummarized_notes(sys.argv[1], sys.argv[2], sys.argv[3]))
' "$VAULT_PATH" "$SESSIONS_FOLDER" "$PROJECT"
Parse the JSON output: {"unsummarized": ["/path/to/note1.md", ...], "auto_fixed": N}.
The function handles project filtering, defense-in-depth (skips notes with real ## Summary but stale auto-logged status, auto-fixes them), and returns only genuinely unsummarized note paths.
If auto_fixed > 0, report: Auto-fixed N note(s) with stale status.
Store the length of unsummarized as N.
Update task #1 to completed. Update task #2 subject to Summarize N unsummarized note(s) and set to in_progress.
Path A: N=0 (no unsummarized notes)
Update task #2 subject to No unsummarized notes found and set to completed. Skip to Step 3.
Path B: N>=1 (parallel Haiku pipelines with sub-agent fallback)
Task management threshold: If N 5, skip per-note sub-tasks — use a single progress update on task #2 instead. This saves ~15-20s of parent round-trip overhead at large N.
Phase 1 — Parallel Haiku upgrades (single batch call)
If N ", activeForm "Upgrading via Haiku"`).
Single Bash tool call — upgrade_batch() fans out N Haiku invocations in parallel inside one Python process via concurrent.futures.ThreadPoolExecutor. This sidesteps the Claude Code harness's serialization of parallel Bash tool calls for subprocess-blocking work:
cd "$(git rev-parse --show-toplevel 2>/dev/null || pwd)"
printf '%s' "$UNSUMMARIZED_PATHS_JSON" | 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"))
from obsidian_utils import upgrade_batch
paths = json.loads(sys.stdin.read())
results = upgrade_batch(paths, sys.argv[1], sys.argv[2], sys.argv[3])
print(json.dumps([{"path": p, "status": s} for p, s in results]))
' "$VAULT_PATH" "$SESSIONS_FOLDER" "$PROJECT"
Parse the returned JSON array. For each entry:
statusstarts withUpgraded→ mark as succeeded- anything else (including
Failed: ..., empty, or unexpected prefix) → add to the Phase 2 fallback list
If N ). If N > 5: update task #2 subject to Upgrade N notes: M succeeded, F pending fallback`.
> Why a single Bash call, not N parallel calls? The Claude Code harness serializes parallel Bash tool calls through a limited shell pool when each subprocess blocks on I/O (e.g., claude -p --model haiku taking 5-30s). Dispatching 10 Bash calls in one message still executes them one at a time — wall time ≈ Σ per-call. Pushing fan-out into a single Python process with ThreadPoolExecutor gives true concurrency (the GIL releases during subprocess waits), so wall time ≈ max per-call. See claude-insights/2026-04-21-recall-parallel-bash-dispatch-runs-sequentially-fbee-error.md and GH #69.
Phase 2 — Sub-agent fallback (only for failed notes)
If no failures, skip this phase entirely.
For each failed note, spawn a sub-agent. If multiple notes failed, spawn all sub-agents in a single message turn:
Agent({
description: "Summarize session note ",
prompt: "Read the session note at . Produce a structured summary with these exact markdown sections:\n\n## Summary\n1-3 sentence overview of what was accomplished.\n\n## Key Decisions\n- Bullet list of important technical decisions. Write \"None noted.\" if none.\n\n## Changes Made\n- Bullet list of files modified/created with brief description. Write \"None noted.\" if none.\n\n## Errors Encountered\n- Bullet list of errors and how resolved. Write \"None.\" if none.\n\n## Open Questions / Next Steps\n- [ ] Checkbox list of unresolved items. Write \"None.\" if none.\n\nWrite the summary to ~/.claude/obsidian-brain/summary-.md using the Write tool. After the summary sections, add a final line:\nIMPORTANCE: N\nwhere N is 1-10. 1-3: trivial (config, interrupted). 4-6: standard work. 7-8: key decisions or error resolutions. 9-10: major releases or security audits.\n\nReturn ONLY the single line: WRITTEN:~/.claude/obsidian-brain/summary-.md"
})
When sub-agents return, for each:
- If the sub-agent returned
WRITTEN:, extract the path afterWRITTEN:and replace the leading~with$HOMEto get an absolute path. Store this asSUMMARY_TEMP_PATH. Verify the file exists:test -f "$SUMMARY_TEMP_PATH" && echo "EXISTS" || echo "MISSING". - If EXISTS, apply it via Python:
``bash 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 upgrade_note_with_summary with open(os.path.expanduser(sys.argv[6]), "r") as f: summary = f.read() status = upgrade_note_with_summary(sys.argv[1], summary, sys.argv[2], sys.argv[3], sys.argv[4], sys.argv[5]) print(status) ' "$NOTE_PATH" "$VAULT_PATH" "$SESSIONS_FOLDER" "$PROJECT" "sub-agent" "$SUMMARY_TEMP_PATH" ``
If the write-back status starts with Failed:, count this note as permanently failed — do NOT count it as upgraded. If N `.
If the write-back succeeds, and N `.
- If MISSING or sub-agent didn't return
WRITTEN:→ note stays unsummarized for next/recall. If N `.
Always clean up temp files from Phase 2 after all write-backs complete, regardless of outcome. Use the actual SUMMARY_TEMP_PATH values collected from each sub-agent's WRITTEN: response (not placeholder names):
rm -f "$SUMMARY_TEMP_PATH_1" "$SUMMARY_TEMP_PATH_2" ...
If N > 5: update task #2 subject to reflect final Phase 2 results (e.g. Upgrade N notes: M Haiku + F fallback succeeded, K failed).
Completion
Mark task #2 as completed. Report results:
- How many upgraded via Haiku pipeline (Phase 1 successes)
- How many upgraded via sub-agent fallback (Phase 2 write-back successes)
- How many permanently failed (notes where both Phase 1 Haiku AND Phase 2 sub-agent fallback failed or were skipped — these stay unsummarized for next
/recall)
For failed notes: "Note ` could not be summarized. It will be retried on the next /recall`."
Step 3 — Build context brief (Python)
Update task #3 to in_progress.
Run a single Python call that reads all session and insight files and composes the brief — no sub-agent needed:
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 build_context_brief, check_hook_status
hs = check_hook_status()
status_line = ("[OK] " if hs["ok"] else "[WARN] ") + hs["message"]
print(build_context_brief(sys.argv[1], sys.argv[2], sys.argv[3], sys.argv[4], hook_status_line=status_line))
' "$VAULT_PATH" "$SESSIONS_FOLDER" "$INSIGHTS_FOLDER" "$PROJECT"
The first line of the emitted CONTEXT_BRIEF is always the hook-status line. If it starts with [OK], omit it from the displayed output — the user doesn't need to see "session logging active" every time. If it starts with [WARN], display it verbatim so the user knows to take action (e.g., run /obsidian-setup).
If the command fails (non-zero exit code), print the error and stop — do not fall back to in-context reads.
Parse the output. Split on section labels:
- Extract
>>— everything between this delimiter and>>. This is the brief to display. - Extract
>>— parsefull_session_title,full_session_date,full_session_path,summary_session_title,summary_session_date,insight_count,snapshot_count(optional), and allsnapshot:lines (there may be zero or more, each followed by optional 2-space-indentedkey_contextbullets). - Extract
>>— eitherNO_CANDIDATES,NO_ITEMS, or a JSON array. Count the number of- [ ]items across all scanned session notes. Store asopen_items_total.
Present the brief immediately (same turn — saves one parent round):
> Here's what I found from your Obsidian vault for $PROJECT:
Then output the CONTEXT_BRIEF section. For the session history table, paraphrase each session's Title column into a concise one-line summary (under ~80 characters) that captures the key accomplishment. Keep all other columns (date, duration, branch) verbatim.
Snapshots appear in the brief as nested indented rows beneath their parent session (rows starting with ↳ HH:MM:SS). Render them verbatim — do not paraphrase snapshot titles (they're already one-line summaries). Display the snapshot: lines from LOAD_MANIFEST as bullet points under the most-recent session in the "Loaded into this conversation" output.
If unsummarized notes were upgraded in Step 2, also mention:
> Upgraded N session note(s) with AI summaries.
Step 4 — Show read-only context brief footer
Append to the brief written in Step 3:
> N open items in this project — run /check-items to triage.
Where N is the count of - [ ] items found while scanning sessions in Step 3. Use the open_items_total value already computed by the Python block in Step 3 (if not present, count by re-scanning the same notes). This step is read-only: do NOT prompt the user about individual items, do NOT compute candidate matches, and do NOT cite any session note.
If N == 0, omit the footer entirely.
Mark task #3 (the renamed final task) as completed and end.
Edge Cases
- No sessions found: Tell the user no session history was found for this project. Suggest they start a session and it will be logged automatically.
- No insights found: Omit the "Curated Insights" section. Mention: "No curated insights yet for this project."
- Very large vault (50+ sessions): Only grep, never glob the entire folder. Limit reads to the most recent 5 sessions + all insights.
- Config exists but vault path is invalid: Warn the user and suggest running
/obsidian-setupagain. - Open items exist: Do not attempt to check them off. Append the footer nudge pointing to
/check-itemsinstead.
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: abhattacherjee
- Source: abhattacherjee/claude-code-skills
- License: MIT
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet — be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.