nasindex Documentation
nasindex makes a document archive on a NAS searchable for Claude without changing anything there. This page describes how it works, how to set it up and run it, and where its limits are.
A finding aid, not a knowledge store
The index takes Claude to the right five documents. Claude then reads the complete original with its tables intact. That way no figures torn out of context end up in an answer, figures that sound plausible and are wrong.
Read-only
The archive is never written to. Access runs through a folder path on the NAS itself or through a NAS user that may only read.
The folders carry the knowledge
nasindex reads project, phase and year from the path before it opens a file. In construction project archives also BKP code, trade, award stage and document class.
Claude quotes the original
Answers with figures always rest on the document that was read, never on a search snippet.
From archive to answer
One container does both: it indexes the archive in a continuous loop and answers Claude's requests. The index lives inside the container on its own volume, never on the archive.
One indexing cycle
The wizard shows these steps by their German names.
| Step | What happens |
|---|---|
| Vorlage-Katalog einspielenload seed catalogue | Only on the first start and only if a catalogue from another computer is provided (seed). |
| Dateien suchenfind files | The archive is walked. The metadata comes from each path. |
| Texterkennung prüfencheck text recognition | One-off calibration of the text extractor on sample files. |
| Dateien abgleichenreconcile files | New, changed or gone? Detected by size and modification date. |
| Prüfsummen bildencompute checksums | SHA-256 per file, for changes and duplicates. |
| Texte auslesenread texts | Full text from PDF, Office and scans. Scans go through text recognition (OCR, German). |
| Volltextindex aufbauenbuild full-text index | Search index over file name, path, trade and content. |
| Katalog exportierenexport catalogue | A copy of the index from which another computer can build its own index. |
The first cycle over a large archive takes hours. After that, a cycle over an unchanged archive takes only seconds to minutes.
What the path reveals
In any archive the first folder level counts as the project and the second as the phase. The year comes from the path or file name. If a document in a construction project archive sits under a folder «Aufträge» (orders), nasindex also recognises BKP code, trade, award stage and document class. From 24031-MST / 1 Aufträge / 22810_Sonnenschutz Südfassade / 2 Vergabe / 1 Angebote / Offerte_250312.pdf it reads:
24031-MST
1 Aufträge
22810 · Sonnenschutz Südfassade
2 Vergabe
Angebot (offer)
2025
BKP is the Swiss construction cost classification. Duplicates are merged by content. The copy that wins has a document class, then a trade, then lies outside backup, copy or dispatch folders, then has the shorter path. The year comes from the path or file name, otherwise from the file date. It is good enough for date-range filters, not for statements like «awarded in year X».
Three ways, one wizard
nasindex comes as a package nasindex-<version>.zip with the program for Intel/AMD and ARM. The install script loads the matching program, starts the setup wizard, checks access to the archive and starts nasindex. The step-by-step guide is included in the package as ANLEITUNG.md (German).
| Way | When | Archive access |
|---|---|---|
| Directly on the NAS | Permanent operation, if the NAS runs Docker (Synology Container Manager) | bind: the folder path of the share, e.g. /volume1/Projekte |
| On another computer | A Linux machine that runs around the clock, or a Mac with Docker Desktop | cifs: the container connects to the share itself, with a read-only user |
| Mac first, then NAS | Large archive, weak NAS: the Mac reads everything once, the NAS takes over | first cifs on the Mac, then bind on the NAS with the Mac's catalogue |
Requirements
- Docker with Docker Compose v2. Check with
docker compose version. - At least 5 GB free in the installation folder, plus space for the index, about a tenth of the archive size.
- Tailscale on the computer running nasindex and on every workstation using Claude.
- On the workstations Claude Desktop with Node.js, or Claude Code.
- In
cifsmode: a NAS user with read-only access to the archive share. nasindex never needs an administrator account.
Starting
Unpack the package, do not rename or move the nasindex folder, then inside it:
sh installieren.sh
On the Mac a double-click on Installieren.command works too. If Docker needs administrator rights, as is usual on a Synology NAS, the script asks for the password itself.
Seed: catalogue from another computer
A more powerful computer builds the index once in full. Its catalogue consists of four entries: catalog.jsonl.gz, catalog.json, text/ and meta/. Inside the container they are under /daten:
mkdir nasindex-katalog
docker cp nasindex:/daten/catalog.jsonl.gz nasindex-katalog/
docker cp nasindex:/daten/catalog.json nasindex-katalog/
docker cp nasindex:/daten/text nasindex-katalog/
docker cp nasindex:/daten/meta nasindex-katalog/
Copy the folder to the target system and give it to the wizard when it asks for the seed path. On the first start nasindex loads it, visible in the log as a line seed: …. After that the reconciliation usually reports «0 neu, 0 geändert» (0 new, 0 changed).
Questions, files, checks
The wizard is divided into numbered sections: Voraussetzungen, Archivzugang, Startdaten, Netzwerk, Feldextraktion, Dateien schreiben, Start und Erstindexierung, Abschluss (requirements, archive access, start data, network, field extraction, writing files, start and first indexing, completion). Before it writes anything, it shows the requirements with their check results.
| Question | Meaning |
|---|---|
| Läuft der Container auf dem NAS selbst?Is the container running on the NAS itself? | Yes: mode bind, then the archive path. No: mode cifs, then SMB server, share, read-only user, domain (optional) and password. |
| Katalog von einem anderen Rechner (Seed-Pfad)Catalogue from another computer (seed path) | Empty: first indexing here. Otherwise the folder with the catalogue. |
| Tailscale-IP-AdresseTailscale IP address | The suggestion comes from tailscale ip -4. Only an IP address, no host name. The wizard rejects 0.0.0.0. lokal binds to 127.0.0.1 only. |
| Port | Default 8765. |
| FeldextraktionField extraction | Provider, model, optionally client name, key and approval. Default: anthropic, no key, no approval. See Field extraction. |
What it creates
| File | Content |
|---|---|
.env | Mode, Compose files, address and port, field extraction settings. No credentials. |
docker/mcp.token | The access key for Claude, randomly generated. |
docker/smb.cred | Only in cifs mode: read-only user and password. |
docker/llm.key | Optional: API key for field extraction. |
All files have permissions 600, only the owner can read them. The wizard overwrites existing files only after asking. In cifs mode it checks the credentials against the NAS before starting, and asks again up to three times on failure.
Progress and running it again
After the start the wizard shows the first cycle live, for example Schritt 5 von 7: Texte auslesen - 1234 von 20000 (6 %), noch ~3 h 10 min. Ctrl-C only ends the display, indexing continues. To see the status again later:
sh installieren.sh
The script then asks «Die Fragen neu beantworten?» (answer the questions again?). Enter means no: it only checks and shows the status.
Connect the workstations
At the end the wizard prints both recipes with address and port. Read the key from docker/mcp.token. It gives full read access to the indexed archive: treat it like a password.
Claude Desktop
Add this to claude_desktop_config.json, then quit Claude Desktop completely and restart it. --allow-http is correct, Tailscale provides the encryption.
{
"mcpServers": {
"archiv": {
"command": "npx",
"args": ["-y", "mcp-remote", "http://<tailscale-ip>:8765/mcp", "--allow-http",
"--header", "Authorization: Bearer <key>"]
}
}
}Claude Code
claude mcp add --transport http archiv http://<tailscale-ip>:8765/mcp --header "Authorization: Bearer <key>"Skill
In Claude under Settings → Capabilities, upload the file claude/archiv-skill.zip from the package. The skill tells Claude which tool to use when, that figures come from the original and how to cite sources. Test: «What is the status of the archive?»
Search finds, reading proves
| Tool | Purpose |
|---|---|
archiv_suche | Full-text search with filters on project, document class, trade, BKP and year. |
archiv_lesen | The whole document from the index, with tables. This is where figures and dates come from. |
archiv_datei | The original file straight into the chat: PDF and images (png, jpg, gif, webp) up to 10 MB. Otherwise path, size and the reason. |
archiv_sql | A single read-only SQL query. Count, group, find gaps. |
archiv_status | What is in the index, how current it is, whether the archive is reachable. |
Questions this answers
| Question | Route |
|---|---|
| What was offered for the sun shading on the south façade? | Search with document class Angebot, then read |
| How many offers do we have per trade? | SQL with GROUP BY gattung |
| Which trades have invoices but no contract? | SQL with HAVING |
| Where else does company X appear? | Search without filters across all projects |
| All contracts over CHF 50,000 | SQL on the table felder, once field extraction has run |
What archiv_sql sees
For counting and comparison questions. The query is read-only, a single SELECT. Column names are German.
Table docs
One row per file. The most important columns:
rel,filename,ext,size: path in the archive, name, extension, sizeprojekt,phase,bkp,gattung,stufe,dokklasse,jahr: from the path (project, phase, BKP, trade, stage, document class, year)dup_of: set for a duplicate, points to the originalchars,page_count,table_count,ocr_used: about the extracted textextract_status:ok,meta_only,duplicate,pending,error,missing
Table felder
One row per processed contract, joinable on rel:
auftragnehmer,gegenstand,vertragsdatum,vertragsnummer(contractor, subject, contract date, contract number)netto,bruttoas numbers,waehrung(net, gross, currency)unterzeichner_ag,unterzeichner_an,sachbearbeiter,telefon,email(signatories, contact person, phone, email)statusper document:ok,leer,zu_gross,fehler(empty, too large, error)<feld>_statusper field:wert,nicht_im_dokument,nicht_extrahiert(value, not in document, not extracted)
meta_only means no full text, for example for CAD drawings and images. They can still be found by path and file name. missing means the file has disappeared from the archive.
What runs by itself, what you do
nasindex starts by itself after every restart of the computer and checks the archive every hour for new and changed files. All commands run in the nasindex folder. On a Synology NAS, prefix them with sudo.
| You want to | Command |
|---|---|
| See whether it is running | docker compose ps |
| See the latest messages | docker compose logs --tail 200 nasindex |
| See progress and status | sh installieren.sh, then Enter |
| Know how much is in the index | docker exec nasindex python3 /app/nasindex.py stats |
| Index now instead of waiting for the hour | docker exec nasindex python3 /app/docker/entrypoint.py build |
| Check the server without Claude | docker exec nasindex python3 /app/docker/entrypoint.py selbsttest |
Check the NAS credentials (cifs) | docker compose run --rm --no-deps nasindex smb-pruefen |
| Restart | docker compose restart |
| Stop and start again | docker compose stop and docker compose up -d |
| Change the key for Claude | Write a new value to docker/mcp.token (at least 32 characters), then docker compose restart. Update the workstations. |
Updating
An update is a new package. Unpack it in the same place and let it overwrite existing files. .env and the credentials are not in the package and stay. Then run sh installieren.sh and answer «Die Fragen neu beantworten?» with Enter (no). The index is kept.
Amounts from contracts
Full-text search finds documents but cannot calculate. For thresholds and totals, felder reads twelve fields from each contract (Werkvertrag) and writes them into the table felder. To do this the contract text goes to a language model. This is the only outbound network call in nasindex.
Never automatic
The container never starts felder by itself. It only runs on your command.
Only with approval
Without approval felder refuses every run, even the dry run. The wizard asks for it, the default is no.
The original is quoted
Amounts are never calculated or converted, only taken over when they appear like that in the document.
Providers
| Provider | Address | Model | Key |
|---|---|---|---|
anthropic | fixed, api.anthropic.com | optional, default claude-sonnet-5 | required, starts with sk-ant- |
openai | fixed, api.openai.com | required | required, starts with sk- |
eigener (own) | required, an OpenAI-compatible endpoint such as vLLM, llama.cpp or OpenRouter. http:// only inside the tailnet. | required | optional |
Digital Apes has only tested anthropic with the default model. With openai and eigener, the model must follow a JSON schema and fit documents of up to 500,000 characters, about 125,000 tokens.
Procedure
docker compose exec nasindex python3 /app/nasindex.py felder --trockenlauf
docker compose exec nasindex python3 /app/nasindex.py felder --limit 15
docker compose exec nasindex python3 /app/nasindex.py felder
The dry run (--trockenlauf) shows count, characters and, with anthropic, the estimated cost, without sending a document. Reference value: 541 contracts with 14.7 million characters cost around 13 to 15 US dollars. --limit 15 gives a small set for proofreading. An interrupted run continues on the next call. Do not start it in parallel with a running indexing cycle.
Pre-check with your own model
Before every run nasindex asks the server for the context size and sends a probe with a made-up mini contract. That shows in advance whether the model keeps to the format. Documents that are too large are left for a model with more context. If the server does not report a context size (Ollama, LM Studio, llama.cpp), set it in the wizard or as EXTRAKTION_KONTEXT. Ollama silently truncates inputs that are too long.
Field rules
- Contract number only if the document calls it a contract, order, purchase, addendum or additional-cost number. Never project, object, cost-centre or BKP numbers.
- The contractor is never the architect, specialist planner, site management or engineering firm, except in that planner's own contract. Nor a delivery or invoice address.
- Contractor's signatory only from the contractor's company.
- Currency only with a stated amount, never from an IBAN.
- Phone and email only if they clearly belong to the contractor.
- Optionally a client name: that name is then never taken as the contractor.
A rule change only affects contracts read afterwards. felder --force reads all of them again and costs as much as a full run.
EXTRAKTION_FREIGABE= in .env, then docker compose up -d. The status is shown by docker compose logs nasindex | grep felder:.Settings
The wizard writes the .env. The other values are in the environment section of the Compose file, compose.bind.yml or compose.cifs.yml. After a change run docker compose up -d. Credentials never go into these files, only into the files under docker/.
| Setting | Default | Meaning |
|---|---|---|
INDEX_INTERVALL | 3600 | Seconds between two cycles, at least 60 |
ARCHIV_PROJEKTE | empty = all | Projects, comma-separated |
OCR | an | aus turns off text recognition for scans |
OCR_SPRACHE | deu | Language of text recognition |
EXTRAKT_JOBS | 2 | Files read at the same time |
EXTRAKT_TIMEOUT | 600 | Seconds per file before it is aborted |
XBERG_THREADS | 2 | Threads per extraction |
MCP_HOST_ADRESSE | 127.0.0.1 | Address the port is bound to. The Tailscale IP, never 0.0.0.0 |
MCP_PORT | 8765 | Port for Claude |
EXTRAKTION_FREIGABE | empty | ja approves field extraction |
EXTRAKTION_ANBIETER | anthropic | anthropic, openai or eigener |
EXTRAKTION_MODELL | with anthropic: claude-sonnet-5 | Model, required for openai and eigener |
EXTRAKTION_BASIS_URL | empty | Address of your own endpoint, only with eigener |
EXTRAKTION_KONTEXT | empty | Context size of your own model in tokens |
EXTRAKTION_AUFTRAGGEBER | empty | Client name for the prompt |
Resources: The Compose files set 4 CPUs and 4 GB of memory. Rule of thumb: 1 GB per file read at the same time plus 1 GB base load. An invalid value stops the start with a message that names only the setting.
What is protected, and how
Archive read-only
In bind mode the folder is mounted read-only. In cifs mode the container connects with hard-wired read-only options, without execution. The read-only user has no write rights anyway.
Access only inside the tailnet
The server speaks HTTP without its own TLS. Tailscale provides the encryption. The port is bound only to the Tailscale address, never to 0.0.0.0. Public exposure is not intended.
The key is full access
Anyone who knows docker/mcp.token can search, read and retrieve original files. Do not pass it on by email or chat. A change takes effect after a restart.
Credentials as files
Password, token and API key live in files with permissions 600, never in environment variables and never in the program. Messages never show a key or a password.
A single way out
Only field extraction sends text to a provider, only with approval and only on command. A key only goes to the configured address, never to a redirect target.
Two routes
POST /mcp requires the key. GET /gesundheit (health) answers without a key, only with ok or nicht ok. Every other path returns 404.
What nasindex cannot do
- CAD drawings and images have no full text. They can be found by path and file name.
- The year is an approximation from path, file name or file date.
- Amounts are not numbers in the full text. Thresholds and totals only work via the table
felder, currently for contracts. - New files appear with the next cycle, that is within an hour at the latest.
- Own models for field extraction are untested. Quality applies per model.
- The container runs as root, in
cifsmode with the right to mount shares. - Windows with Docker Desktop is untested in
cifsmode. A Mac with a network drive asbindis not supported, usecifsthere.
When something goes wrong
If the archive goes away, search keeps working. The index lives in the container. Only new files and reading originals are missing.
| What you see | What helps |
|---|---|
| «nasindex erreicht das Archiv nicht»nasindex cannot reach the archive | Path, server, share, user or password are wrong. Start the wizard again and answer the questions anew. |
| Claude cannot find the server | Is Tailscale active on both devices? Address, port and key correct? Claude Desktop fully restarted? |
| Claude cannot find a new file | The next cycle picks it up. Right away: entrypoint.py build, see Operation. |
scan: missing-Abgleich uebersprungenscan: reconciliation of missing files skipped | A safety guard triggered because many files are suddenly missing. Nothing is lost. If a folder really was removed, see below. |
felder: Konfiguration unvollstaendig (…)felder: configuration incomplete | The named field extraction setting is missing or invalid. Start the wizard again. |
Kein Dokument passt in den KontextNo document fits into the context | Not an error of the run. Choose a model with more context or set the context size correctly. |
Folder removed on purpose
The safety guard prevents a briefly missing network drive from emptying the index. If the drop is intended, confirm it once:
docker exec nasindex python3 /app/nasindex.py scan --verschwunden-ok --root /archiv --index /daten --cache /daten --rolle indexer
Messages from archiv_status
| Code | Meaning |
|---|---|
erreichbar | The archive is there and has content. |
leer | The folder is empty, usually a wrong path. |
fehlt | The folder does not exist, or read permission is missing. |
haengt | No answer in time, probably a hanging connection to the NAS. |
Exit codes of build
| Code | Meaning |
|---|---|
0 | Success, including «nothing to do» |
1 | Configuration error or archive unreachable, the message names the reason |
75 | Archive source unreachable, or a safety guard triggered |
130 | Run aborted by a stop |
137 | Terminated from outside, often too little memory |
If you have questions, send Digital Apes the output of docker compose logs --tail 200 nasindex. It contains no credentials.