Every night before bed I read back through the day's Claude Code session logs. Once the sixteen articles for the day have shipped across my four blogs, the subagents have left dozens of "considerations" and "implementation notes" lying on disk. I used to paste these into Notion. Six months in, that approach broke: searches missed obvious results, and the results that did return were summaries that had quietly lost the original context.
I have been shipping iOS and Android apps as a solo developer since 2014. The wallpaper apps alone have crossed 50 million downloads, and these days I run four AI tech blogs through a Claude Code pipeline. On the app side, AdMob and App Store Connect data is pulled in every morning; on the site side, articles.json is regenerated on every build. Across both, "knowledge" needs to behave like a working asset rather than a junk drawer. What drew me to Karpathy's LLM Wiki idea, and to the Qiita article "Implementation: Claude Code × Obsidian Autonomous Knowledge OS '2do BRAIN'" that distilled it into something operational, is that they answered the question "how do you keep knowledge from rotting?" using directories and Skills alone.
This article walks through that three-layer storage and Skill set, reshaped to fit the realities of running four sites and six apps in parallel. If you are juggling several projects as an indie developer, I hope this is useful.
Why I Stopped Believing a Vector-DB RAG Would Be Enough
My first instinct was to throw the entire Obsidian vault into a Vector DB and let Claude Code pull from it via RAG. That is half right, and the other half quietly postpones the real problem.
You can absolutely re-vectorise raw material — PDFs, official docs, chat logs — and retrieve fragments on demand. But that only "searches knowledge in pieces"; it does nothing to "grow knowledge as structure" or "detect contradictions with what I wrote before". In a setup like mine, where the same concept (say, Claude Code Skills) is written about at different granularities across four sites, a Vector DB will cheerfully return the most recent article and miss the fact that it contradicts a judgement I made two months ago on another site.
The lever that fixes this is structural: force the AI to re-read the primary source before writing, and force it to record every change. Asking nicely inside a Skill prompt breaks down the moment the pipeline gets busy. Express the rule as a directory layout and YAML frontmatter, and both Claude Code and Obsidian's Dataview plugin can verify it mechanically.
Three-Layer Storage: Keep 01_raw, 02_wiki and Schema Strictly Separated
The core of the implementation is three directories at the root of the vault.
2do-brain/
├── 01_raw/ # Raw layer: immutable primary information (Source of Truth / Read-Only)
├── 02_wiki/ # Edited knowledge layer: structured knowledge AI edits (Write-Allowed)
│ ├── index.md # Catalogue: index of every page
│ └── log.md # History: ## [YYYY-MM-DD] operation | target
└── Schema/ # Discipline layer: the librarian's rules
├── CLAUDE.md # The constitution of the knowledge OS
└── .claude/
├── rules/ # Domain-specific rules
└── skills/ # Execution playbooks (covered below)
The purpose of strict separation is to stop recursive summarisation drift. If you let the AI re-summarise from existing wiki summaries, the important small print of the primary source gradually disappears. To prevent this, my Skills only see Read-class tools when they touch 01_raw/. Anything that mutates is off-limits there.
In my four-site setup, 01_raw/ holds official-doc PDFs from Anthropic / Google / Microsoft, snapshots of the Cloudflare Workers changelog, frozen copies of Stripe API references, and the GSC export CSV for each of the four sites. 02_wiki/ holds the synthesised notes — for example, "Pitfalls when rolling Cloudflare Workers + Next.js 16 across four sites" — assembled by Claude Code from those raw sources.
Schema/CLAUDE.md carries only a minimal rule set:
- Every claim must carry an inline citation in the form
[[01_raw/filename]]
- When updating an existing page, you must re-read the corresponding section of
01_raw/ before rewriting
- After any rewrite, you must append
## [YYYY-MM-DD] operation | target to 02_wiki/log.md
Three rules. Six months of running this vault, and contradictions still do not pile up.
ingest Skill: Read the Primary Source, Write Only a Draft
The first Skill is ingest, which loads any new document landing in 01_raw/. From the terminal: /ingest 01_raw/admob-reporting-v1beta.pdf, passing the target path as an argument.
The heart of .claude/skills/ingest/SKILL.md:
name: ingest
description: Reads new documents in 01_raw/, extracts entities, and produces a draft summary.
argument-hint: Target file path (e.g. 01_raw/sample.pdf)
arguments:
- name: target_path
description: Path of the primary information to extract from
required: true
allowed-tools:
- Read
- Grep
paths:
- 01_raw/*
The design point is the deliberately narrow allowed-tools. The Skill body says "save the extracted result to 02_wiki/_drafts/YYYY-MM-DD_[filename].md" — but any write tool is intentionally absent from the pre-approved list, so the draft-save step always pauses to ask the user to approve the write.
That tiny pause is the safety valve that lifts the quality of the extraction by one notch. In practice, whether I can take a breath at the 02_wiki/_drafts/ checkpoint has been the single best predictor of how well the subsequent compile will land.
compile Skill: Make the Integration Side Manual-Only on Purpose
The second Skill, compile, is the most important part of the design. It integrates the draft from ingest into the existing 02_wiki/ pages.
name: compile
disable-model-invocation: true
description: Integrates extracted draft information into existing 02_wiki/ pages, updating sources and metadata.
argument-hint: Path of the source draft (e.g. 02_wiki/_drafts/draft.md)
arguments:
- name: draft_path
description: Path of the draft file produced by Ingest
required: true
allowed-tools:
- Read
- Grep
- Edit
- Bash(git add 02_wiki/*)
The single line that matters most is disable-model-invocation: true. With this set, the Skill is no longer pre-loaded into Claude's context or visible to subagents. It is invisible to the AI side — it will only fire when a human types /compile explicitly.
The reason to lock it down this tightly is that compile writes directly into 02_wiki/ itself. Across my four sites, 02_wiki/ accumulates notes like "Five filters for selecting Claude Code Skills". If those get silently overwritten, the next article-generation run will read the degraded note as context, and quality drift propagates outward. Auto-invocation is fine in toy setups; in production, requiring a human to confirm the draft before triggering the merge is a line I am not willing to cross.
Another small but important detail is Bash(git add 02_wiki/*). Rather than handing over unrestricted Bash, the command is scoped down to a specific verb and path. Anthropic's Skill documentation recommends exactly this practice, and in my setup any git push-equivalent stays squarely on the human side.
The body of the Skill enforces three non-negotiable steps:
# Wiki Compile workflow
Target draft: $draft_path
1. When updating an existing page, re-read the corresponding section of 01_raw/ to confirm facts and prevent recursive summarisation drift
2. Every claim must carry an inline citation in the form [[01_raw/filename]]
3. The last action must update both 02_wiki/index.md and 02_wiki/log.md
Bundling examples/output_template.md next to the SKILL.md as a few-shot example also helped. On my sites the YAML did not stabilise on the three keys updated_at / sources / status until I added that template — without it, the format kept drifting. The extra file is worth it.
Use Dataview to Permanently Watch "Pages Not Yet Reviewed"
By this point, pages with YAML frontmatter are accumulating in the vault. Querying them dynamically with Obsidian's Dataview plugin immediately surfaces "pages the AI has written but a human has not yet reviewed".
```dataview
TABLE updated_at, status
FROM "02_wiki"
WHERE status != "reviewed"
SORT updated_at ASC
```
I pinned this query to the home note of the vault and check the unreviewed count every morning. When the number creeps past 10 in a single week, that is a signal that compile precision is slipping, and it is time to revisit the Skill prompt and examples/output_template.md. Over eight weeks of running this, keeping the number in the 5–10 range has correlated tightly with stable article generation across the four sites.
What I Changed When Mapping It to Four-Site Operations
The structure above is essentially the Qiita article's design. Three things I changed to fit my own setup:
First, 01_raw/ is split by site. The primary information needed for Claude Lab / Gemini Lab / Antigravity Lab / Rork Lab rarely overlaps, so it lives in 01_raw/claudelab/, 01_raw/gemilab/, etc. The ingest Skill's paths field accepts 01_raw/{site}/*.
Second, 02_wiki/ carries a separate folder for cross-site notes. Decisions that span sites — "shared design for rolling Stripe membership across four sites", for example — live under 02_wiki/_cross/. Without that bucket, the same decision ends up duplicated four times, and contradictions become inevitable.
Third, there is a weekly-audit Skill that runs on Sunday morning via cron. Triggered as /weekly-audit, it pulls the last seven days from 02_wiki/log.md, joins the Dataview unreviewed-count query, and writes a single Markdown summary. Looking back at those summaries month by month makes update-frequency imbalance across the four sites obvious in a way that no dashboard ever did.
Six Operating Rules That Settled After Eight Weeks
Implementation alone is hard to visualise, so here are the rules that crystallised over eight weeks of running this setup.
- Where possible, every file in
01_raw/ should carry the original URL and a hash in its metadata. For pages that update dynamically (like the Cloudflare changelog), overwrite with a monthly snapshot
- Aim for 1,500–2,500 characters per note in
02_wiki/. Longer than that, and compile should be instructed to split
- Never grant
compile unrestricted Bash. Scope every command down to the verbs and paths it actually needs
- If Dataview's unreviewed count tops 20 for three consecutive weeks, rewrite the Skill prompt from scratch
- Cross-site notes should be re-validated against the actual state of the four sites 30 days after first being written
- Update
Schema/CLAUDE.md no more than once a quarter. Frequent edits dissolve the vault's overall coherence
Rule four has saved me more than once. "Skill output is feeling thinner lately" is a vague intuition; the Dataview number turns it into a signal you can act on early.
A Working Brain, Not a Searchable Trash Bin
I have written a lot of implementation detail, but the underlying motivation is simple. I got tired of re-explaining context to an AI every time, and tired of the way every re-explanation drifted slightly from the last.
The freedom of Markdown plus directories is valuable because it does not chain you to any one SaaS. Layer on top of that the discipline of Claude Code's SKILL.md spec used properly, and knowledge starts compounding into a real asset. My four sites are only eight weeks into this layout, and already "what I was thinking about today" can be picked up by next week's me without ceremony.
You can start tomorrow. Open a terminal, run claude, and try /ingest 01_raw/your-document.md. The moment it starts moving, it becomes obvious what this design is actually doing for you.
I hope this is useful to anyone working on the same problem.