GET /api/work/second-brain
A second brain where the agent does the filing
Live · · solo · personal tooling
A filing cabinet for my own notes, mail and messages that an AI assistant maintains for me. It proposes what needs doing next; I approve it. Everything stays on my Mac.A plain-markdown vault an AI librarian keeps for me — 738,000 messages indexed locally, nothing filed without my approval, no cloud.
- Claude Code
- Node.js
- SQLite
- FTS5
- Astro
- Markdown
- Obsidian
- messages indexed locally
- 738K
- text hidden in a binary blob
- 88%
- notes filed without approval
- 0
- leaks a careful scrub missed
- 3
Parameters
Request
curl "https://portfolio.dylansg0318.workers.dev/api/ work/ second-brain"
Response200 OK · 3.3 KB
{ "id": "second-brain", "title": "A second brain where the agent does the filing", "status": "live", "year": 2026, "category": "tool", "role": "solo · personal tooling", "summary": "A plain-markdown vault an AI librarian keeps for me — 738,000 messages indexed locally, nothing filed without my approval, no cloud.", "summary_plain": "A filing cabinet for my own notes, mail and messages that an AI assistant maintains for me. It proposes what needs doing next; I approve it. Everything stays on my Mac.", "stack": [ "Claude Code", "Node.js", "SQLite", "FTS5", "Astro", "Markdown", "Obsidian" ], "metrics": { "messages_indexed_locally": "738K", "text_hidden_in_a_binary_blob": "88%", "notes_filed_without_approval": "0", "leaks_a_careful_scrub_missed": "3" }, "problem": "The things that actually run my week lived in four places that don't talk to each other: notes I wrote when I remembered to, a message archive going back to 2018, mail, and my head. On 2026-09-03 the vault's front page said zero overdue while eight real obligations sat unread in the inbox and an order had been sitting in the building for three weeks, forgotten twice. A second brain that only knows what you sat down and filed is a diary.", "unique": "Nothing is auto-filed, and the search layer is disposable. Plain markdown is the truth; a local SQLite + FTS5 index over every message and note is derived, so you can delete it and lose nothing. The sweep proposes loops and I approve them, because the way these systems die is by making more filing than thinking. And the copy I could hand someone is a build output with a gate in front of it, not a scrubbed sibling repo.", "ai": "The agent is the librarian here, not something I ask questions of. The vault was bootstrapped overnight by four parallel Claude Code sessions under a conductor, and day-to-day filing is one on-demand run against a constitution file the session reads first. Mine: the rules (raw before synthesis, \"captured\" isn't \"occurred\", nothing auto-filed, no patient detail ever), the approval on every proposed item, and the checking. Every number on this page came out of a command, not a summary.", "learned": [ "A naive ingester would've captured 12% of my message archive and looked like it worked. Only 86,113 of 730,507 rows carry plain text; the other 88% keep it in a binary blob. Measuring the decoder, 400/400 exact against rows carrying both, was the difference between a corpus and a guess.", "A mechanical gate finds what a careful read can't. My own by-hand scrub still left a real vendor domain and a real revenue figure, quoted in code comments as examples. You remember the names you used, not the things you quoted.", "Files forget, git doesn't. A scan of the file set I meant to open-source found 128 identifier hits, including my home address in five commits of history, where deleting the line would've published it anyway. Nothing shipped, and the scan now blocks the build.", "A write path has to check what the parser reads back for the records it didn't mean to touch. Ticking a wrapped checkbox changed one line, passed every guard, and put an item on the front page nobody had written." ], "page": "https://portfolio.dylansg0318.workers.dev/projects/second-brain" }
Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.
The problem
The things that actually run my week lived in four places that don't talk to each other: notes I wrote when I remembered to, a message archive going back to 2018, mail, and my head. On 2026-09-03 the vault's front page said zero overdue while eight real obligations sat unread in the inbox and an order had been sitting in the building for three weeks, forgotten twice. A second brain that only knows what you sat down and filed is a diary.
What was unique
Nothing is auto-filed, and the search layer is disposable. Plain markdown is the truth; a local SQLite + FTS5 index over every message and note is derived, so you can delete it and lose nothing. The sweep proposes loops and I approve them, because the way these systems die is by making more filing than thinking. And the copy I could hand someone is a build output with a gate in front of it, not a scrubbed sibling repo.
Where AI fit in
The agent is the librarian here, not something I ask questions of. The vault was bootstrapped overnight by four parallel Claude Code sessions under a conductor, and day-to-day filing is one on-demand run against a constitution file the session reads first. Mine: the rules (raw before synthesis, "captured" isn't "occurred", nothing auto-filed, no patient detail ever), the approval on every proposed item, and the checking. Every number on this page came out of a command, not a summary.
Parameters
Request
curl "https://portfolio.dylansg0318.workers.dev/api/ work/ second-brain? fields=problem%2Cunique%2Cai"
Response200 OK · 1.4 KB
{ "problem": "The things that actually run my week lived in four places that don't talk to each other: notes I wrote when I remembered to, a message archive going back to 2018, mail, and my head. On 2026-09-03 the vault's front page said zero overdue while eight real obligations sat unread in the inbox and an order had been sitting in the building for three weeks, forgotten twice. A second brain that only knows what you sat down and filed is a diary.", "unique": "Nothing is auto-filed, and the search layer is disposable. Plain markdown is the truth; a local SQLite + FTS5 index over every message and note is derived, so you can delete it and lose nothing. The sweep proposes loops and I approve them, because the way these systems die is by making more filing than thinking. And the copy I could hand someone is a build output with a gate in front of it, not a scrubbed sibling repo.", "ai": "The agent is the librarian here, not something I ask questions of. The vault was bootstrapped overnight by four parallel Claude Code sessions under a conductor, and day-to-day filing is one on-demand run against a constitution file the session reads first. Mine: the rules (raw before synthesis, \"captured\" isn't \"occurred\", nothing auto-filed, no patient detail ever), the approval on every proposed item, and the checking. Every number on this page came out of a command, not a summary.", "page": "https://portfolio.dylansg0318.workers.dev/projects/second-brain" }
Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.
The story
I kept notes the way most people do, badly. A file here, a text there, an email I meant to come back to. The front page told me nothing was overdue on a morning when eight things were waiting on me, and an order sat in the building for three weeks because nobody wrote it down anywhere it’d be seen.
So I built a filing cabinet and made an AI the librarian.
The cabinet is plain text files in a folder, the kind anything can open now or in ten years. No app owns them and nothing needs the internet. Obsidian is just a window onto the folder.
Capture takes no discipline. I type a fragment, dictate it, or star an email, and that’s my whole job. The librarian reads the scraps, writes them up, links them to the person and project they belong to, and rebuilds the front page.
Two rules keep it honest. The original gets saved before anything is summarized, so a question I haven’t thought to ask yet can still be answered later. And nothing goes on my list without me saying yes. The system reads my messages and proposes what looks like an obligation; I approve or not. These systems usually die by making more filing than thinking, and that’s the thing I’m designing against.
There’s an inspector too. If I ever hand a copy of the machinery to someone, a gate checks it first and refuses to build if it finds anything that identifies me, my employer, my vendors or my money. It caught three things a careful read of my own work missed, and one bigger one: my home address, sitting in the project’s history from an early setup note. Deleting the line wouldn’t have removed it. Nothing was published, the address is out, the plan to open-source it was dropped, and the tooling now carries no names or addresses at all, so a copy comes out clean instead of being cleaned on the way out.
Markdown is the truth
The vault is a private git repo of plain markdown. Obsidian is a viewer, the local app is a read layer, the search index is derived and can be deleted and rebuilt. That’s the one property that survives a decade, and everything below defers to it.
Every note carries frontmatter with two dates people keep conflating: captured: is
when it was recorded, occurred: is when it happened. An email read today about a call
last Tuesday carries both. Notes are atomic, wikilinked both ways, and a lint pass makes
sure every note is reachable from the front page within two hops.
Search without a model
Under the notes sits one local SQLite database with FTS5: 737,962 rows across iMessage,
WhatsApp and Apple Notes, 2018 to today, that the agent and the app both query.
Embeddings were considered and rejected. The queries I actually run are name-shaped (a
vendor, a person, an order number), and bm25 answers those in milliseconds with nothing
to re-embed. Querying the live sources per question was rejected too: a 1.1 GB store
with write-ahead locks and no way to search across sources at once. The index is
gitignored, never leaves the Mac, and keeps each source’s payload verbatim in a raw
column, so a field I want next month never means re-reading the source.
The decoder that decided whether any of it was real
iMessage stores text in a text column, except only 86,113 of 730,507 rows have
one. The other 642,288 (88%) keep it inside attributedBody, an NSArchiver blob. An
ingester that reads text alone captures 12% of the archive, throws no errors, and
looks like a success.
So the decoder was measured before anything was built on it: find NSString, seek the
next 0x2b, read a length byte, take that many UTF-8 bytes. 400/400 exact against
rows with both fields and 2,000/2,000 recovered on rows with no text at all, but
only after normalising an attachment placeholder the two fields disagree about on 4,620
rows. Before that it’s 399/400. The verify command prints the raw number next to the
normalised one on purpose, so the gate can’t be quietly tuned into passing.
Two smaller ones from the same pass. A field that looked like a contact name was a base64 media hash on all 2,296 rows that had one, and an earlier build had indexed 2,119 of them as message text. And WhatsApp’s 17% empty-body rate is real — captionless photos and system events with no text in the source — so there’s no per-source floor, and the reason is written down.
Propose, don’t file
The sweep reads the index and proposes candidate open loops. It writes nothing; I approve. That’s the main product decision, and it’s about a known failure mode, not technology: a second brain that files for you makes more filing than thinking and gets dropped within months. The agent’s job is to make sure nothing is missed, not to decide what’s mine.
Every external ingest writes an immutable raw copy before any synthesized note. If answering a new question would mean re-fetching from the source, the ingest was too lossy. Personal messages are the one exception, and only about where the raw layer lives: the local index is the raw layer for them, since it keeps every row verbatim, and a thread’s raw copy is written out the moment it earns a note. The rule holds without committing 730,000 personal messages to git.
The page that writes back
The local app runs on 127.0.0.1 and writes in exactly two ways: a capture, and
ticking, snoozing or adding one loop — one line, in one note, or it refuses.
A review pass found a defect my own read and every guard had missed. Ticking a loop whose text wraps orphans the continuation line behind the new checkbox, and the parser folded that orphan into the loop above it. One line changed, every guard passed, the diff looked fine, and the front page showed an item nobody had written.
The shareable copy is a build output
There’s a generic version of the machinery — tooling, read layer, note shapes, the librarian skill and a genericized constitution — I can hand to a person. It’s built by a command, never edited by hand, because two repos kept in parallel drift and the one that drifts is the one a stranger reads.
There’s no regex scrub in that build. The tooling in the real vault carries no identifiers to begin with, and a gate blocks the build on names, real email domains, phone numbers, addresses, money figures, gendered pronouns and secrets before it copies anything. It earned its place right away: after a careful by-hand pass, it found a real vendor domain and a real revenue figure sitting in code comments as examples, plus a verbatim quote from a private note.
The original plan was to publish the tooling. Scanning the proposed public file set first turned up 128 identifier hits, including my home address, in a file five commits of history had touched, so deleting the line would’ve published it anyway. Nothing shipped. The address came out of the working files, the identifiers came out of the tooling itself, and the scan became a build gate. The vault stays private, and the framework is a private starter with the rules included.
What it doesn’t do
No public remote. No scheduled agents or background jobs; the librarian runs when I trigger it. No plugin that phones home, and sync and publish are committed off. No patient detail in the repo in any form — the business is a supply company, but an order can still carry a clinician’s note, and that gets redacted before anything is filed. An ERP bridge that would show live order status beside a loop is scoped and parked until there’s a read-only credential for it.
What I learned
- A naive ingester would've captured 12% of my message archive and looked like it worked. Only 86,113 of 730,507 rows carry plain text; the other 88% keep it in a binary blob. Measuring the decoder, 400/400 exact against rows carrying both, was the difference between a corpus and a guess.
- A mechanical gate finds what a careful read can't. My own by-hand scrub still left a real vendor domain and a real revenue figure, quoted in code comments as examples. You remember the names you used, not the things you quoted.
- Files forget, git doesn't. A scan of the file set I meant to open-source found 128 identifier hits, including my home address in five commits of history, where deleting the line would've published it anyway. Nothing shipped, and the scan now blocks the build.
- A write path has to check what the parser reads back for the records it didn't mean to touch. Ticking a wrapped checkbox changed one line, passed every guard, and put an item on the front page nobody had written.
Parameters
Request
curl "https://portfolio.dylansg0318.workers.dev/api/ work/ second-brain? fields=learned"
Response200 OK · 1.1 KB
{ "learned": [ "A naive ingester would've captured 12% of my message archive and looked like it worked. Only 86,113 of 730,507 rows carry plain text; the other 88% keep it in a binary blob. Measuring the decoder, 400/400 exact against rows carrying both, was the difference between a corpus and a guess.", "A mechanical gate finds what a careful read can't. My own by-hand scrub still left a real vendor domain and a real revenue figure, quoted in code comments as examples. You remember the names you used, not the things you quoted.", "Files forget, git doesn't. A scan of the file set I meant to open-source found 128 identifier hits, including my home address in five commits of history, where deleting the line would've published it anyway. Nothing shipped, and the scan now blocks the build.", "A write path has to check what the parser reads back for the records it didn't mean to touch. Ticking a wrapped checkbox changed one line, passed every guard, and put an item on the front page nobody had written." ], "page": "https://portfolio.dylansg0318.workers.dev/projects/second-brain" }
Live: this is the answer from this site’s API, not a mock. Send re-runs the request with your parameters; if the answer names a page, it opens.