← Loss of Function
AIproductivityknowledge-managementobsidianclaude-codeprompts

Building an Obsidian Vault Intake Pipeline with Claude Code

By David Byas-Smith·~1,511 tokens
Building an Obsidian Vault Intake Pipeline with Claude Code

The Idea

A bash script + Claude Code CLI pipeline that watches an intake/ folder, automatically processing markdown files into an Obsidian vault — complete with frontmatter, tags, and wikilinks.

Drop a file, run one command, and Claude does the rest: reads the content, classifies it, creates or updates vault notes, links entities, and archives the source file.

The Problem

A Claude-managed vault is great for notes created during live conversations. But there's no good way to batch-process existing markdown files — meeting notes, brain dumps, exported docs — into the vault. An intake pipeline bridges the gap between "stuff you've written down" and "organized vault knowledge."

Architecture

Components

  1. intake/ folder — lives outside the vault so Obsidian doesn't index it
  2. intake.sh — the bash orchestration script
  3. intake-prompt.md — the system prompt that instructs Claude how to process each file

Flow

intake/meeting-notes.md
        │
        ▼
  ./intake.sh intake/meeting-notes.md
        │
        ▼
  claude -p (reads file, queries vault state, creates/updates notes)
        │
        ▼
  vault/ gets new/updated notes with links
        │
        ▼
  intake/processed/2026-03-09-meeting-notes.md (source archived)

What Each File Produces

Each intake file generates:

  1. One primary dated note — the full content, classified and formatted
  2. Multiple entity notes — stub or updated notes for people, projects, and ideas extracted from the content

Example: a meeting notes file produces:

  • Notes/2026-03-09 Call with Alex.md (primary note)
  • People/Alex Johnson.md (stub, tagged meta/follow-up)
  • Updated Projects/SomeProject.md (appended under a dated heading)

The Script

#!/bin/bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
INTAKE_DIR="$SCRIPT_DIR/intake"
PROCESSED_DIR="$INTAKE_DIR/processed"
PROMPT_FILE="$SCRIPT_DIR/intake-prompt.md"
DATE=$(date +%Y-%m-%d)

# Pre-flight checks
if [[ -n "${CLAUDECODE:-}" ]]; then
  echo "Error: Cannot run intake from within a Claude Code session."
  exit 1
fi

if ! obsidian files total vault="YOUR_VAULT" &>/dev/null; then
  echo "Error: Obsidian is not running or vault is not accessible."
  exit 1
fi

if [[ ! -f "$PROMPT_FILE" ]]; then
  echo "Error: Intake prompt not found at $PROMPT_FILE"
  exit 1
fi

if [[ $# -eq 0 ]]; then
  echo "Usage: ./intake.sh <file1.md> [file2.md] ..."
  exit 1
fi

mkdir -p "$PROCESSED_DIR"

for file in "$@"; do
  if [[ ! -f "$file" ]]; then echo "SKIP: $file does not exist"; continue; fi
  if [[ ! "$file" == *.md ]]; then echo "SKIP: $file is not a .md file"; continue; fi
  if [[ ! -s "$file" ]]; then echo "SKIP: $file is empty"; continue; fi

  filename=$(basename "$file")
  echo "Processing: $filename"

  if cat "$file" | claude -p \
    "Process this intake file into my Obsidian vault. The file is named: $filename" \
    --append-system-prompt-file "$PROMPT_FILE" \
    --allowedTools "Bash(obsidian *)" \
    --max-turns 25 \
    --max-budget-usd 1.00 \
    --model sonnet \
    --no-session-persistence; then
    mv "$file" "$PROCESSED_DIR/${DATE}-${filename}"
    echo "DONE: $filename -> processed/${DATE}-${filename}"
  else
    echo "FAIL: $filename (left in intake/)"
  fi
done

echo "Intake complete."

The System Prompt (intake-prompt.md)

This is where the intelligence lives. It instructs Claude to:

  1. Discover vault state first — query existing files and search for entities mentioned in the input before doing anything
  2. Classify the content — meeting, brain dump, project update, idea, decision, or reference
  3. Create the primary note — with proper frontmatter, tags from your taxonomy, and wikilinks
  4. Extract entities — identify people, projects, and distinct ideas
  5. For each entity:
    • Search the vault to see if it already exists
    • If it exists: append under a ## Update YYYY-MM-DD heading
    • If new: create a stub note with meta/follow-up tag
  6. Cross-link everythingrelated properties and inline [[wikilinks]]
  7. Print a summary of actions taken

The prompt should include your full tag taxonomy, frontmatter templates per note type, naming conventions, and entity resolution rules (always search before creating).


Key Design Decisions

Why claude -p (headless mode)? No interactive session needed — each file is a one-shot operation with a defined output.

Why sequential, not parallel? Files are processed one at a time so vault state stays consistent between files. If two files both mention the same new person, the second file will find the stub note the first file created.

Why --allowedTools "Bash(obsidian *)"? Locks Claude to only the Obsidian CLI and file reads. No arbitrary shell commands.

The CLAUDECODE guard: You can't invoke claude -p from within a Claude Code session (the env var is set). The script checks this upfront and exits with a clear error.

Obsidian must be running: The pipeline uses the Obsidian CLI as the single source of truth for vault writes rather than direct filesystem operations. That's a hard dependency — the script validates it before processing anything.


Edge Cases Worth Handling

  • Existing frontmatter — treat as hints, apply vault conventions, preserve extra fields
  • Duplicate detection — search before creating; if a very similar note exists, append instead
  • Partial failure — created notes persist, source file stays in intake/ for retry
  • Large files — warn if file exceeds a reasonable size threshold

What It Costs

Each file runs ~$0.05–0.30 with Sonnet depending on length and how many vault queries Claude makes. The --max-budget-usd flag provides a per-file cost cap as a safety net.


The Result

One command turns a pile of unprocessed markdown into a linked, tagged, navigable vault. The value compounds — every entity note created becomes a connection point for future intake files.