Working through Least Privilege with Obsidian

Working through Least Privilege with Obsidian
Not every key should be used

I've been using Obsidian as a note taking application for years, and until recently, my integration with Claude has been through the file system integration, which, for the most part works pretty well. It was incredibly handy at conferences, for example, for Claude to search and document a term or concept I wanted more information on, but didn't want to stop taking notes. This was, for me, the promise of what Claude as assistant was supposed to do. "Dig into this concept for me and make a quick note with links" gave me an extra pair of hands that added a whole new dimension to what I could get out of seminars.

Researching was great, but searching within the Obsidian vault, even though it was a directory of markdown files, was very token intensive and extremely slow. The slowness was all the more frustrating because I could hop over to Obsidian, type in the search term and get a list of files while Claude was still churning.

I've been meaning to write an MCP server to make searching my larger Obsidian vaults less token intensive. By the time I got around to it, someone had already written a pretty excellent MCP server for Obsidian and made it part of their REST API.

A Wide Variety of Tools

The MCP server is incredibly robust, and this list actually sells it short when you realize that the command list tool provides access to over two dozen commands.

Tool Description
vault_list List files and subdirectories inside a vault directory
vault_read Read a file's content, frontmatter, tags, and stat
vault_write Create or overwrite a vault file
vault_append Append content to the end of a vault file
vault_patch Patch a specific heading, block reference, or frontmatter field
vault_delete Delete a vault file
vault_move Move (rename) a vault file to a new path
vault_get_document_map List the headings, block references, and frontmatter fields in a file
active_file_get_path Return the vault path of the file currently open in Obsidian
periodic_note_get_path Return the vault path of the current periodic note (daily, weekly, monthly, quarterly, yearly)
search_query Search using a JsonLogic query against note metadata
search_simple Full-text search using Obsidian's built-in search
tag_list List all tags across the vault with usage counts
command_list List all registered Obsidian commands
command_execute Execute an Obsidian command by ID
open_file Open a file in the Obsidian UI

For someone that just wanted search, that's a lot of capabilities -- a lot of very destructive capabilities that would require serious recovery. Cool capabilities, but capabilities that I didn't need. The rare times that I needed writing, filesystem access was good enough.

Something else -- I really liked the idea of open_file. I could ask to search for items that referred to a topic and they could pop up in Obsidian. However, there was an interesting, very wiki-like feature of this command. If the file that you asked to open didn't exist, it got created. Handy in certain circumstances, just not in mine.

So, I had a choice. I could create a skill which would prevent these features from being used. A bold choice when context gets long and that "do not" is many many tokens, and weights away. I didn't trust this when things like deleting a vault were on the line.

I could encapsulate the MCP server -- create an MCP server which would call the MCP server and only expose the tools I wanted to expose. If the API hadn't been so well documented, I might have gone this route. I could expose the features I wanted and the features I didn't were hidden. But it meant another MCP server and a communication hop I didn't need.

Ultimately, I wound up creating my own MCP server to expose the features I wanted, and had an open_file that wouldn't create new files. It sped up my searches tremendously, and I found myself using a lot fewer tokens to get what I wanted. My server instead offered:

  • vault_list
  • vault_read
  • vault_get_document_map
  • active_file_get_path
  • tag_list
  • search_simple
  • search_query
  • open_file (with the fixed behavior — verifies file exists first, won't create)

I used go because I was using Kit from mark3labs -- a lightweight AI agent with a Go SDK -- as part of a larger failed experiment using a local 'librarian' model to make Claude even more economical. I ultimately gave up on that, though the semantic search component survived as its own server used by Claude directly. A delve into the world of over-engineering that is worth its own article.

Initial results have been extremely promising. Initially, I thought that I'd need a skill for JsonLogic syntax, but adding examples in the tool description itself seemed to be sufficient (for now).

To be honest, there's still a lot to do here. Working with multiple vaults is a challenge. I haven't found a way to know which vault is considered active by the API, which means I either have to change keys, or tell the server which vault I'm working with — and a mismatch produces authentication errors. As long as I set this up before a conference, it's more of an inconvenience than an obstacle. But aside from that, I can be comfortable that a mistyped prompt won't wind up nuking my entire vault or a hallucination creates dozens of files. To me, that's what least privilege is about: granting the capabilities you want, and leaving the risk behind.