Demo
Ringbinder is a small, simple CLI tool that lets me do this:
> docs "there was a picture with a few dragons of different colors/sizes/shapes, find it"
Found it:
/Users/max/Documents/Unsorted Photos/2024_11_03_12_07_23.jpg
It shows “six colorful winged dragons…of different sizes,” including red, orange, yellow, grey-black, brown, and a tiny brown dragon
in a row.
“Untitled” by ~5yo
Or this:
> docs "when does my car need an oil change next?"
Your Toyota Sienna’s next oil change is due at approximately **46,388 miles**.
- Last documented oil change: **March 26, 2026 at 40,388 miles**
- Recorded interval: **6,000 miles**
- Latest recorded mileage: **44,743 miles**
- Remaining: **about 1,645 miles**
That puts it around **early September 2026**, depending on your current mileage.
> “MILEAGE: 40,388” and “OIL CHANGE INTERVAL: 6000” — `/Users/max/Documents/Unsorted/2026-03-26 - Jiffy Lube Invoice - Toyota Sienna.pdf` (page 1)
> “Mileage In: 44743” — `/Users/max/Documents/Unsorted/2026-07-23 - State Inspection - Toyota Sienna.pdf` (page 1)
I have 9,700 documents with 23,000 pages. Each of the above requests took a few seconds with GPT-5.6-luna.
Backstory
I hate keeping lots of paper, so I scan and discard it immediately. Mail, paperwork, kids’ drawings, schoolwork, etc. When I’m on the go, iPhone is the scanner. A custom shortcut takes a pic and throws the file into an iCloud-synced Unsorted directory. At home, it’s my ~17yo ScanSnap S1500M that still works perfectly. For years I’ve been adding files into Documents/Unsorted (with some exceptions).
It used to be that I would name all Unsorted docs by hand, and rely on scanner’s OCR software. This was a lot of work, and I still couldn’t find many files like drawings and handwriting.
Then vision and agentic AI models started popping up, which was perfect for this.
My thinking was — if there is a cheap way to describe PDFs, images, diagrams, etc in plain text, throw those texts into a database, each mapped to the original file path, then any agent should be able to find documents, link the original files, rename those files if needed, and answer any complex questions with citations, in seconds.
So I settled on a Go binary that manages a single SQLite3 db file. Next, I needed to decide which vision model to use.
Choosing the vision model
At first I went with Mistral OCR, because it produced the most convenient output, and had the most straightforward API pricing. It promised to entirely describe any PDF page or image at a fixed price per page. Pretty great, I thought. So I built ringbinder around Mistral’s API.
Then months later I finally gave Gemini Flash a try, and was shocked.
Remember that drawing of dragons from the beginning of this post? Mistral completely failed to OCR it, and just returned numbers 1 through 99. I brushed it off thinking “meh, what can I expect from a kid’s drawing”. But then Gemini Flash, the poor thing, broke my expectations. Look at that diligence!
8 y 4 q 9 n 3
b u u q t h
va a b w w b
cea b w y w
g w cz
[Page: A children's marker drawing featuring a row of six multi-colored dragon-like mythical creatures standing at the bottom
against a white background, beneath a rectangular purple-bordered chart or poster containing fictional or stylized handwritten
script.]
[Image: illustration — A purple framed rectangular sign or paper pinned on the upper-left wall with purple triangular corners
containing five lines of stylized or invented glyph-like script.]
[Image: illustration — Six colorful winged dragons or reptilian creatures of different sizes arranged in a horizontal row along
the bottom, marching toward the left. From left to right: a very large red dragon with tall spiky wings and open jaws with sharp
teeth, a small orange dragon, a bright yellow winged dragon with an open mouth and red eye, a slender greyish-black dragon, a dark
brown dragon with upright wing feathers, and a tiny brown creature at the far right edge.]
[Image: diagram — Brown structural lines across the page delineating the wall and floor/room division.]
I was blown away and immediately added Gemini-based OCR as the recommended choice. The price wasn’t even higher than Mistral to begin with, but Gemini supports Batch API too (which I promptly added to ringbinder). This allowed me to reprocess all ~23,000 pages for half the price, like $50 total.
Recommended workflow
The way ringbinder is meant to be used is as follows:
~/.config/ringbinder/config.yml— set the paths you want to sweep. Here’s my config:paths: - "~/Documents/Manuals" - "~/Documents/Unsorted" - "~/Documents/Unsorted Photos" - "~/Local/Screens" # These are attachments to Apple Notes, because why not: - "~/Library/Group Containers/group.com.apple.notes/Accounts/*/FallbackPDFs" - "~/Library/Group Containers/group.com.apple.notes/Accounts/*/FallbackImages" - "~/Library/Group Containers/group.com.apple.notes/Accounts/*/Media" # ...a few more... model: - gemini - mistral # used as a backup in case Gemini refuses sweep_concurrency: 8 # much faster scan of all the files, takes a couple secondsringbinder sweep— collect all file paths and checksums from those locations into the database.ringbinder cost— All newly added files (content-deduplicated) start out with pending OCR, so you can find out how much it would cost to process them.ringbinder ocr— Actually process the files using the provider API, store the returned text in the database.- Or instead, run
ringbinder batch startto begin a half-priced Gemini batch. This will kick off all pending files for batch processing. - To finish batch processing you’ll need to run
ringbinder batch continueafter some time. If it’s not done, you’ll just need to run it again. You can do it anytime until it’s all finished.
- Or instead, run
Now you have a populated database of all paths and their plain texts, with FTS index, ready to be queried.
To avoid the above song and dance, I wrote 2 shell scripts that tie ringbinder and Pi together.
docs-update— run it after adding new documents to any directoriesdocs— run it to start a Pi session with any doc-related question
The docs-update script does sweep and ocr, backs up the db, then asks Pi to propose renames:
#!/usr/bin/env bash
set -e
ringbinder sweep # Sync the database with actual files in folders
ringbinder ocr # Run Gemini OCR
fname="ringbinder-$(date +'%Y-%m-%d_%H-%M-%S').db"
echo "Backing up to $fname"
cp ~/.config/ringbinder/ringbinder.db ~/Local/Ringbinder\ Backups/$fname
docs "look through recent files (only in Documents/Unsorted) and propose renames where appropriate"
Notice that the last line calls the docs script.
The docs script just launches Pi with customized config:
#!/usr/bin/env bash
set -euo pipefail
DESIRED_MODEL="openai-codex/gpt-5.6-luna"
THINKING="xhigh"
read -r -d '' DOCS_BASE_SYSTEM_PROMPT <<'PROMPT_EOF' || true
Load ringbinder skill first.
You are docs console assistant.
Treat every user message as a document-search request via the loaded ringbinder skill.
Always run ringbinder retrieval before answering.
Quote supporting lines and cite sources as path (page X).
If evidence is weak or missing, run broader probes or ask one focused clarification question.
For rename requests, propose a full plan and ask for explicit confirmation before any rename action.
Do not use subagents.
PROMPT_EOF
# Personal context facts for query interpretation.
# Add one real fact per line (delete the examples). These facts are only for
# interpreting references like "my wife"; the assistant must still find evidence
# in documents before claiming an answer.
read -r -d '' PERSONAL_QUERY_CONTEXT_FACTS <<'FACTS_EOF' || true
[REDACTED]
I list names, relationships, and other personal facts here.
[/REDACTED]
FACTS_EOF
print_error_and_exit() {
echo "docs: $1" >&2
exit 1
}
build_docs_system_prompt() {
local cleaned_personal_query_context_facts
cleaned_personal_query_context_facts="$(
printf '%s\n' "$PERSONAL_QUERY_CONTEXT_FACTS" | awk '
{
line = $0
sub(/^[[:space:]]+/, "", line)
if (line == "") next
if (line ~ /^#/) next
print $0
}
'
)"
if [[ -n "$cleaned_personal_query_context_facts" ]]; then
printf '%s\n\n%s\n%s\n' \
"$DOCS_BASE_SYSTEM_PROMPT" \
"Personal context for query interpretation (not document evidence):" \
"$cleaned_personal_query_context_facts"
return
fi
printf '%s\n' "$DOCS_BASE_SYSTEM_PROMPT"
}
ensure_ringbinder_is_available() {
if command -v ringbinder >/dev/null 2>&1; then
return
fi
}
main() {
command -v pi >/dev/null 2>&1 || print_error_and_exit "pi is not in PATH."
ensure_ringbinder_is_available
local docs_system_prompt
docs_system_prompt="$(build_docs_system_prompt)"
local -a pi_args=(
--model "$DESIRED_MODEL"
--models "$DESIRED_MODEL"
--append-system-prompt "$docs_system_prompt"
--thinking "$THINKING"
--no-session
)
# Run pi in interactive mode so you can keep asking follow-up questions in the
# same session.
if [[ "$#" -gt 0 ]]; then
pi_args+=("$*")
fi
exec pi "${pi_args[@]}"
}
main "$@"
Renaming files
Scanner and iPhone produces filenames like 2026-03-14-17-28-13.pdf or Clipboard 2026-03-14.png.
At the end of my docs-update, I launch docs with a prompt to rename recent files. Here’s my approximate rename skill:
- List recent docs and pick out the ones with non-descriptive names.
- Read the OCR text (first page, then more if needed).
- Extract the document’s own date from the text, falling back to the filename date only if the content doesn’t have one.
- Write a short, specific, filesystem-safe title based on what the page actually says.
- Propose
YYYY-MM-DD - Title.extfor each file and show me the whole plan. - Wait. Rename nothing until I say yes.
- After I confirm, rename and re-sweep the folder.
The Pi session comes on, reads texts, and proposes file renames for my approval. Honestly, I’ll probably get rid of the approval step, because the renames are always solid.
Searching docs
I always interact with the docs via the aforementioned docs executable, which starts an interactive Pi session (again, GPT-5.6-luna xhigh).
So I type docs "when did we last have the furnace serviced" and it searches, reads the pages, and comes back with a quote and a file path.
Why not RAG?
Because it’s a lot of additional infrastructure (a model for vectorizing the text, vector database capabilities) and no worthy benefits. An agent is perfectly capable of turning all your searches “fuzzy” by simply running various probes.
Why not searchable PDFs?
A sqlite database full of plain text is such a great way to let AI answer any questions, that I felt PDF injection is just more fiddly, slow, and isn’t worth it. Plus it would require the vision models to specify coordinates, angles, all this extra complexity in their output. You do get a bit of portability, but I never needed it in practice. So for ringbinder this is out of scope.
Feel free to run something like ocrmypdf on your files, ringbinder doesn’t care if your PDFs are searchable or not, it will still do its own thing. Only, do it before you sweep and ocr, otherwise ringbinder will think the files have changed and will want to re-OCR them again. Or to fix it after the fact, just instruct your agent to update the checksums and reset the pending flag directly in ringbinder’s database.
Overall thoughts
My favorite thing about ringbinder is that it’s so simple and flexible. Just a binary file, and a sqlite3 db file. You can safely re-sweep anytime, it will know when something is added, moved (since it uses content checksums), or deleted. It’s made to fit into your workflow, because it’s just an easy-to-script CLI. You can query the sqlite3 db directly if you want. Maybe you can build a GUI on top of it if that’s something you’re into. If you want to re-process all documents with a future better vision model, you can just start ringbinder with a different db file path, re-sweep, and re-ocr again. Or instruct your agent to partially carry some data over. That’s why I’ve loved using this tool for the past few months.