---
title: Weekly Project Review with Claude Code and Obsidian CLI
description: Scan all open Obsidian projects, flag stale ones, and triage each — keep, hold, drop, or done. A Claude Code skill built on tag-based GTD.
publishDate: 2026-03-04
canonical: https://www.mandalivia.com/obsidian/weekly-project-review-with-claude-code-and-obsidian-cli/
---

I manage projects in Obsidian using a GTD-inspired system: each project is a markdown file with a standard structure, status managed through tags, and activity tracked through dated log entries. It works — until you have a dozen open projects and no systematic way to review them as a portfolio. Individual project catchups keep each one moving, but nothing tells you which projects have gone quiet, which have no next actions, or which should have been put on hold weeks ago.

So I built a weekly review skill for Claude Code that scans every open project, classifies it, and walks me through triage decisions.

## How Projects Work in the Vault

Every project starts from a Templater template. The frontmatter carries the metadata that matters:

```yaml
tags:
  - Projects/Open       # status: Open, Inbox, Hold, Dropped, Done
area: next-act           # life area the project belongs to
review-cycle: 7          # days before the project is considered stale
start-date: 2026-01-15
complete-date:            # set when marked done
```

The body follows GTD's natural planning model:

- **Purpose** — why this project exists
- **Outcome** — what "done" looks like
- **Current Status** — the bookmark, updated each session with `As of [YYYY-MM-DD](/obsidian/yyyy-mm-dd/)`
- **Task Library** — all tasks in Obsidian Tasks format (`- [ ]` checkboxes)
- **Log** — chronological entries with `### [YYYY-MM-DD](/obsidian/yyyy-mm-dd/)` headings, newest first

Status flows through tags: a project starts as `Projects/Inbox`, gets promoted to `Projects/Open` when you commit to it, and eventually moves to `Projects/Done`, `Projects/Hold`, or `Projects/Dropped`. The tag is the single source of truth — Dataview queries, the Obsidian CLI, and the weekly review all key off it.

## The Problem with Per-Project Catchups

I already had a `/catchup` skill that reads a single project file, asks what's happened since the last log entry, updates status, and suggests a next action. It works well for the project you're thinking about. It can't tell you what you're *not* thinking about — the project that quietly went stale three weeks ago while you were focused elsewhere.

A weekly review needs to see the whole portfolio at once.

## What the Weekly Review Skill Does

The skill runs in four phases.

### Phase 1: Gather

A shell script pulls all `Projects/Open` and `Projects/Inbox` files via the Obsidian CLI, then reads each file directly to extract:

- Last log date (from `### [YYYY-MM-DD](/obsidian/yyyy-mm-dd/)` headings)
- Count of incomplete tasks (`- [ ]` lines)
- The `review-cycle` and `area` properties from frontmatter
- Whether Current Status has meaningful content

It classifies each project:

| Classification | Criteria |
|---|---|
| **Active** | Log entry within review-cycle, has open tasks |
| **Stale** | Last log older than review-cycle days |
| **No next actions** | No incomplete tasks, project isn't done |
| **Inbox** | Still in Projects/Inbox, needs initial decision |

A project can be both stale and have no next actions — the script flags both. The output is JSON, one object per project with all the data the LLM needs to present a summary without further file reads.

### Phase 2: Present

The summary groups projects by classification. Something like:

```
Active (2)
  Home renovation — today, 3 tasks
  Launch blog — 2 days ago, 5 tasks

Stale (4)
  Spanish taxes — Jan 7, 55 days, no tasks
  Investment rebalance — Feb 16, 15 days, no tasks
  ...

Inbox (2)
  Gmail integration — no log, no tasks
  Conference prep — no log, no tasks
```

Each project shows its area, last log date, days since activity, and open task count. Active projects are listed for awareness; flagged projects are what you'll triage.

### Phase 3: Triage

Walks through flagged projects one at a time, starting with inbox, then stale, then no-next-actions. For each:

- **Keep open** — define a next physical action, add a log entry
- **Hold** — swap the tag to `Projects/Hold`, optionally set a revisit date
- **Drop** — swap to `Projects/Dropped`, add a closing log entry
- **Done** — swap to `Projects/Done`, set `complete-date`, log it

Tag changes preserve all non-status tags. If a project is tagged `[Projects/Open, Area/Work]`, only the status tag gets swapped — area tags stay. The Obsidian CLI handles property writes:

```bash
obsidian vault=MyVault property:set name=tags value="Projects/Hold" type=list path=<file>
```

Log entries follow the vault's date-linking convention:

```markdown
### [2026-03-04](/obsidian/2026-03-04/)
- Moved to hold during weekly review. Revisiting after tax season.
```

### Phase 4: Wrap Up

Summarizes what changed — how many moved to hold, how many dropped, how many got new next actions — and reports how many projects remain open.

## The Gather Script

The key design decision was moving data extraction into a shell script rather than having the LLM do it inline. The first run of the skill took multiple LLM turns: calling the Obsidian CLI for file lists, then grepping each file individually, then classifying results. Slow, token-heavy, and inconsistent across runs.

The script does it in one call:

1. Calls `obsidian vault=MyVault tag name=Projects/Open verbose` to get file paths (one CLI call per status tag — two total)
2. Reads each file directly with grep and awk for log dates, task counts, and current status content
3. Extracts properties from YAML frontmatter directly — no per-file CLI calls
4. Classifies each project and outputs a JSON array

```json
[
  {
    "name": "Investment rebalance",
    "path": "Planner/Projects/PR-2025-Reorient investments.md",
    "status_tag": "open",
    "area": "finance",
    "last_log": "2026-02-16",
    "days_ago": 15,
    "review_cycle": 7,
    "open_tasks": 0,
    "has_current_status": false,
    "classification": "stale,no-next-actions"
  }
]
```

The LLM consumes the JSON, formats the summary, and moves straight to triage. Phase 1 went from roughly four tool calls to one.

### Why Not Use the Obsidian CLI for Everything?

The Obsidian CLI's `property:read` command reads one property from one file at a time. For twelve projects and three properties each, that's 36 CLI calls — each launching a process, connecting to the running Obsidian instance, and returning a value. Reading the file directly with grep is an order of magnitude faster.

The CLI is still the right tool for *writing* properties during triage (it handles YAML serialization correctly) and for the initial file listing (it resolves tags through Obsidian's index). The split is: CLI for tag lookups and property writes, direct file reads for bulk data extraction.

## Skill File Structure

The skill follows a pattern shared by other Claude Code skills in the vault:

```
.claude/skills/weeklyreview/
├── SKILL.md                        # Thin wrapper: name, description, pointer to prompt
└── scripts/
    └── gather_projects.sh          # Data extraction + classification

Planner/Prompts/
└── SKILLPROMPT-WeeklyReview.md     # Full workflow instructions
```

The `SKILL.md` is what Claude Code registers as a slash command. It points to the prompt file, which contains the detailed phase-by-phase instructions. Keeping the prompt in the vault (not buried in `.claude/`) means it's editable in Obsidian alongside other notes.

## What I Haven't Built Yet

The triage phase is designed but only lightly tested. The tag-swap logic — read existing tags, find the project status tag, replace only that one, write back the full list — is where things tend to get fiddly with YAML list serialization.

I also want this to feed into a weekly note eventually: a summary of what moved, what's still open, maybe a portfolio health score over time. That's a separate piece.

## Caveats

**Obsidian CLI required.** The gather script and triage actions depend on the [Obsidian CLI](https://github.com/Obsidian-CLI/obsidian-cli) being installed and Obsidian running. Without it, you'd need to parse frontmatter and write properties through direct file manipulation — doable but messier.

**Review-cycle defaults matter.** If a project doesn't set `review-cycle` in frontmatter, the script defaults to 14 days. For fast-moving projects that's too generous; for slow-burn research projects it's too aggressive. Set it per-project.

**Template discipline.** The whole system depends on projects following the template structure — specifically `### [YYYY-MM-DD](/obsidian/yyyy-mm-dd/)` log headings and `- [ ]` task format. Projects that predate the template or use different conventions show up as stale with zero tasks regardless of actual activity.

**Environment variable for vault path.** The vault path (especially on macOS with iCloud sync) is long and awkward. Setting an `$OBSIDIAN_VAULT` environment variable in your shell config and referencing it in scripts keeps paths manageable. The gather script falls back to a hardcoded default if the variable isn't set.

## Related

- [Semantic Search for Your Obsidian Vault](/obsidian/semantic-search-for-your-obsidian-vault-what-i-tried-and-what-worked/)
- [Syncing Things Tasks into Daily Notes](/obsidian/syncing-completed-things-3-tasks-into-obsidian-daily-notes/)