Your notes stay Markdown.
The agent learns to read them.
secondbrain is an MCP server that gives an LLM structured access to a knowledge base of plain files on disk. The notes are the product; this program is a lens over them. Delete the container and you still have a directory you can open in Obsidian, grep, or commit.
The problem
Two wrong shapes for a memory
A chat assistant with no memory starts every conversation from nothing, so the same context gets pasted in again and again and the useful parts of it are lost when the window closes. The obvious fix — put the memory inside the assistant — produces the other wrong shape: a store you can only reach through the product that owns it.
A knowledge base you cannot read without its software is a hostage, not an asset. The test is simple: if the vendor disappears tomorrow, or the container will not start, what is left? With secondbrain what is left is a directory of Markdown files with the same names, the same headings and the same links as before. Everything the server keeps for itself lives in <vault>/.secondbrain/ and can be deleted without losing anything.
Without Memory owned by an app
- Nothing survives the chat window, so the same context is pasted in every time
- Or it survives, but only inside a proprietary store with an export button you have never tested
- Your editor, your scripts and your backups cannot see any of it
- An agent rewriting a whole document can drop a paragraph and nobody notices for months
- No history: the note is simply different now
- The conventions of your knowledge base live in a system prompt somewhere else
With secondbrain Memory owned by you
- Files on disk, one Markdown file per note, readable with
cat - Obsidian compatible: wikilinks, frontmatter, tags, checkboxes, attachments
- The index, the trash and the instructions are a cache; delete them and nothing is lost
- Edits are scoped to a section or an exact string, and refused when they are ambiguous
- Every write is a git commit naming the tool and the path
- Each vault ships its own instructions to the client on connect
How it works
A lens over a directory
An agent editing prose fails differently from an agent editing code. Wrong code does not compile; a wrong edit to a note is a paragraph that quietly disappears. Almost every design decision here exists to make that failure mode impossible rather than unlikely.
Markdown you can leave at any time
One file per note, YAML frontmatter, [[wikilinks]], inline tags, checkboxes. Nothing is stored in a format only this server understands, so leaving costs a docker rm and nothing else.
Search that survives external edits
SQLite FTS5 with BM25 ranking, reconciled against the filesystem at start and kept live by a debounced watcher. Editing the same vault in Obsidian, or pulling it from git, is normal operation rather than a hazard.
Edits that cannot silently destroy a paragraph
Whole-note replacement requires the hash you read. Section edits address a heading. String edits must match character for character and are refused when ambiguous. Every mutating tool takes dry_run and answers with a diff.
Every change is a commit
One git repository per vault, one commit per write, the message naming the tool and the path. note_history, note_diff and note_restore are the same history your own git log shows.
Vaults teach the agent their own conventions
Each layout writes .secondbrain/instructions.md, and its contents are sent to the client in the MCP initialize response. Where notes go and how they are named travels with the vault instead of living in a system prompt somewhere else.
Nothing is ever really deleted
No tool calls os.Remove. Deletions and overwrites go to .secondbrain/trash/ with a timestamp and stay there for the retention window, on top of the commit that can be reverted.
In practice
A volume to mount, a search before every write
Configuration is environment variables for a single user and an optional YAML file when there are several. The interesting half is on the right: the agent is expected to find the note that already exists and add to it, not to create a fourth note about the same subject.
services:
secondbrain:
image: ghcr.io/andreaskasper/secondbrain:latest
ports: ["2020:2020"]
volumes:
- ./data:/data
environment:
SECONDBRAIN_PUBLIC_URL: https://brain.example.com
SECONDBRAIN_USERNAME: andreas
SECONDBRAIN_PASSWORD: "bcrypt:$2a$12$..."
SECONDBRAIN_GIT: "true"
user: "65532:65532"
cap_drop: [ALL]
security_opt: ["no-new-privileges:true"]
restart: unless-stopped
# chown -R 65532:65532 ./data before the first start.
-> note_search
{ "query": "postgres connection pool" }
<- 2 hits
[ { "path": "wiki/postgres.md",
"score": 8.4,
"snippet": "...pool size is per process..." } ]
-> note_section_edit
{
"path": "wiki/postgres.md",
"mode": "append_to_section",
"heading": "Connection pooling",
"content": "PgBouncer in transaction mode...",
"dry_run": true
}
<- diff only, nothing written yet
config.yaml can be restricted to a subset of them. A work vault and a private vault on the same server never see each other.
Quick start
Running in three steps
Prepare the volume
The image runs as the distroless nonroot user, UID 65532. A bind mount owned by root is the single most common way a first deployment fails.
mkdir -p ./data sudo chown -R 65532:65532 ./data docker run --rm -it ghcr.io/andreaskasper/secondbrain hashpw
Start the container
Port 2020, one volume. Unlike its sibling aegis this container cannot run read-only: it writes notes, an index and a git repository.
docker run -d --name secondbrain -p 2020:2020 \ -v "$PWD/data:/data" \ -e SECONDBRAIN_PUBLIC_URL=https://brain.example.com \ -e SECONDBRAIN_USERNAME=andreas \ -e SECONDBRAIN_PASSWORD='bcrypt:$2a$12$...' \ --user 65532:65532 --cap-drop ALL \ ghcr.io/andreaskasper/secondbrain:latest
Point a client at it
Add https://brain.example.com/mcp as an MCP server. The client discovers the OAuth endpoints, registers itself and opens the login page. After signing in, ask the agent to call vault_create once and the layout it writes will explain itself on every later connection.
The server is observable when you ask it to be: an optional Prometheus endpoint, off by default, reports vault and tool counters and never a line of note content — see Metrics.