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)
- Write new private + shared shards via
handoff_cli.py - 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