Give AI agents the right context, not your entire filesystem.
Shelfmark turns years of scattered documents into a governed context map for AI agents. An agent can discover what exists, understand what kind of material it is, and select the documents relevant to a task — before spending context opening files.
No document migration. No duplicated content store. No need for a perfect folder structure.
uv tool install shelfmark # or: pipx install shelfmarkLocal by design · metadata only · governed discovery · built for MCP
Reports, presentations, models, research, proposals and working documents accumulate over years. Some are carefully organised. Others sit inside crowded project folders, old archives, download directories, or collections that made sense only at the time.
The value is still there. The problem is that agents cannot use what they cannot discover — and giving an agent unrestricted filesystem access does not solve that. It transfers the work of finding, interpreting and filtering thousands of files into the context window.
Shelfmark gives the agent a map first.
An agent does not need every document. It needs to know what exists, what is likely to matter, where it came from, and whether it should be used at all.
Shelfmark is a discovery layer between the agent and your files. The agent searches the catalogue, narrows the field, and requests only the material the task actually needs — so an existing document estate becomes working context without turning the filesystem into one enormous prompt.
Shelfmark builds a local SQLite catalogue of references and derived metadata: paths, filenames, formats, sizes, document types, authors, dates, classifications, selected Office properties, presentation titles, and optional content hashes.
It does not copy your documents into the catalogue. It does not index document body text. It does not create a second repository to govern, synchronise and maintain. Your files stay where they are; the catalogue points at them and describes what can be established from their metadata.
Finding a document does not expose its contents.
Shelfmark does not require a designed information architecture. Point it at accumulated project files, forgotten archives, or folders where documents have simply been dropped over the years.
It builds an inventory from signals that already exist — filenames, extensions, paths, authorship, dates, Office properties, and your own classification rules — creating a virtual structure across the material. Agents can then search by document type, client, project, purpose, author, year or location without anything being moved or renamed.
A messy folder can become navigable even when it never becomes tidy.
Shelfmark also stays honest about the limits of metadata. A file called
final7.pdf with no useful properties cannot reveal its meaning without
someone reading it. Shelfmark reports what is known rather than inventing
certainty — the same reason it drops placeholder slide titles instead of
listing twelve headings a deck does not have.
- Discover before opening. Search thousands of references before deciding which few files deserve attention.
- Preserve the context window. Concise catalogue results instead of whole documents that may not be relevant.
- Build on previous work. Reports, models, proposals, research and methods stay discoverable across new tasks and future engagements.
- Keep provenance visible. Path, date, author, document type and surrounding context help an agent judge relevance.
- Reduce blind exploration. Structured search instead of repeatedly walking directories and inspecting files one at a time.
- Separate discovery from access. Shelfmark helps identify material; opening the original remains a separate, controllable decision.
Shelfmark walks only the roots you configure.
Symlinks are not followed. A link inside a root reads as an ordinary
file and would otherwise walk straight out of the tree you configured — and
hash opens files. Skipped links are reported, never silently dropped. To
index another tree, add it as an extra root: the boundary widens by saying
so in config, not by planting a link.
The catalogue is refused inside any root, because a database that indexes
itself grows on every refresh. Both checks compare resolved paths, so ..
and a symlinked root cannot slip past them.
Not every useful document should be treated the same way. Shelfmark separates two questions that usually get confused:
- Who owns or may reuse this? →
rights:OWN/REFERENCE/RESTRICTED - May this document leave its current context? →
confidential:0/1
A method may belong to you while the client deliverable containing it stays confidential. Modelling the two separately lets agents discover reusable knowledge without treating everything discoverable as freely shareable.
Files matching your private/secret patterns become RESTRICTED: no tool
returns their path, name, metadata or content, no argument overrides it,
they are never opened for hashing, and the database is opened read-only.
corpus_stats() reports a single corpus-wide count of sealed files and
nothing else about them — not which root, not which folder. That count is
the one thing disclosed, deliberately: silence about it would misrepresent
the size of the corpus.
Governance is applied by the catalogue, not left to the wording of a prompt.
A search result is only useful if the agent knows the catalogue is current.
The MCP server keeps its own index current while it runs, so nothing has to
be scheduled and no agent has to remember. When it cannot — never built,
stale, a failed refresh, a clock it cannot reason from, or an index that no
longer agrees with the filesystem — every tool says so above its answer,
and corpus_stats() compares index against disk in full.
An old snapshot is never presented as complete knowledge.
| Consultants and advisors | Find previous analyses, proposals, frameworks and deliverables without exposing unrelated client material. |
| Researchers and analysts | Navigate large collections of reports, datasets and source material through consistent metadata. |
| Product and strategy teams | Reconnect decisions, research, roadmaps and previous thinking across projects and time. |
| Studios and independents | Turn years of accumulated work into reusable context while keeping control over client files and IP. |
| Agent builders | Give local agents a governed discovery layer over MCP. |
- Point Shelfmark at your existing folders. One or more roots. Files stay where they are.
- Build the local catalogue. It walks the permitted roots, extracts available metadata, applies classification rules, and writes references into SQLite.
- Review ownership and confidentiality.
shelfmark reviewasks a few questions about your own folders and writes the answers to config. - Connect an MCP-compatible agent. It searches, browses and inspects catalogue records through structured tools.
- Retrieve only what matters. The agent identifies the relevant artefacts before any separate content access takes place.
uv tool install shelfmark # or: pipx install shelfmark
# from a checkout:
uv tool install /path/to/shelfmarkPython ≥ 3.11. macOS and Linux. Email ingestion is optional and pulls extra
dependencies: uv tool install "shelfmark[email]".
shelfmark init # writes ~/.config/shelfmark/config.toml
$EDITOR ~/.config/shelfmark/config.toml # set your [[roots]]
shelfmark refresh # first build (add --no-hash for a fast pass)
shelfmark review # answer a few questions -> rights get set
shelfmark stats # census of what it foundRegister with Claude Code:
claude mcp add shelfmark -s user -- shelfmark-mcpThen in a session: corpus_stats() to orient, browse_folder() to
navigate, search_docs() / get_file() to find and inspect.
| Tool | What it answers |
|---|---|
corpus_stats() |
What is here overall + an honest freshness line. Call first. |
browse_folder(prefix) |
What is inside a folder: counts, sizes, facet mix. |
search_docs(query, …) |
Metadata full-text search with facet filters. |
get_file(path) |
Full record for one file: rights, authorship, slide titles, identical copies, on-disk status. |
search_emails(query, …) |
Full-text over an ingested .pst/.msg email archive (optional). |
Result lists always say when they are cut (showing 100 of 195 …), unknown
filter values are reported as bad filters with suggestions (never as an
empty corpus), and excerpts mark their truncation point.
Everything corpus-specific lives in config.toml — the code ships with
neutral defaults only. Resolution order: --config flag →
$SHELFMARK_CONFIG → ~/.config/shelfmark/config.toml. See
config.example.toml for the full annotated reference. Highlights:
| Section | What it controls |
|---|---|
[[roots]] |
The trees to index. One unlabelled primary root; extra roots get a label prefix. |
[index] |
Where the SQLite catalogue lives. Must be outside every indexed root and outside cloud-synced folders — it is a mutating binary DB, and this is enforced. |
[privacy] |
Regexes for secrets and private subtrees → RESTRICTED. Built-ins already cover .env, key/cert files, id_rsa, backup codes, identity documents. |
[authors] |
Regexes for your own name/company, for client authors, and for generator tools — drives OWN/REFERENCE classification from OOXML authorship. |
[rights] |
Path-prefix rules for the two-axis model: rights (may I reuse it) × confidential (may it leave). |
[facets] |
Which top-level folders count as work/personal; where client/project names sit in the path. |
[doc_types] / [context_types] |
Extra filename/folder rules, checked before the built-in bilingual (EN/ES) defaults; built-ins can be disabled by name. |
shareable_only=True means positively classified: confidential=0 AND rights IN (OWN, REFERENCE). Never-reviewed files are held back — unreviewed
is not the same as cleared.
Most files carry no OOXML author — a corpus is mostly PDFs, markdown and
images — so authorship classifies almost nothing and path rules are the
only mechanism that can. They ship empty, which is why a fresh catalogue
is mostly UNKNOWN and shareable_only comes back nearly empty.
That knowledge is yours, not the corpus's, so review asks for it — biggest
win first, with what the catalogue knows on screen:
[1/8] Projects
493 unclassified of 662 files · folder reads as: pitch
is: note 202, document 89, code 88, pdf 51
authored by: R. Okonjo (31), A. Lindqvist (12), openpyxl (9)
e.g. programme-overview.pptx · phase-two-proposal.pptx
answer [skip]:
Answer own, own-private, reference, client, personal, scratch
or skip. On a ~1,900-file corpus, five answers settled 80% of the
unclassified files and eight settled 90%.
shelfmark review # dry run — shows what it would write
shelfmark review --apply # writes the prefixes, re-derives rightsIt stops whenever you do, only ever asks about subtrees that are still
unclassified (so re-running continues where you left off), and writes
config, never rights values in the data — so any answer is undone by
editing the config and re-running shelfmark rights. Where authorship is
lopsided enough to be evidence it offers a default; where it is thin it
stays quiet rather than guessing.
Two things people trip over, learned the hard way:
-
Anchor short regex alternatives on both sides. An unanchored
rfimatches inside "Dockerfile" and the Spanish word "perfil". Before adding or deleting an alternative, list the filenames it actually matches. -
A rule edit does not relabel existing files. The builder is incremental, so after any rule change run:
shelfmark reclassify all # dry run — shows what would change shelfmark reclassify all --applyDo not use
build --rebuildfor this: it re-walks everything (and on cloud-synced trees drags every evicted file back down) when the filenames are already in the DB.
shelfmark refresh # build + rights + prune + assertions
shelfmark refresh --if-needed # only if a write landed or the index is oldrefresh is incremental by (size, mtime, cloud-residency) — a couple of
seconds over a ~30k-file tree — and it asserts its own correctness on every
run: walk coverage (an OS-denied walk must not read as success), guarded
pruning of deleted files (refused when it looks like a mass deletion or an
unmounted root), and governance invariants (no RESTRICTED row in the search
index, no unsealed secret, no unsearchable non-RESTRICTED row). Results
land in REFRESH_STATUS.json next to the DB, and the MCP server reports
them on every corpus_stats() call.
Any guard that declines to touch the index says so on stderr and exits non-zero — you never get a clean-looking summary hiding a refusal.
The two size guards cannot tell "the root was unreadable" from "those files really were deleted" — both look like a short walk. So they refuse, name both possibilities, and leave the index untouched:
FAILED — walk saw 63/123 catalogued files, below the 80% floor — either the
root was unreadable to this process or that many files really went away.
Index NOT updated; re-run with --force if the deletion was real.
Check which it was. If the files are genuinely gone:
shelfmark refresh --force # accept the short walk, prune past the ceiling--force backs the catalogue up to catalog.db.bak-preprune before
deleting anything. If instead a root was merely unmounted or unreadable,
fix that and refresh normally — the rows are still there.
The MCP server keeps its own index current. It is spawned by your client,
lives for the whole session, and refreshes once on startup and then
whenever refresh.max_age_seconds has passed. Nothing to install, no
timer to configure, no command to remember.
That works because a resident process the client already starts is a
better trigger than a scheduler: it inherits the same file access the
client has, and it is running exactly when you are asking questions. If
you maintain the catalogue some other way, shelfmark-mcp --no-auto-refresh leaves it alone.
Editor hooks are now an optimisation, not the mechanism — they cut the
delay between an agent writing a file and that file being searchable, from
one refresh interval to the end of the turn. With Claude Code, in
~/.claude/settings.json:
{
"hooks": {
"PostToolUse": [{"matcher": "Write|Edit|MultiEdit|NotebookEdit",
"hooks": [{"type": "command", "command": "shelfmark mark-dirty"}]}],
"Stop": [{"hooks": [{"type": "command",
"command": "shelfmark refresh --if-needed >/dev/null 2>&1"}]}]
}
}mark-dirty drops a marker only when a write landed under an indexed root
(near-free on every other write); refresh --if-needed picks it up at the
end of the turn.
Only worth it for non-MCP use, or an instant first query on a very large
tree. On Linux, a systemd --user timer running shelfmark refresh --if-needed is clean.
On macOS, the obstacle is TCC, not launchd. A LaunchAgent is denied
~/Documents, ~/Desktop and ~/Downloads by default; os.walk swallows
the error, so the build walks a handful of files and exits 0 — every
layer reports success while the index never updates. Grant Full Disk Access
to the interpreter that runs the job and a LaunchAgent is fine. The
coverage assertion catches the ungranted case either way: the refresh
fails loudly rather than quietly indexing nothing.
The refresh never reads file contents (it must stay fast). To populate hashes for duplicate detection:
shelfmark hash # reads every unhashed, non-sensitive file
shelfmark hash --limit 2000 # chip away at itSensitive rows are never opened, and neither are symlinks. On cloud-synced trees, dataless placeholder files are skipped — reading one silently yields the hash of the empty string, which would make distinct files look identical. Re-run after large materialisations.
- Desktop search helps a person find text inside files.
- Document management systems require documents to be imported and managed inside a new environment.
- Retrieval systems parse, chunk and copy document content into search indexes or vector stores.
- Filesystem tools give an agent direct access and leave every discovery decision to the agent.
Shelfmark sits at a different layer: a local, governed, metadata-based map of the documents you already have, built specifically for selective agent context.
It is not a document management system. It does not replace your filesystem. It does not require embeddings. It does not reorganise your folders. It does not claim to understand content it has never read. And it does not assume every discoverable document is safe to share.
It gives agents a better starting point.
These are decisions, not a backlog:
- No embeddings. Metadata FTS + facets first.
- No content extraction. Body text stays out of the index by design.
Those two refusals should be revisited on evidence, not on a competitor's feature list — so shelfmark records the searches that found nothing, locally, and tells you what they mean:
shelfmark misses # the evidence
shelfmark misses --clear # forget itThe report answers one question: could metadata search ever have found it? A term appearing in no filename, path, author, title or slide title was unreachable however it was phrased — that is the shape content extraction fixes. A term that is in your corpus but still missed was a phrasing or filter problem, which is a different repair. Without that split, a miss log only proves that people search.
73 searches returned nothing (2026-06-02 → 2026-08-07)
Most-missed terms:
9 abatement
7 timeline (nowhere in your metadata)
6 commitments (nowhere in your metadata)
41 of 62 distinct terms (66%) appear nowhere in your filenames,
paths, authors, titles or slide titles.
These are mostly things metadata search could NEVER have found, however
phrased. That is the pattern the README says should reopen content
extraction — not embeddings.
Bad filters, impossible year ranges and searches against a stale index are
not recorded as misses: each is already explained to the caller, and
logging them would bury the real signal. It stays local, is capped, and
never leaves your machine. Turn it off with [misses] enabled = false —
recording your own search terms is a privacy-affecting choice, and whether
that is acceptable genuinely varies by corpus.
Does Shelfmark upload my documents? No. It runs locally and builds a local catalogue. Document contents are not copied into it.
What does the catalogue contain? File references and derived metadata: paths, filenames, types, sizes, dates, authors, classifications, selected Office properties, presentation titles, and optional content hashes.
Does it read document contents?
The file catalogue does not index body text. Two deliberate exceptions:
shelfmark hash opens files to compute content hashes for duplicate
detection, and optional email ingestion can index message bodies so
search_emails works. Both are opt-in commands, both skip symlinks, and
both honour your privacy rules — a file or archive matching your
secret/private patterns is never opened by either.
Do I need to reorganise my folders first? No. Better filenames and metadata improve classification, but no formal structure is required.
Can it understand every badly named file? No. A file with a meaningless name and no embedded metadata stays hard to identify without reading it. Shelfmark says what it knows rather than guessing.
Why not just give an agent filesystem access? Filesystem access lets an agent open files. Shelfmark helps it decide which files are worth opening — and which should stay out of its results entirely.
Why not a vector database? A vector database is for semantic retrieval from contents. Shelfmark solves the earlier problem: discovering and governing what exists, before deciding what content should be processed at all. They are compatible; this one comes first.
Which agents can use it?
Any client that supports local MCP servers. Register with
claude mcp add shelfmark -s user -- shelfmark-mcp, or point your client at
the shelfmark-mcp command.
uv run --group dev pytest # the suite ships in the sdist, so this
# runs from the release artifact tooThe test corpus — including its OOXML files — is synthesised on every run; no fixture binaries are committed and no real document is ever read.
MIT — see LICENSE. Free to use, modify and redistribute.