Activating the skill & daily workflow

Activating the research workflow skill

The skill is a markdown file that tells Claude Code how to behave during research sessions. One-time installation:

# Create the skills folder in your vault
mkdir -p ~/Documents/ResearchVault/.claude/skills

# Copy the skill file to the vault
cp SKILL.md ~/Documents/ResearchVault/.claude/skills/

Then add the following line to your CLAUDE.md (at the bottom):

## Active skills
- Read and follow `.claude/skills/SKILL.md` during every research session.

From that point on, the skill is active as soon as you open Claude Code in your vault. You can start the workflow by typing: /research or simply "start research workflow".


Daily workflow after installation

Once everything is set up, the daily workflow is straightforward. The feedreader runs automatically — no action required.

  1. Browse the filtered feeds in NetNewsWire (the three type-specific Atom feeds, synced through FreshRSS). Items are sorted by relevance score. Send interesting ones to Zotero _inbox via the Zotero browser extension or iOS app. Press 👎 on clearly off-topic items to give a negative signal to the learning loop. There is no longer an HTML reader — generate_html() was removed on 18 Apr 2026 and NetNewsWire plus FreshRSS took its place.
  2. Open Terminal in your vault: cd ~/Documents/ResearchVault && claude
  3. Activate the skill: type /research or "start research workflow"
  4. Claude Code asks an intake question and guides you interactively from there

You do not need to know exactly what you are looking for — the skill is designed to help you with that.

Full session flow

  1. Browse the filtered feed and forward interesting items to Zotero _inbox

  2. Open Terminal, navigate to your vault, and start Claude Code:

    cd ~/Documents/ResearchVault
    claude
    
  3. Activate the research workflow:

    /research
    

    or just type: start research workflow

  4. Optionally, run index-score.py first to prioritize your review:

    ~/.local/share/uv/tools/zotero-mcp-server/bin/python3 .claude/index-score.py
    

    This ranks all _inbox items by semantic similarity to your existing library (using the ChromaDB embeddings from zotero-mcp), so you know which items to focus on.

  5. Claude Code retrieves all items from your Zotero _inbox and presents each one with a short summary and relevance assessment — the Phase-2 preview summary is generated locally by summarize_item.py (fallback model qwen3.5:9b). You respond Go or No-go per item.

  6. For each Go: Claude Code builds a canonical bundle with build-zotero-bundle.py (writing raw/{citekey}__{itemKey}.md). That bundle is the intake artifact, and it is where Phase 3 ends.

    Turning bundles into a wiki is handled by whichever backend is configured for the vault — see Choosing a backend.

  7. For each No-go: Claude Code removes the item from _inbox (after your confirmation).

  8. At the end of the session, Claude Code shows a summary: X approved, Y removed. The Zotero semantic search database is updated automatically each day as part of the login-triggered morning batch job (nl.<jouwnaam>.nachtelijke-taken daemon) — no manual action needed before a session. If you process items later in the day and want the database to reflect them immediately, run:

    zotero-mcp update-db --fulltext # recommended (includes full text, 5–20 min on Apple Silicon)
    

    Or use the alias: update-zotero. Check database status with zotero-mcp db-status.


Helper scripts

The workflow uses three helper scripts in .claude/. They keep source content out of Claude Code's context and handle Zotero write operations.

fetch-fulltext.py — retrieve and save attachment text

Fetches the full text of a Zotero attachment and saves it to a local file. Only prints status; never prints content.

~/.local/share/uv/tools/zotero-mcp-server/bin/python3 .claude/fetch-fulltext.py ITEMKEY inbox/bron.txt
# Output: Saved: inbox/bron.txt (12,345 chars, type: application/pdf)

For HTML snapshots the script extracts only the main article text via trafilatura (extract_article_text()), stripping navigation/ads/comments so full-page snapshots (e.g. Tweakers) don't bloat the bundle or slow olw ingest; it falls back to a naive tag-strip if trafilatura is unavailable, returns nothing, or returns fewer than 300 words — the same threshold at which build-zotero-bundle.py rejects a bundle, so below it the choice is between boilerplate and nothing at all. Until 22 August 2026 the fallback only checked whether the extraction was empty, which a degenerate extraction is not: four Skipr and Zorgvisie articles with perfectly good snapshots yielded 58, 183, 52 and 239 words where the naive strip found 498, 587, 966 and 717, and all four were rejected as empty. Across all 198 snapshots in the Zotero store, 56 go from rejected to usable. The threshold is absolute rather than a ratio, because producing far less text than the naive strip is exactly what trafilatura is for. trafilatura must be installed in the zotero-mcp venv.

ollama-generate.py — generate text via local LLM (Ollama or MLX)

Calls a local LLM REST API directly (no CLI, no ANSI codes). Supports two backends:

  • ollama (default): Ollama REST API on localhost:11434
  • mlx: mlx_lm OpenAI-compatible server on localhost:8080

Backend is selected via --backend ollama|mlx or the LLM_BACKEND env var in ResearchVault/.env. Prints only status lines.

By default the reasoning step is suppressed: /no_think is prepended to the instruction and think: false goes into the Ollama payload. Pass --denk to keep reasoning on. Note that --no-think is a no-op left over from an earlier version — it is a store_true flag whose default is already True, so it cannot switch anything off; --denk is the counterpart that actually does. Reasoning is markedly slower, so whether it is on belongs in any report that compares model output.

~/.local/share/uv/tools/zotero-mcp-server/bin/python3 .claude/ollama-generate.py \
  --input  inbox/bron.txt \
  --output raw/notitie.md \
  --prompt "Summarise this source in Dutch..." \
  [--backend ollama|mlx]
# Output: Input: inbox/bron.txt (12,345 chars) | Model: mistral-small:22b | backend: ollama | Written: raw/notitie.md (3,200 chars)

zotero-remove-from-inbox.py — remove processed item from _inbox

Removes the item from the _inbox collection in Zotero. Uses the local Zotero API by default (requires Zotero desktop running); mode is controlled by ZOTERO_ACCESS (see zotero_api.py).

~/.local/share/uv/tools/zotero-mcp-server/bin/python3 .claude/zotero-remove-from-inbox.py ITEMKEY
# Output: Item ITEMKEY removed from _inbox.