Synthesizing Obsidian periodic notes with Claude Code

Published

Screenshot of a generated weekly note staged before being moved into an Obsidian vault.

Overview

I keep a daily note in Obsidian, but I’d never reliably sit down and roll those daily notes up into a weekly summary, let alone a monthly, quarterly, or annual one — it’s the kind of synthesis that’s valuable in hindsight and easy to skip in the moment. periodic_notes uses Claude Code to write that rollup for me, built from the notes one level below it: a week from its 7 daily notes, a month from the weekly notes whose Monday falls within it, a quarter from its 3 monthly notes, a year from its 4 quarterly notes.

What it does

Two scripts split the work: generate_periodic_notes.py gathers the child notes for a given period and handles the date math, and synthesize_periodic_note.py is the orchestrator — it determines the target period, stages the source notes, invokes claude non-interactively to write the synthesis, then moves the finished note into the vault.

periodic                                  # last week (default)
periodic --type month 2026-06             # a specific month
periodic --type month 2026-06 --dry-run   # inspect before writing to the vault

With no date given, each --type defaults to the most recently completed period of that type.

Why it’s built this way

Claude never touches the live vault. Source notes are copied to a staging directory first; Claude is invoked with that staging directory as its working directory and a restricted tool list, so its filesystem access literally cannot reach the real vault. The only write against the vault is a single shutil.move of the finished note — one line in the whole pipeline that can do damage, and it’s gated behind a check that refuses to overwrite an existing file. --dry-run skips that move entirely, leaving the result in staging for review. When you’re handing an LLM write access to years of personal journal entries, the safety property you actually want isn’t “it’s usually careful” — it’s “it’s structurally unable to touch the thing you care about.”

No task-manager integration, on purpose. An earlier version called a TickTick MCP server to auto-populate the “Completed tasks” section. It was removed — not because it didn’t work, but because the actual workflow was pasting tasks in manually anyway, and the integration added a personal-service dependency for a problem that wasn’t really there.

No tags: field, on purpose. Claude wasn’t reliably adding it even when instructed to, so rather than have a flaky field, the script drops it entirely. Sometimes the right fix for an unreliable LLM behavior is to stop asking for it rather than to keep prompting harder.

A gotcha worth knowing

CLI date validation is strict on purpose: --type month requires zero-padded YYYY-MM (2026-06, not 2026-6). A prior version accepted the unpadded form, and that laxness once turned a “let me test some bad input” run into a real pipeline execution, because the “bad” input was silently valid. If you’re testing that a validator rejects something, actually confirm the rejection — don’t assume a shape looks malformed just because it looks unusual.

Try it yourself

You’ll need Python 3.10+, the Claude Code CLI installed and authenticated, and an Obsidian vault using the Journals plugin with a specific folder layout. Full setup, the vault layout, and an optional man periodic page are in the README.

codeberg.org/splitsubdued/periodic_notes

Thanks for reading. Questions or corrections are welcome at jlmc.space@gmail.com. If this was helpful and you'd like to support the site, a coffee is always appreciated.

Buy Me A Coffee Buy Me A Coffee