← All Posts
claudeaiengineeringproductivity

Obsidian as Claude Code's Second Brain: Structured Retrieval Beats Context Stuffing

Claude Code forgets everything between sessions. Obsidian's MCP server fixes that — not by remembering more, but by retrieving exactly the right note on demand.

Obsidian as Claude Code's Second Brain: Structured Retrieval Beats Context Stuffing

Obsidian as Claude Code's Second Brain: Structured Retrieval Beats Context Stuffing

You open Claude Code a few days after the last session on a project. The terminal is ready, the repo hasn't changed much — everything should pick up exactly where you left off.

It doesn't. The assistant has zero memory of any of it: the architecture decisions, the conventions you agreed on, the reasons behind them. You start re-explaining from scratch. This is the third time this month you've had this exact conversation.

This isn't a context-window problem. It's a retrieval problem.


Why a Vault, Not a Monolith

The obvious fix is a project instructions file — CLAUDE.md, a README, whatever your tool calls it. And it works, for a while. Then it only grows. A style guide. An SEO architecture. A design-asset workflow. Every new convention is one more paragraph in the same file, because there's nowhere else for it to go.

Here's the problem that creates: editing one line in a 7.4KB CLAUDE.md — say, adding a sentence to the SEO section — doesn't lower the cost of the next session. Claude still loads the entire file, whether the change was relevant to the task at hand or not. The file doesn't get cheaper to read as it gets more specific. It gets more expensive as it gets more complete.

A vault of many small notes doesn't have that failure mode, because each note is addressable. A session can fetch exactly blog/Writing-Style.md without touching blog/Posts.md, without touching anything else in the vault. And because it's plain markdown with YAML frontmatter, a human opening it in Obsidian and an agent calling a vault_read tool see the literal same file — no separate index to keep in sync, no translation layer between "the docs" and "what the agent knows."

The monolith's real cost was never disk space. It was that every read was billed at the size of the whole file, never the size of what the task actually needed.


Retrieval, Not Recall: How MCP Actually Fetches Data

How MCP actually fetches data — Claude Code as an MCP client calling a Local REST API plugin over markdown files, not a vector-embedding pipeline

The mechanism behind this is unglamorous, which is exactly why it works. Obsidian's Local REST API community plugin runs an HTTP server on localhost — port 27123 for plain HTTP, 27124 for HTTPS with a self-signed cert. Claude Code connects to it as an MCP client, declared in a project-level .mcp.json:

{
  "mcpServers": {
    "obsidian": {
      "type": "http",
      "url": "http://127.0.0.1:27123/mcp/",
      "headers": {
        "Authorization": "Bearer <api-key>"
      }
    }
  }
}

Once connected, the server exposes a small set of typed tools: vault_read, vault_write, vault_list, search_query, tag_list. That's the entire surface area.

Claude Code  ──MCP──►  Local REST API  ──►  Vault (markdown files on disk)
   (client)              (plugin, :27123)      blog/Writing-Style.md
                                                blog/Posts.md
                                                blog/Drafts/...

Here's the claim worth stating plainly: this is not vector-embedding RAG. There's no embedding model, no vector database, no nearest-neighbor search over chunked documents. It's a REST API in front of a folder of markdown files. Deliberately dumb, and that's the point — it's reliable and inspectable in a way a similarity-search pipeline isn't. There's no embedding drift to debug, no chunk boundary that accidentally split the fact you needed from the sentence that explained it.

The tradeoff is real: you lose fuzzy semantic match — "find something about X even if it's worded differently" doesn't work here the way it does with embeddings. What you gain is determinism. You can look at exactly what a tool call returned and know exactly why it returned that and nothing else.


The Token Math: O(1) vs O(n)

Context-stuffing cost scales linearly with vault size; targeted retrieval cost stays flat regardless of vault size

Numbers from this exact repo: CLAUDE.md is about 7.4KB. blog/Writing-Style.md is about 4.2KB.

If the workflow were "paste the whole vault into context on every turn," the cost would scale with total vault size. Fine at three notes. Unworkable at a hundred, once the vault has grown to cover every post's status, every style decision, every project note you've ever written down.

Targeted retrieval doesn't have that curve. When the writer skill needs the voice profile, it calls vault_read on exactly blog/Writing-Style.md — one note, 4.2KB, regardless of whether the vault has ten notes or ten thousand.

One caveat keeps this honest, and it matters: the server-side search inside the Local REST API still scans across notes when you call search_query — that part is O(n), it's just fast, because it's a local filesystem lookup, not a distributed query across a cluster. What's actually flat is the token cost paid by Claude's context window, because only the matched note comes back into the conversation, never the vault. The claim is about context budget, not about the underlying search algorithm's complexity — conflating the two is exactly the kind of oversimplification that doesn't survive scrutiny.

The practical result stands regardless: a growing knowledge base doesn't degrade the assistant's per-turn cost, as long as retrieval stays targeted instead of loaded up front.


Three Traps That Will Bite You

Getting the connection working the first time surfaces exactly these, in order.

1. Self-signed cert on the HTTPS port

The plugin's default port, 27124, serves HTTPS with a certificate it generated itself. Claude Code's MCP client — a standard Node.js HTTPS client — rejects it outright:

Failed to reconnect to obsidian: DEPTH_ZERO_SELF_SIGNED_CERT at https://127.0.0.1:27124/mcp/

The tempting fix is to set NODE_TLS_REJECT_UNAUTHORIZED=0 and move on. Don't. That disables certificate validation for every HTTPS connection the process makes, not just this one local server.

Fix: switch to the plugin's separate non-encrypted HTTP port instead. Both endpoints are 127.0.0.1 — localhost never left the machine either way.

2. Connection refused on the "safe" port

Point .mcp.json at http://127.0.0.1:27123/mcp/ and it can still fail:

Failed to reconnect to obsidian: ConnectionRefused at http://127.0.0.1:27123/mcp/

Nothing was listening there. The Local REST API plugin ships with "Enable Non-encrypted HTTP Server" turned off by default — the HTTPS port is the one that's live out of the box.

Fix: open the plugin's own settings page in Obsidian and confirm which port is actually enabled before assuming the documented default is live.

3. A real secret sitting in a tracked file

The working config has an actual bearer token hardcoded in .mcp.json. Commit that file as-is, and the token ships in git history — readable by anyone who ever clones the repo, forever, even if the file is deleted in a later commit.

Fix: add .mcp.json to .gitignore. If the config needs to be shared across a team, use ${ENV_VAR} substitution instead of a literal token in the file.


Where This Fits: Three Tiers of Memory

Three tiers of memory — CLAUDE.md owned by the repo, assistant memory owned by the agent, Obsidian vault owned by the human

None of this replaces CLAUDE.md, and it doesn't replace an assistant's own cross-conversation memory either. The three exist together, each with a different owner:

TierOwned byScopePersists across
CLAUDE.mdThe repo (git-versioned)Static rules, shipped with the codeEvery clone, every session
Assistant memoryThe agentCross-conversation facts about this user and projectThis assistant, this user
Obsidian vaultThe humanArbitrarily large knowledge baseForever, independent of any single tool

The decision rule follows directly from ownership: a rule that should apply to anyone who clones the repo belongs in CLAUDE.md. A fact about how this specific person likes to work belongs in assistant memory. Open-ended, growing knowledge — post status, a writing-style profile, running project notes — belongs in the vault, where it can keep growing without ever making a session more expensive to start.


What You Give Up: The Drift Problem

What belongs where — cheap-to-rederive facts get read from source, expensive-to-derive investigation findings get cached in the vault

The vault's biggest weakness isn't performance. It's trust — an agent doesn't know a note is wrong until it's already acted on it.

CLAUDE.md sits in the same git history as the code it describes. A pull request that changes behavior can carry the CLAUDE.md diff in the same review — the process that changes the code has a built-in chance to keep the docs honest. A note in Obsidian has no such enforcement. Nothing forces blog/Writing-Style.md to update the day the actual writing style shifts. Nothing forces blog/Posts.md to reflect reality if someone edits data.json by hand and skips the sync step. An agent reading a stale note trusts it exactly as much as it would trust something true, and acts on it with the same confidence.

That doesn't mean facts derived from source code belong nowhere near the vault, though. The real question was never "is this about the code" — it's what does re-deriving this fact cost, right now, against the risk it goes stale before it's needed again.

Two categories behave completely differently:

  • Cheap to re-derive, fast-changing. A function's current signature. What an endpoint returns today. Reading the source is close to free and always correct. Caching this in a note buys nothing and risks drift for a savings of zero.
  • Expensive to derive, comparatively stable. The actual root cause behind a workaround that looks wrong until you know why it's there. Why a Lambda timeout is set to exactly 29 seconds. The half-formed findings from a debugging session that spans three separate days of npm run dev. Re-deriving these from scratch every session is the exact tax this whole architecture exists to avoid paying twice.

The mitigation for drift on that second category isn't to avoid writing it down — it's to stamp it. Note the date or the commit it was true as of. And treat a surprising disagreement between the note and the current code as a signal to go re-investigate, not as something to quietly overwrite and forget happened.

Cache the hour it took to find out why. Don't bother caching the five seconds it takes to read what.


Closing

A second brain's job was never to hold everything in view at once. It's to know exactly where to reach.

That's the whole architecture: not remembering more, but retrieving precisely.