What it does
An Obsidian vault fills up faster than anyone files it. Clippings, chat exports, PDFs and mail land in a raw/ folder and stay there. The job is to turn that into a connected set of pages, where what one page claims links to the pages that support it and the pages that contradict it.
The obvious build is a pipeline: triage, file, prune, answer. Every branch in one has to be anticipated, so a null where a list was expected takes the whole thing down, and ambiguity — which folder does this go in — has nowhere to go.
The agent version has no stages. It has the vault and a shell, so an ambiguous filing decision is settled by looking: the neighbouring pages, how the last twenty were filed, what the vault already says about the same subject. Then it writes the page and the backlink in both directions, because a page nothing points at is a page nobody finds.
Where the line is
Four invariants. Each is one rule and one consequence: a rule whose reason the model can reconstruct survives being paraphrased.
| Rule | Why it is written this way |
|---|---|
raw/ is append-only | A corrected page goes elsewhere and links back. Source is never rewritten. |
| Nothing is hard-deleted | Removal is a move to .librarian/trash/<today>/. That is the undo path; rm has none. |
| Credentials are invisible | Eleven filename globs and secret: true in frontmatter, listed out. Never opened, indexed, quoted or moved. |
| Never invent a link | A [[wikilink]] to a page that does not exist is worse than an admitted gap — future work follows it and finds nothing. |
The credential rule is a list rather than a principle on purpose: *key*, *pass*, *login*, *token*, *secret*, *credential*, *.env*, *private key*, *recovery code*, *backup code*, *seed phrase*. *code* would be shorter and catch two of them — and also catch every page about code. The list is a list because an enumeration can be checked against a vault and a principle can only be argued with.
It asks about four things and no others: a naming convention about to be applied to many pages, whether two pages should merge, anything destructive beyond one obvious stub, and a source it cannot tell is important or noise. And the inverse: do not ask about things you can find out. Which folder similar pages live in is in the vault, and looking is the job.
The tools
Two. Inside its container the agent already has a shell, curl, python3 and the web, so reading pages, grepping for a broken wikilink, rewriting frontmatter and counting orphans are all shell work. A tool earns its place when it holds a credential or gates an effect.
| Tool | Gate | Why it exists |
|---|---|---|
fetch_mail | credential | Writes mail into raw/email/ and nothing else — no filing, no summarising. One path in for source material rather than two. |
trash | effect: 'write' | The only removal path. Stops and asks, in the terminal and on your phone. |
export const trash = defineTool<{ path: string; why: string }>({
name: 'trash',
effect: 'write',
preview: (args) => `trash ${args.path} — ${args.why}`,
// …moves the page to .librarian/trash/<today>/, folder layout kept.
});Two of those four have code behind them, both inside trash. The credential list and the wikilink rule are north star text, enforceable only in the sense that a rule carrying its reason survives being paraphrased.
// realpath, not resolve: a symlink inside the vault resolves on the host
// side of the bind mount, and resolve() does not follow one.
const from = realpathSync.native(resolve(VAULT, args.path));
const rel = relative(realpathSync.native(VAULT), from);
if (rel.startsWith('..')) throw new Error(`${args.path} resolves outside the vault.`);
if (rel.split('/')[0] === 'raw') {
throw new Error(
'raw/ is append-only. Source material is never removed — write a corrected page elsewhere and link to it.',
);
}Be honest about what that buys. The shell can still rm, and a second approval prompt inside the container would only train someone to tap yes. What the guard actually does is make the correct path the easy one, keep the wrong one from being the first thing the model reaches for, and put every removal in the journal with a reason attached.
What it needs
One Google client — id, secret, refresh token — and nothing else. All three are gated: held by the supervisor and kept out of the container’s environment, and released to fetch_mail only after you approve that tool holding that credential. The supervisor runs inside the container, so this stops the shell and the model — not a compromise of the container itself. The scope is gmail.readonly, so the worst case of a bug is a page you did not want, never an email you cannot get back.
The gate is on the tool call, not on the agent. Inside its container it has a shell, curl and python3, with no prompts in front of any of them. What a gated tool buys is that the credential is withheld until you release it and the effect is previewed before it happens — which is why the credentials matter more than the guards.
There is no vault setting. You start the agent inside the vault:
cd ~/Documents/vault
librarianIt lives and dies with that terminal, on purpose — there is no daemon. Its schedules are timers inside the session, so with the terminal closed it is not running and not fetching; on the next start it is told what came due and decides whether catching up is still worth doing.
How it reaches you
Librarian is quiet — a few messages a week. What earns one: a genuine contradiction between something new and a position you have already recorded, a decision that looks like it needs you, or something arriving repeatedly that suggests a page you do not have yet. Never a report that amounts to I did the thing you asked me to do continuously; you can read the vault.
When it does need you, it asks with options rather than in prose: a question with options arrives on your phone as tappable answers, and a tap is cheaper than a sentence.
A trash call is the other thing that reaches you, as an approval block with the preview written for a lock screen: trash notes/Old Stub.md — empty since March, superseded by [[Filing]].
In a group chat with your other agents it answers questions about the vault and cites the pages it used as [[wikilinks]] — in a room you read every word of.
The decision worth stealing
The mount it does not have.
A folder-scoped agent runtime gives the agent one folder and nothing else on the machine; anything outside it needs an explicit mount, and a mount is a real hole — the shell writes straight through it with no gate in front.
The obvious build mounts the vault. Librarian does not. You run the command inside the vault, so the working folder is the vault, and two things follow. There is no mount, so nothing outside the vault is reachable — which is what makes write access to it safe, because the blast radius is exactly the job. And a second vault is a second agent, with its own memory and its own journal, which is right, because filing conventions from one vault are wrong in the other.
The cost is that append-only can no longer be a mount flag, which is why it is a throw instead. Most agents that work on “a folder somewhere” are better built this way, and the mount setting is for the second folder, never the first.
Build it
The build lives in Temper — a terminal agent runtime, sandboxed in Docker, with memory, schedules, custom tools and a line to your phone already there. One guide per agent, and it writes every file: the interview that comes first, the north star, the manifest, both tools, the schedules and the tests.