A Claude Code skill that gives your AI persistent memory across sessions using plain Markdown files — Obsidian-vault friendly.
Requires Python 3 (stdlib only). Any writable directory works as the store; an iCloud-synced Obsidian vault is what the author uses. Legacy Apple Notes migration tools (macOS only) are included.
Claude Code sessions are stateless. Every time a session ends, all context vanishes. The next session starts from zero — no memory of what was done, what's in progress, or what decisions were made.
This skill writes structured Markdown handoff shards to a filesystem store before each session ends, and reads them back at the start of the next session via a SessionStart hook. It's like leaving yourself a sticky note, except the AI does it automatically.
v2.0 storage cutover — the backend switched from Apple Notes to plain Markdown files. If you used v1.x, see Migrating from Apple Notes.
Session ends
↓
┌───────────────────────────────────────────────┐
│ [Private] {root}/Active/{AgentID}.md │ ← per-agent working state
│ [Shared] {root}/Shared/{AgentID}.md │ ← cross-agent sync (per-agent shard)
└───────────────────────────────────────────────┘
↓ old content archived
┌───────────────────────────────────────────────┐
│ [Archive] {root}/Archive/{YYYY}/….md │ ← one file per session
└───────────────────────────────────────────────┘
↓ periodic consolidation
┌───────────────────────────────────────────────┐
│ [Long-term] MEMORY.md / episodic memory files │ ← distilled knowledge
└───────────────────────────────────────────────┘
Every file carries YAML frontmatter (schema_version / kind / agent / updated_at), generated and validated by the bundled scripts. Malformed files fail loudly instead of being silently mangled.
| Tier | Storage | Lifecycle |
|---|---|---|
| Active | Private + Shared shards | Overwritten each session |
| Archive | Archive/{YYYY}/ files |
Rolling, keeps last 5 entries |
| Long-term | MEMORY.md / episodic files | Permanent, distilled patterns |
If you run Claude Code on multiple machines (e.g., a laptop for interactive dev + a server for unattended tasks), each agent writes its own private shard and its own shared shard (Shared/{AgentID}.md) — no agent ever touches another agent's files. At session start, each agent's hook merges every Shared/*.md.
Single-agent mode is also supported — just skip the shared shard.
npx skills add fredchu/claude-session-handoffAny writable directory. Two common choices:
~/.agents/handoff— local only, zero dependencies- A folder inside an Obsidian vault on iCloud/Syncthing — synced across machines, browsable in Obsidian
mkdir -p ~/.agents/handoffCopy the example hook and set your agent name and root:
cp hooks/session-start.sh ~/.claude/hooks/session-start.sh
chmod +x ~/.claude/hooks/session-start.sh
# Edit AGENT_ID and HANDOFF_ROOT in the scriptAdd to .claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "~/.claude/hooks/session-start.sh"
}
]
}
]
}
}chmod is a Unix-only step.
Requires Python 3.9+ on PATH or available through the py launcher.
In File Explorer, copy hooks/session-start.ps1 to %USERPROFILE%\.claude\hooks\session-start.ps1, then edit $AgentId and $HandoffRoot in the copied script.
Add this to .claude/settings.json:
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{
"type": "command",
"command": "powershell.exe -NoProfile -ExecutionPolicy Bypass -File \"C:/Users/<you>/.claude/hooks/session-start.ps1\""
}
]
}
]
}
}Replace <you> with your Windows username. Use forward slashes in this command. Agent IDs may contain letters, digits, spaces, and hyphens. Avoid < > : " / \ | ? *, trailing dots or spaces, and reserved device names (CON, PRN, AUX, NUL, COM1–COM9, LPT1–LPT9).
The platform hook calls handoff_cli.py session-start, which prints the private shard plus every agent's shared shard (with a ⚠️ stale marker on shards older than --stale-days, default 14).
Add to your user-level CLAUDE.md (~/.claude/CLAUDE.md):
## Session Handoff Config
- Agent ID: Main
- Handoff root: ~/.agents/handoff
- Episodic dir: ~/.agents/memory/episodic
- Other Agents: (leave empty for single-agent mode)
- Private budget: 1500 chars
- Shared budget: 1000 chars
## Session Handoff Rules
- When user says "bye", "done", "handoff", "收工", or "結束" → run `/session-handoff`
- Do NOT skip even if "nothing was done" this sessionJust say "bye" or "handoff" at the end of your session. The skill will:
- Archive the previous handoff content (one frontmatter-tagged file per session)
- Reconcile a decision list first: every decision, deliverable, open item and pitfall from the session gets one line, and after writing the shards each line must appear in them or be explicitly dropped (running summaries lose facts most easily)
- Write new private + shared shards via
handoff_cli.py; a section that existed in the previous shard and is missing from the new one is refused unless you pass--drop '<heading>' - Consolidate automatically when the Archive reaches 5+ entries (old entries distilled into episodic memory)
- Extract lessons worth saving to long-term memory
The executing LLM never writes shard files by hand. Every write (Active / Shared / Archive) goes through scripts/handoff_cli.py, which handles frontmatter generation, schema validation, atomic writes, and path-escape protection:
python3 scripts/handoff_cli.py write --root "$ROOT" --kind active --agent "Main" --body-file /tmp/private.md
python3 scripts/handoff_cli.py write --root "$ROOT" --kind shared --agent "Main" --body-file /tmp/shared.md
python3 scripts/handoff_cli.py archive --root "$ROOT" --agent "Main" --session-id "20260726-topic" --slug "topic" --body-file /tmp/old.mdReads are just file reads. This design came from hard experience: letting the LLM improvise storage writes (the v1.x Apple Notes era) produced duplicate notes and mangled content; deterministic scripts fixed it at the source. Atomic writes also keep cloud sync from uploading half-written files.
This session's output
↓
Does another agent need to know?
├── Yes → Shared shard
└── No → Private shard
- Private: feature branches, environment-specific issues, this machine only
- Shared: cross-agent project state, user decisions, environment sync status
Shards are kept compact to minimize token usage when injected at session start:
| Shard | Budget |
|---|---|
| Private | 1500 chars |
| Shared | 1000 chars |
| Total injected | ~2500 chars |
- Git-diffable, greppable, no vendor lock-in
- Works with any sync layer (iCloud, Syncthing, git) and browsable in Obsidian
- Schema-validated frontmatter — corruption fails loudly
- No AppleScript flakiness, no HTML round-trip mangling (Apple Notes used to shred CJK bold headers into fragments)
- Cross-platform: the storage scripts are stdlib-only Python
Archive creation uses hardlinks (os.link), which can fail on OneDrive placeholder files or non-NTFS volumes. Atomic replacement (os.replace) can raise PermissionError while a sync client locks the target, so OneDrive is conditionally supported. On Windows, setting PYTHONUTF8=1 system-wide is recommended. The 2>> diagnostic log redirect uses different encodings in Windows PowerShell 5.1 and PowerShell 7; the log is diagnostic-only.
The repo keeps the legacy tooling for a one-time export:
python3 scripts/export_notes_to_markdown.py --root ~/.agents/handoff --agent "Main" \
--folder "Claude Workspace"This converts your existing Private / Shared / Archive notes into the Markdown store. After exporting, stop writing to Apple Notes entirely — the old notes become a frozen archive.
MIT