Welcome to journal
You write notes every day — decisions you made, bugs you chased, ideas you want to revisit. But finding them again when you need them? That's the hard part.
journal is a command-line tool that turns a folder of plain Markdown files into a searchable, AI-queryable personal knowledge base. You write naturally. journal handles the rest: it keeps your notes in git, builds a local search index, and lets you ask questions in plain English — without sending your notes to any cloud service.
What you can do with it
- Capture a thought in seconds. One command, and your note is timestamped, saved, and committed to git.
- Find anything, instantly. Semantic search understands what you meant, not just what you typed. "How did we handle the auth issue last month?" actually finds the right note.
- Get AI summaries. Ask journal to summarize your week, surface your open questions, or digest your recent meetings — using Claude, a free cloud model, or a local model running entirely on your machine.
- Bring your meetings in. Pull Quill transcripts or any recording into the same search index. Your notes and your meetings, searchable together.
- Stay on top of your todos. Drop
@todoanywhere in a note. journal tracks them, lets you check them off, and even exposes them to Claude so your AI assistant can help. - Keep your data yours. Everything runs locally by default. No account, no server, no subscription. Your notes stay in plain Markdown files in a git repository you own.
Who this is for
journal is built for developers, but you don't need to be technical to use it once it's set up. If you're comfortable with a terminal and want a better way to capture and retrieve your thinking, journal will feel immediately useful.
The Installation guide gets you up and running in about ten minutes.
A note on privacy: journal indexes your notes locally using Ollama, an open-source tool that runs AI models on your own computer. Your notes never leave your machine for search or indexing. If you use the AI synthesis features, you can choose between cloud providers or keep everything local too. You're always in control.
Looking for contributor or developer documentation? The technical reference lives in the docs/ folder on GitHub.
What is journal?
journal is a tool for capturing, searching, and making sense of your notes — all on your own computer, without a cloud service in the middle.
The basic idea
Your notes live as plain Markdown files in a git repository. When you run journal capture, it adds a timestamped entry to today's file and commits it. Your history is always safe in git.
The clever part is retrieval. journal uses a technique called semantic search (powered by a local AI model via Ollama) to understand what your notes mean, not just the exact words they contain. When you search for "the auth issue from last month," you'll find the right entry even if you used different words when you wrote it.
Local-first means your data stays yours
journal is designed around a simple principle: your notes are yours. The search index is built entirely on your machine. No notes are uploaded to build it, and no account is required to use it.
The vector index (the database that powers semantic search) is a disposable cache — you can delete and rebuild it at any time from your Markdown files. The Markdown files are the source of truth.
Optional AI synthesis
If you want more than search, journal can generate summaries and digests using a language model. You have three options:
- Cloud Claude (the default): great quality, requires an Anthropic API key
- OpenAI-compatible endpoint (OpenRouter, Groq, etc.): flexible, often cheaper
- Local Ollama model: zero egress, fully offline, no API key needed
You can switch providers at any time in the config file. If you skip synthesis entirely, journal still works great — you just get search without AI-generated summaries.
Just a binary
journal ships as a single static binary. No runtime, no daemon, no Docker. Install it, point it at a folder, run journal init, and you're ready.
It works from the command line — pair it with any text editor you like, or use journal tui for a built-in interactive dashboard.
In a sentence
journal turns a folder of Markdown notes into a searchable knowledge base with optional AI synthesis, all running locally on your machine.
Installation
journal ships as a single static binary for macOS and Linux. Pick the method that works best for your setup.
macOS — Homebrew (recommended)
brew install ericmann/tap/journal
Homebrew handles the quarantine flag automatically, so you won't see an "Allow Anyway" security prompt.
macOS and Linux — install script
This script downloads the latest release, verifies its checksum, and installs it on your PATH:
curl -fsSL https://raw.githubusercontent.com/ericmann/journal/main/install.sh | sh
Linux — packages
Each release includes .deb, .rpm, and .apk packages for amd64 and arm64:
sudo dpkg -i journal_*_linux_amd64.deb # Debian / Ubuntu
sudo rpm -i journal_*_linux_amd64.rpm # Fedora / RHEL
sudo apk add --allow-untrusted journal_*_linux_amd64.apk
Verify the installation
journal --help
You should see a list of available commands. If that works, move on to installing Ollama.
Install Ollama (required for search)
journal uses Ollama to build the search index on your machine. This is what makes local semantic search possible — no API key, no data leaving your computer.
macOS:
brew install ollama
brew services start ollama # runs Ollama as a background service
Linux:
curl -fsSL https://ollama.com/install.sh | sh
# Ollama is installed as a systemd service and starts automatically
Pull the embedding model
Once Ollama is running, pull the model journal uses for indexing:
ollama pull qwen3-embedding:4b
This downloads a ~2.5 GB model. It only runs on your machine, and it only runs when journal is indexing or searching.
Check everything is working
journal doctor
journal doctor checks that Ollama is reachable, the embedding model is present, and the search index is healthy. If something is misconfigured, it tells you exactly what to fix.
Next step
Now that journal is installed, head to Your First Day to initialize a journal and capture your first note.
Build from source (Go 1.26+)
git clone https://github.com/ericmann/journal.git
cd journal
make install
This builds a version-stamped binary and installs it to /usr/local/bin. You can override the install prefix with PREFIX=/your/path make install.
Your First Day
You've installed journal and Ollama. Let's get your first journal set up and capture something in it.
Initialize a journal
Navigate to the folder where you want to keep your notes, then run:
journal init
This scaffolds a few directories and a config file:
.journal/config.yaml your settings (committed to git)
.journal/index/ the search index (gitignored — rebuilt from your notes anytime)
daily/ your daily notes land here
projects/ long-running threads go here
reflections/ AI synthesis output
journal also creates a .gitignore that excludes the search index — it's a cache, not source of truth.
If you already have a git repository in this folder, that's fine.
journal initworks in existing repos too.
Capture your first note
journal capture "Set up journal — this is going to be useful"
That's it. journal creates a file at daily/YYYY/MM/YYYY-MM-DD.md (today's date), appends a timestamped entry, and commits it to git. You didn't have to open a file, format anything, or remember where you put it.
Want to write something longer? Leave off the text and your editor opens:
journal capture
Journal follows $EDITOR, or falls back to nano if none is set.
Build the search index
Before you can search, journal needs to embed your notes:
journal index
This runs through your notes and builds the local search index using Ollama. It only processes notes that are new or changed since the last index run.
Search
Now try a search:
journal search "what did I set up today"
Journal embeds your query and finds the most relevant notes — semantically, not just by keyword. You'll see the matching entry, the file it came from, and the exact lines.
See today's notes at a glance
journal today
This shows everything you've captured today, your open todos, and any meetings. A useful command to run at the start or end of your day.
Keep the index fresh
As you capture more notes, you'll want the search index to stay current. The easiest way is to run the watcher in a background terminal:
journal index --watch
This stays running, notices when your notes change, and re-indexes them automatically. See Capturing Notes for more on the watcher and auto-indexing options.
You're up and running. The next sections walk through each feature in more detail — but honestly, journal capture and journal search will cover 80% of your daily use.
Capturing Notes
Every note you capture goes into a plain Markdown file. This means you can always read, edit, or move your notes with any text editor — journal doesn't lock you in.
The basic capture
journal capture "the redis cache is evicting too aggressively under load"
This appends a timestamped block to today's daily file (daily/YYYY/MM/YYYY-MM-DD.md) and auto-commits it to git.
Adding tags
Tags help you find related notes later. Add them inline with #:
journal capture "redis eviction is too aggressive under load #redis #infra"
Or pass them as a flag:
journal capture "redis eviction issue" --tags redis,infra
Both ways work, and journal deduplicates them. Tags are case-insensitive. When you search later, you can filter by tag with --tag redis.
Adding markers
Markers are special annotations that the search and synthesis layers understand:
@decision— a choice you made@question— something you're still working out@todo— an action item (see Tracking Todos)
Add them inline:
journal capture "decided to use Redis Cluster instead of Sentinel @decision #redis"
Or as a flag:
journal capture "use Redis Cluster" --marker decision
Later, journal decisions lists everything you marked @decision, and journal search can filter by marker.
Writing longer notes in your editor
Leave off the text and journal opens your editor:
journal capture
Write your note, save, and close. Journal uses $JOURNAL_EDITOR, then $VISUAL, then $EDITOR, then falls back to nano.
You can also pipe content in from stdin:
cat meeting-notes.md | journal capture
Organizing by project
By default, notes go into the daily file. If you're tracking something longer-term, route it to a project:
journal capture "pricing model decision: go with usage-based @decision" --project acme-launch
This writes to projects/acme-launch/notes/YYYY-MM-DD.md. You can search, filter, and synthesize by project independently of your daily notes.
Capturing from any directory
You can use journal from any folder in your terminal — just tell it where your journal lives:
journal capture "quick idea" --journal-dir ~/notes
Or set it once in your environment:
export JOURNAL_DIR=~/notes
After that, every journal command works from any directory.
Auto-commit
journal automatically commits your notes to git after each capture. This means your words are in version control the moment you write them — no git add, no git commit, nothing to forget.
The commit message is generated automatically, something like:
📓 scribbled notes — +1 new, ~1 revised -0 removed · Mon 2026-06-01 12:32
If you prefer to manage commits yourself, set git_autocommit: false in .journal/config.yaml.
Keeping the index current
Capture saves your note immediately, but doesn't update the search index. Run journal index to embed new notes, or keep the watcher running so indexing happens automatically:
journal index --watch
The watcher watches your notes folder, debounces changes, and re-indexes only what's changed. Run it in a background terminal or set it up as a background service (see the Configuration Reference).
Tracking Todos
journal has a lightweight todo system built directly into your notes. You don't need a separate task manager — just drop @todo into any note you capture, and journal tracks it for you.
Adding a todo
Write @todo anywhere in a captured note:
journal capture "follow up with the Acme team on pricing @todo"
journal capture "check if the Redis eviction fix is still holding @todo #redis"
Or use the marker flag:
journal capture "write up the architecture decision" --marker todo
Once indexed, the todo appears in journal todos.
Listing your todos
journal todos
You'll see a numbered list with a snippet of each todo and the file and line it came from:
1. daily/2026/06/2026-06-12.md:8 — follow up with the Acme team on pricing
2. daily/2026/06/2026-06-11.md:14 — check if the Redis eviction fix is still holding
3. projects/acme-launch/notes/2026-06-10.md:5 — write up the architecture decision
Those file:line references are exact — you can open the file in your editor at that line to read the full context.
Checking off a todo
Mark a todo as done by passing part of its text or its citation:
journal done "follow up with the Acme team" # by text fragment
journal done daily/2026/06/2026-06-12.md:8 # by citation
journal rewrites that one @todo to @done 2026-06-12 in your note file (the rest of the note is untouched), re-indexes it, and auto-commits. It's the Markdown equivalent of checking a checkbox.
Viewing completed todos
journal todos --done # only completed items
journal todos --all # everything: open and done
Filtering
journal todos --project acme-launch # todos in a specific project
journal todos --since 1w # open items from the last week
Bulk dismissal
When todos pile up and become irrelevant all at once — a project wraps up, a planning doc is superseded — journal dismiss clears them in a single command and a single git commit:
# Dismiss all open todos in a project:
journal dismiss --project acme
# Dismiss todos older than 4 weeks:
journal dismiss --before 4w
# Combine filters, skip the prompt:
journal dismiss --project janus --before 2w --yes
# Attach a resolution note to each dismissed todo:
journal dismiss --project acme --resolution "superseded by new plan" --yes
dismiss shows you the matched set before changing anything and asks for confirmation (answer y, or pass --yes to skip the prompt). Each @todo is rewritten to @done YYYY-MM-DD — exactly what journal done does — and all the rewrites land in one git commit. At least one filter is required.
Todos via Claude
If you use journal with Claude Desktop or Claude Code, Claude can check off todos for you. The todos and done tools are exposed through the MCP server, so you can ask:
"What are my open todos? Mark the Acme pricing follow-up as done."
Claude calls the same done command behind the scenes, and the rewrite happens in your actual Markdown files.
Searching Your Journal
journal's search understands what you mean, not just what you typed. This matters because you rarely remember your exact words from six months ago — but you remember the idea.
Basic search
journal search "how did we handle the auth token expiry issue"
journal embeds your question (using the same local Ollama model that indexed your notes), finds the most similar passages, and returns them with file and line citations.
● daily/2026/05/2026-05-14.md:23-27 (score: 0.91)
## 14:32 #auth #backend
Decided to use short-lived JWTs (15 min) with a sliding refresh window.
The key insight: treat the refresh token as the session, not the JWT itself.
The path:line_start-line_end reference is clickable in most terminals and editors. You can open that exact section of your notes instantly.
Getting an AI answer (optional)
If you have ANTHROPIC_API_KEY set in your environment, journal search also generates a short grounded answer above the raw results — synthesized from the matching notes, not from general knowledge. If the notes don't cover the question, it says so.
journal search "what did we decide about the auth approach"
# → AI answer: "Based on your notes from May 14, you decided to use short-lived
# JWTs with a sliding refresh window, treating the refresh token as the session..."
# → raw results follow
Pass --no-answer to skip the AI summary, or --answer to require it (and fail if no key is set). The --json flag never includes the AI answer.
Filtering results
By tag:
journal search "caching strategy" --tag redis
By project:
journal search "pricing decision" --project acme-launch
By time:
journal search "deployment approach" --since 2w # last 2 weeks
journal search "auth design" --since 3m # last 3 months
By number of results:
journal search "redis" --k 10 # top 10 results (default is 5)
Other retrieval commands
Recent notes
journal recent # newest notes first
journal recent --tag redis # filtered
journal recent --since 1w # last week
Decisions
journal decisions # all your @decision notes
journal decisions --project acme-launch # just for one project
journal decisions --since 4w # last month
Project threads
journal threads # all active projects with recent activity
journal threads --stale # projects with no activity in 14+ days
Today's notes
journal today # today's notes, open todos, and meetings
journal show # render today's notes (or pass a date/path)
journal show 2026-05-14 # a specific day
Machine-readable output
Every retrieval command supports --json for use in scripts or with AI tools:
journal search "auth" --json | jq '.results[].path'
journal decisions --json | jq '.results[].snippet'
The schema is stable across versions. An empty result set looks like {"results": []}, not an error — so you can tell the difference between "found nothing" and "something went wrong."
Managing tags
journal tags lists every distinct #tag in your indexed corpus with its usage count — useful for spotting typos and inconsistencies (#redis vs #Redis vs #redis-cache):
journal tags # list all tags with usage counts, sorted by frequency
journal tags --json # machine-readable: {"tags": [{"tag": "redis", "count": 12}, ...]}
Renaming a tag
journal tags rename redis redis-cache # rewrite #redis → #redis-cache in all notes
journal tags rename redis redis-cache --dry-run # preview which files would change
rename rewrites the tag across all matching notes, re-indexes the changed files, and auto-commits — one command to tidy the whole corpus. The leading # is optional on both arguments.
How semantic search works (the short version)
When you run journal index, each heading block in your notes gets turned into a vector (a list of numbers) that represents its meaning. Your search query gets the same treatment. journal then finds the notes whose vectors are closest to your query's vector — "closest" here means "most similar in meaning."
The model that does this runs entirely on your machine via Ollama. No notes are sent anywhere.
AI Synthesis
journal synth reads your notes and generates curated summaries — weekly digests, daily rollups, decision histories, stale project alerts — using a language model of your choice. It never runs automatically; you invoke it when you want it.
The basics
journal synth weekly # preview: prints the prompt and output path, no API call
journal synth weekly --write # actually generates and saves the output
By default, synthesis is a dry run: it shows you the prompt it would send and the file it would write, but doesn't call any API. Add --write when you're ready.
Output files land in reflections/ in your journal repo (e.g. reflections/2026-W24.md). Existing files are never overwritten — journal adds a -2, -3, etc. suffix if one already exists.
Synthesis kinds
| Command | What it generates |
|---|---|
journal synth weekly | A summary of your ISO week's notes → reflections/YYYY-Www.md |
journal synth daily | Today's notes at a glance → reflections/daily-YYYY-MM-DD.md |
journal synth daily --date 2026-06-02 | A specific day |
journal synth meetings | Digest of recent meeting transcripts (last 7 days by default) |
journal synth decisions --project acme | A rollup of @decision notes for a project |
journal synth stale --days 21 | Surface threads you haven't touched in 3 weeks |
Choosing a provider
You have three options, set in .journal/config.yaml:
Cloud Claude (default)
synth_provider: anthropic
synth_model: claude-sonnet-4-6
Requires an Anthropic API key in ANTHROPIC_API_KEY. Best quality for long-form synthesis and voice-matching. Your note excerpts are sent to Anthropic's API.
OpenAI-compatible (OpenRouter, Groq, etc.)
synth_provider: openai
synth_openai_base_url: https://openrouter.ai/api/v1
synth_openai_model: google/gemma-3-27b-it:free
Requires an API key in OPENAI_API_KEY. This is the middle path: capable cloud synthesis without a Claude bill. Free models are available on OpenRouter.
Local Ollama (fully offline)
synth_provider: ollama
synth_ollama_model: gemma4:12b
No API key, no data leaving your machine. For daily summaries and decision rollups, the quality is close to cloud Claude. For long-form weekly digests where voice matters a lot, cloud models have the edge — but for many workflows, local is excellent.
Pull the model first: ollama pull gemma4:12b
Scheduling synthesis
Run journal synth daily --write on a schedule to get automatic daily digests. Add it to cron (at, say, 11:55 PM):
55 23 * * * /usr/local/bin/journal synth daily --write >> ~/.journal-synth.log 2>&1
Or use launchd (macOS) / systemd timers (Linux) — the same patterns as the index watcher. See Configuration Reference for details.
Writing in your voice
If you create a file at docs/VOICE_PROFILE.md in your journal repo, journal reads it at synthesis time and injects it as a style reference. The model uses it to match your vocabulary, tone, and any phrases you want to avoid.
journal init creates a starter template at docs/VOICE_PROFILE.example.md — copy it, make it yours, and your digests will start sounding more like you over time.
The profile is plain Markdown. Evolve it whenever you notice the output drifting from your style.
Meetings & Transcripts
journal can bring your meeting transcripts into the same search index as your notes. Once they're there, you can search across everything — your captured thoughts and your meeting discussions — in one place.
Two ways to add meeting transcripts
Option 1: Quill (macOS and Windows)
Quill records your meetings and stores everything in a local SQLite database. journal reads that database and renders your meetings to Markdown:
journal quill-sync # pull new meetings into transcripts/
journal index # embed them (or let `journal index --watch` do it automatically)
Quill is only available on macOS and Windows. If you're on Linux, use the manual transcript path below.
Option 2: Any recording (via WhisperX)
For Zoom calls, recorded presentations, voice memos — anything you have as an audio or video file — journal can transcribe and ingest them using WhisperX:
# Step 1: transcribe with WhisperX (one-time Python setup required)
python scripts/transcribe.py meeting.mp4 --min-speakers 2 --max-speakers 8
# Step 2: ingest into journal
journal transcribe ./out/meeting.json --title "Q2 Planning" --date 2026-06-02
journal transcribe renders the transcript to Markdown, generates an AI summary at the top (using your configured synthesis provider), and indexes it immediately.
The summary is important: a two-hour meeting is hundreds of search chunks, so the AI notes at the top are what search hits first, instead of making you trawl the full transcript.
Option 3: Drop in a .qm file
If you have a Quill .qm export file, drop it into your transcripts/ folder. With quill.accept_qm_imports: true (the default), the watcher picks it up and renders it automatically. This works on Linux too.
Using your meeting transcripts
Once synced and indexed, your transcripts are searchable like any other note:
journal search "what did we decide about the pricing model" --source transcript
journal search "anything about deployment" --source all # notes + transcripts
List recent meetings:
journal meetings
Get an AI digest of the last week of meetings:
journal synth meetings --write
View a specific meeting:
journal show transcripts/2026-06-02-q2-planning.md
Keeping transcripts fresh
Set up a schedule to run quill-sync regularly. If you're also running journal index --watch, the watcher embeds newly-synced transcripts automatically:
# In cron: sync Quill meetings every hour, then re-index
0 * * * * /usr/local/bin/journal quill-sync && /usr/local/bin/journal index
If the watcher is already running, you only need to schedule quill-sync.
WhisperX setup (one-time)
WhisperX requires Python 3.11 and ffmpeg. Set up a virtual environment:
brew install ffmpeg # macOS; or your package manager
python3.11 -m venv ~/.venvs/whisperx
source ~/.venvs/whisperx/bin/activate
pip install -r scripts/requirements.txt # in your journal source directory
You'll also need a free Hugging Face token (for speaker diarization), and you'll need to accept the terms for two gated models:
- https://huggingface.co/pyannote/segmentation-3.0
- https://huggingface.co/pyannote/speaker-diarization-3.1
Export the token in your shell profile: export HF_TOKEN=hf_...
Transcription is the slow step — a two-hour meeting can take a while on CPU. On a GPU-equipped machine it's much faster.
A Day in the Life
Here's what a typical day with journal looks like — from morning standup to end-of-day wrap.
Morning: get your bearings
Start the day by checking what's on your plate:
journal today
This shows today's notes (empty at the start of the day), your open todos, and any meetings that were indexed overnight. It's a quick 10-second pulse check. The notes section aggregates everything captured today — the daily note, any per-project notes (journal capture --project foo …), and more — so you see the full picture regardless of where each note landed.
If you run the index watcher, it's already running in the background from yesterday. If not, kick it off now:
journal index --watch & # or in a dedicated terminal tab
During the day: capture as you go
The goal is to capture with as little friction as possible. Don't format, don't organize — just get it out:
journal capture "standup: blocked on auth PR review, picking up #auth after"
journal capture "discovered the redis timeout is 5s not 500ms, explains the latency spikes #redis @decision"
journal capture "need to follow up with Sarah about the API contract @todo #acme-launch"
Each capture is timestamped, committed to git, and (if the watcher is running) searchable within seconds.
When something needs more thought, open your editor:
journal capture # opens $EDITOR, write freely, save and close
Midday: find something from last week
You half-remember something about a caching approach from a few weeks ago:
journal search "caching strategy for the API layer"
journal finds the right notes even if you used different words at the time. The results come with file:line citations you can open directly.
If you have a synthesis provider configured, you'll also get a short AI answer that synthesizes the relevant notes for you.
Need to see all your open action items?
journal todos
Mark one done:
journal done "follow up with Sarah"
End of day: review and synthesize
Check what you captured:
journal today
If you have synthesis configured, generate a daily digest:
journal synth daily --write
This writes a summary to reflections/daily-YYYY-MM-DD.md — useful for standups, weekly reviews, or just reminding yourself what you accomplished.
Interactive mode
Prefer a terminal UI over individual commands? journal tui gives you an interactive dashboard:
journal tui
The TUI has tabs for:
- Today — your daily notes, rendered
- Todos — open action items; press
dto complete the selected one - Search — type a query, hit enter; results update in real time
- Recent / Meetings — latest notes and transcripts
- Stats — capture volume, streaks, tag breakdown
Use tab / shift+tab or 1–6 to switch tabs; q to quit.
Weekly rhythm
At the end of the week:
journal synth weekly --write
This generates a digest of the whole week at reflections/YYYY-Www.md. Over time you'll accumulate a searchable archive of weekly summaries — a natural project history.
Check stale projects:
journal threads --stale
This surfaces anything you haven't touched in two weeks, so you don't lose track of things you meant to come back to.
Stats
journal stats
Shows capture volume, your current and longest streaks, marker counts (open todos, decisions, questions), and your top tags. Satisfying to look at on a Friday.
Configuration Reference
Your journal's settings live in .journal/config.yaml in your journal repository. This file is committed to git — it travels with your notes. The only thing that's never in this file is API keys; those come from environment variables.
Run journal doctor after changing any model setting to verify your configuration.
Full default config
# --- Embedding & retrieval ---
embed_provider: ollama
embed_model: qwen3-embedding:4b
embed_openai_base_url: https://api.openai.com/v1
embed_openai_model: ""
embed_dim: 2560
reranker: ""
ollama_base_url: http://localhost:11434
chunk_strategy: heading
retrieval_instruction: "Represent this query for retrieving relevant developer journal notes:"
store_path: .journal/index/journal.db
excludes:
- reflections/**
- .journal/**
- docs/**
- README.md
# --- Capture ---
editor: ""
# --- Synthesis ---
synth_provider: anthropic
synth_model: claude-sonnet-4-6
synth_ollama_model: gemma4:12b
synth_openai_base_url: https://api.openai.com/v1
synth_openai_model: ""
synth_num_ctx: 32768
synth_max_tokens: 4096
voice_profile: docs/VOICE_PROFILE.md
# --- Egress kill-switch ---
local_only: false
local_only_mcp: block
# --- Git integration ---
git_autocommit: true
git_autocommit_sign: false
# --- Remote backup ---
sync_enabled: false
sync_conflict: manual
# --- Meeting transcripts / Quill ---
transcripts:
enabled: true
path: transcripts
format: auto
auto_index: true
tag: meeting
log_captures: false
quill:
enabled: true
db_path: ~/Library/Application Support/Quill/quill.db
accept_qm_imports: true
schema_version: "2.0"
Key reference
Embedding & search
| Key | Default | What it does |
|---|---|---|
embed_provider | ollama | Where embeddings come from: ollama (local, recommended) or openai (any OpenAI-compatible endpoint). |
embed_model | qwen3-embedding:4b | The Ollama model used to embed your notes and queries. Pull it with ollama pull qwen3-embedding:4b. |
embed_dim | 2560 | Must match your embedding model's output size. Run journal doctor to check — it probes the model and tells you the right value. If you change models, run journal index --rebuild. |
reranker | "" (off) | Optional Ollama model (e.g. qwen3:4b) for re-ranking search results. Off by default; vector search alone is strong. |
ollama_base_url | http://localhost:11434 | Where Ollama is running. Change this only if you've moved Ollama to a different port or host. |
store_path | .journal/index/journal.db | Path to the search index. This file is gitignored and can be deleted and rebuilt at any time. |
excludes | (see above) | Files and folders the indexer skips. reflections/ (synthesis output) and .journal/ (the index) are excluded by default. |
Capture
| Key | Default | What it does |
|---|---|---|
editor | "" | The editor for journal capture with no text. Run as a shell command, so code --wait works. Empty falls back to $JOURNAL_EDITOR, then $VISUAL, then $EDITOR, then nano. |
Synthesis
| Key | Default | What it does |
|---|---|---|
synth_provider | anthropic | Who runs synthesis: anthropic (cloud Claude), ollama (local), or openai (any OpenAI-compatible endpoint like OpenRouter or Groq). |
synth_model | claude-sonnet-4-6 | Anthropic model, used when synth_provider: anthropic. |
synth_ollama_model | gemma4:12b | Local model, used when synth_provider: ollama. Pull it with ollama pull gemma4:12b. |
synth_openai_base_url | https://api.openai.com/v1 | API endpoint when synth_provider: openai. For OpenRouter: https://openrouter.ai/api/v1. |
synth_openai_model | "" | Model ID for the OpenAI-compatible provider, e.g. google/gemma-3-27b-it:free on OpenRouter. |
synth_num_ctx | 32768 | Context window for Ollama synthesis calls. Always set explicitly — Ollama's default is 4096 and it truncates silently. |
voice_profile | docs/VOICE_PROFILE.md | Path to a style reference that shapes how synthesis sounds like you. Optional — synthesis works without it. |
Privacy & egress
| Key | Default | What it does |
|---|---|---|
local_only | false | When true, blocks all cloud AI paths: cloud synthesis is refused, and journal mcp is blocked by default (see local_only_mcp). Use this for a fully air-gapped setup. |
local_only_mcp | block | Under local_only, whether journal mcp can run: block (default) or allow. Set to allow if your MCP client runs a local model and you're confident nothing leaves your machine. |
Git
| Key | Default | What it does |
|---|---|---|
git_autocommit | true | Auto-commit notes after capture and index. Turn off if you prefer to manage commits yourself. |
git_autocommit_sign | false | Sign auto-commits. Off by default so the watcher doesn't prompt for a signing passphrase. |
Remote backup
| Key | Default | What it does |
|---|---|---|
sync_enabled | false | Gates journal sync. Nothing happens until you set this to true. See Backup & Sync. |
sync_conflict | manual | How sync handles a divergence: manual (aborts, lets you resolve), prefer-upstream (takes the remote), prefer-local (keeps local). |
Meetings & transcripts
| Key | Default | What it does |
|---|---|---|
transcripts.enabled | true | Gates the transcript feature. |
transcripts.path | transcripts | Where rendered transcripts are written. Gitignored. |
transcripts.auto_index | true | Embed new transcripts as the watcher detects them. |
transcripts.tag | meeting | Tag applied to every transcript chunk. Find them with --tag meeting or --source transcript. |
quill.enabled | true | Gates journal quill-sync. |
quill.db_path | OS default | Path to Quill's local database. macOS: ~/Library/Application Support/Quill/quill.db. Windows: ~/AppData/Roaming/Quill/quill.db. |
quill.accept_qm_imports | true | Render manually-dropped .qm export files in the transcripts folder. |
API keys
API keys are read from environment variables only — never stored in config files or logged:
ANTHROPIC_API_KEY— forsynth_provider: anthropicOPENAI_API_KEY— forsynth_provider: openaiorembed_provider: openai
Set them in your shell profile (~/.zshrc, ~/.bashrc) or use a tool like direnv to set them per-project.
Running journal from any directory
Every command accepts --journal-dir or the JOURNAL_DIR environment variable:
export JOURNAL_DIR=~/notes
journal search "anything" # always uses ~/notes, regardless of current directory
This is useful for aliases: alias jc='journal capture --journal-dir ~/notes'
Going Fully Local
journal works entirely on your machine by default — your notes are never uploaded for indexing or search. But if you also want synthesis (AI summaries) to run locally, with zero data leaving your computer at all, you can set that up too.
This guide walks through a complete zero-egress setup using Ollama for both embeddings and synthesis.
What "fully local" means
By default, journal synth sends your note excerpts to Anthropic's cloud API. The local_only setup replaces that with a local Ollama model, so:
- Indexing: local (already the default)
- Search: local (already the default)
- Synthesis: local (this is what we're adding)
- MCP tools: optionally local, if your chat client runs locally too
Nothing leaves your machine.
Hardware requirements
| Machine RAM | Embedding model | Synthesis model |
|---|---|---|
| 16 GB | qwen3-embedding:4b (~2.5 GB) | llama3.1:8b (~5 GB) |
| 32–48 GB | same | gemma4:12b (~8–10 GB) — recommended |
| 64 GB+ | same | gemma4:26b (~20–24 GB peak) |
Ollama loads models on demand and unloads them after about 5 minutes of inactivity, so these are transient peaks, not standing costs.
Step 1: Install and start Ollama
macOS:
brew install ollama
brew services start ollama
Linux:
curl -fsSL https://ollama.com/install.sh | sh
# Installed as a systemd service, starts automatically
Verify it's running: ollama --version
Step 2: Pull both models
ollama pull qwen3-embedding:4b # for indexing and search (~2.5 GB)
ollama pull gemma4:12b # for synthesis (~8 GB, or pick a smaller model)
Step 3: Update your config
In your journal repo's .journal/config.yaml:
synth_provider: ollama
synth_ollama_model: gemma4:12b
local_only: true # block all cloud AI paths
local_only_mcp: allow # if you want to use journal with a local MCP chat client
The local_only: true flag is a hard kill-switch — it refuses cloud synthesis and requires Ollama to be on loopback. It doesn't affect journal sync, which backs up to your own git remote (not cloud AI).
local_only_mcp: allow is only needed if you plan to use journal mcp with a local chat client like Jan or LM Studio. Leave it as block (the default) until you've set up the client.
Step 4: Verify
journal doctor
The egress line should say something like:
local_only: no cloud-AI egress (synth local: gemma4:12b); mcp blocked by policy
Try it:
journal synth daily # dry run: shows the prompt, no network call
journal synth daily --write # runs locally, writes reflections/daily-YYYY-MM-DD.md
journal search "what did I work on" # semantic search, fully local
Optional: Enable the reranker
The reranker is a local quality boost for search results. Without it, results are in vector-distance order — already high quality for most queries. With it, the top vector-KNN candidates are re-scored by a small generate model before returning, giving a precision lift especially on longer or ambiguous queries.
Pull a small generate model and add one line to your config:
ollama pull qwen3:4b # ~2.5 GB — the recommended reranker model
In .journal/config.yaml:
reranker: qwen3:4b
That's it. journal doctor confirms the reranker is configured. It runs on the same loopback Ollama — no network egress. The extra RAM cost is transient (~2.5 GB while reranking; Ollama unloads the model after ~5 min of inactivity).
The reranker is off by default. qwen3-embedding:4b is strong enough for most queries on its own; the reranker matters most when you're getting subtly irrelevant hits and want to tighten precision.
Adding a local MCP chat client (optional)
If you want to chat with your notes using a local AI model (no Claude), you can pair journal's MCP server with Jan or LM Studio. Both can connect to Ollama and call journal tools.
See Local MCP Clients for setup instructions for each client.
Quality expectations
For summarization tasks — daily digests, weekly rollups, decision rollups — local models like gemma4:12b perform very close to cloud Claude. The gap shows most on long-form stylistic writing where voice profile matching matters a lot.
A practical approach: run daily synthesis and search answers locally, and keep weekly digests on synth_provider: anthropic if you care about the writing quality. You can switch providers with a single line in config.
Backup & Sync
journal auto-commits your notes to git the moment you capture them. journal sync is the next step: it gets those commits off your machine to a git remote, and pulls in notes you may have captured from another device.
Sync is off by default. Nothing happens until you explicitly enable it. This is by design — pushing to and pulling from a remote is a bigger operation, and you should opt in deliberately.
What sync does
When you run journal sync, it:
- Commits any pending note changes
- Fetches the remote
- Pushes your local commits if you're ahead
- Merges remote changes if you're behind (and re-indexes new notes)
- Handles divergence according to your
sync_conflictsetting
Setting it up
1. Add a remote
Point your journal repo at a git remote (a private GitHub repo works well):
git remote add origin [email protected]:you/your-journal.git
git push -u origin HEAD
2. Enable sync in config
In .journal/config.yaml:
sync_enabled: true
sync_conflict: manual # see below
3. Test it
journal sync --dry-run # preview: shows what would happen without doing it
journal sync # do it for real
Conflict modes
If you capture notes on more than one machine, the remote and your local clone can diverge (both have new commits). How journal resolves that is up to you:
| Mode | What happens on a conflict | Best for |
|---|---|---|
manual (default) | Aborts cleanly, tells you to run git pull | Multiple machines, you want control |
prefer-upstream | Takes the remote version on any conflict | Single authoritative remote |
prefer-local | Keeps your local version on any conflict | This machine is the source of truth |
prefer-upstream and prefer-local resolve conflicts automatically. The losing side's changes disappear from the working tree (they're still in git history). Only opt in if you know what you're doing.
A clean fast-forward (one side is simply ahead) always just works, regardless of this setting.
Running sync on a schedule
journal init drops a helper script at .journal/sync.sh. Wire it to a cron job for hourly backups:
# back up the journal every hour
0 * * * * /path/to/journal/.journal/sync.sh >> /path/to/journal/.journal/sync.log 2>&1
macOS (launchd): see .journal/README.md in your repo for the full plist recipe.
Linux (systemd timer):
# ~/.config/systemd/user/journal-sync.timer
[Timer]
OnCalendar=hourly
Persistent=true # run a missed backup after the machine wakes up
systemctl --user enable --now journal-sync.timer
While sync_enabled: false, each run just prints a "sync is disabled" notice and exits harmlessly — safe to wire up before you're ready to enable it.
Sync and privacy
journal sync pushes your notes to a git remote you control — it's not sending data to a cloud AI service. If your remote is a private GitHub repository, your notes are as private as that repo.
It's independent of local_only. You can have local_only: true (no cloud AI) and sync_enabled: true (backup to your own remote) at the same time.
Claude Code
Claude Code is the primary way to use journal with an AI assistant. You can ask Claude natural language questions about your notes, and it will search, retrieve, and reason over them — all locally.
How it works
Claude Code calls journal's CLI commands (journal search --json, journal decisions --json, etc.) and reads their JSON output. Your notes stay on your machine; Claude Code just gets the search results back.
The integration uses journal's stable --json API, so the results are structured and consistent. Claude always knows exactly which file and lines a finding came from.
Setting up the skill
journal ships a Claude Code skill at skills/journal/SKILL.md. This teaches Claude how to use journal effectively — which commands to run, how to read the output, how to cite findings.
Make it discoverable in one of two ways:
Option 1: Keep your journal repo open as a workspace in Claude Code. Claude picks up the skill file automatically.
Option 2: Symlink it into your skills library:
ln -s "$PWD/skills/journal" ~/.claude/skills/journal
Make sure journal is on your PATH (journal --help works in your terminal).
What Claude can do
Once the skill is active, Claude can:
Search semantically:
"What did I decide about the authentication approach?"
→ runs: journal search "authentication approach" --json
Find decisions:
"What decisions did I make about the Canton project last month?"
→ runs: journal decisions --project canton --since 4w --json
Surface stale threads:
"What projects haven't I touched in two weeks?"
→ runs: journal threads --stale --days 14 --json
Capture notes:
"Note that we decided to use Redis Cluster @decision #redis"
→ runs: journal capture "decided to use Redis Cluster @decision #redis"
Check and complete todos:
"What are my open todos? Mark the Acme follow-up as done."
→ runs: journal todos --json, then: journal done "Acme follow-up"
Citations
Every result includes a path:line_start-line_end reference. Claude cites these in its responses, so you can open the exact section of your notes to verify. In most terminals and editors, these are clickable.
Using the MCP server (alternative)
If you prefer MCP over the CLI skill, you can register journal mcp as an MCP server for Claude Code. This gives Claude Code access to the same 13 tools available to Claude Desktop — no need to shell out to journal CLI commands manually.
Add to your Claude Code MCP config:
{
"mcpServers": {
"journal": {
"command": "/usr/local/bin/journal",
"args": ["mcp", "--repo", "/path/to/your/journal"]
}
}
}
See Claude Desktop for the full tool, resource, and prompt reference — both integrations share the same MCP server.
The JSON schema
If you want to use journal with your own scripts or agents, the search output looks like:
{
"results": [
{
"path": "daily/2026/06/2026-06-01.md",
"line_start": 3,
"line_end": 7,
"heading": "09:14 #cabot #litellm",
"snippet": "Routing fallback isn't triggering when Qwen OOMs...",
"score": 0.91,
"tags": ["cabot", "litellm"],
"markers": ["decision"]
}
]
}
An empty result set is {"results": []} — distinct from an error ({"error": "..."} with a non-zero exit code).
Claude Desktop
You can connect Claude Desktop to your journal using the MCP (Model Context Protocol). Once connected, Claude can search your notes, read specific entries, check your todos, and capture new notes — all from the Claude chat interface.
What is MCP?
MCP is a standard protocol that lets Claude Desktop connect to local tools and services. journal ships a built-in MCP server (journal mcp) that exposes your journal to Claude.
The inference (the AI reasoning) happens in Claude's cloud. The retrieval (the semantic search over your notes) happens locally on your machine. Your notes are shared with Claude as search results — not uploaded in bulk.
Setup
Add journal to Claude Desktop's MCP config file. Find the config at:
- macOS:
~/Library/Application Support/Claude/claude_desktop_config.json - Windows:
%APPDATA%\Claude\claude_desktop_config.json
Add this block (create the file if it doesn't exist):
{
"mcpServers": {
"journal": {
"command": "/usr/local/bin/journal",
"args": ["mcp", "--repo", "/Users/you/journal"]
}
}
}
Replace /usr/local/bin/journal with the actual path to your journal binary (find it with which journal), and /Users/you/journal with the path to your journal repository.
Use an absolute path for the command. Claude Desktop doesn't inherit your shell's PATH, so a relative path like
journalwon't work. Always use the full path.
Restart Claude Desktop, and the journal tools appear.
What Claude can do
Once connected, Claude has access to these tools:
| Tool | What it does |
|---|---|
search | Semantic search over your notes |
show | Read the full content of a specific note |
recent | See your most recent notes |
decisions | Find notes marked @decision |
threads | See project activity and stale threads |
meetings | List recent meeting transcripts |
todos | List open @todo items |
done | Complete a todo |
capture | Add a new note |
stats | Journal metrics: note volume, streaks, open todos, decisions, top tags |
today | Your day at a glance: today's note path, open todos, and meetings |
ask | Ask a question answered from your notes, with path:line citations |
synth | Run a synthesis job (weekly/daily/meetings/decisions/stale) and return the draft |
Try asking:
- "What did I work on this week?"
- "What decisions did I make about the Acme project?"
- "Add a note that we decided to go with Redis Cluster @decision"
- "What are my open todos? Mark the pricing one as done."
Resources
In addition to tools, journal exposes read-only resources that Claude can pull directly:
| Resource URI | Contents |
|---|---|
journal://today | Today's daily note (raw Markdown) |
journal://recent | The 50 most recent note chunks, newest first |
journal://projects/{slug}/index | A project's _index.md — replace {slug} with the project name |
Prompts
journal also provides pre-assembled prompts that Claude can run without hand-crafting the synthesis request. The server assembles context from your journal and returns it as a ready-to-run prompt — no cloud calls are made server-side:
| Prompt | Arguments | What it does |
|---|---|---|
weekly-reflection | — | Gathers this week's notes and assembles a weekly reflection prompt |
decisions-review | project (optional) | Gathers @decision notes and assembles a review prompt |
project-status | project (required), since (optional, e.g. 2w) | Gathers recent project notes and assembles a status prompt |
Privacy
The MCP server runs locally on your machine. Claude Desktop sends tool calls (like "search for X") to the local server, and the server runs the search and returns results. Your note content travels from your machine to Anthropic's cloud as part of Claude's context — the same as pasting text into a chat window.
If you want a fully local setup (including local AI inference), see Local MCP Clients.
Troubleshooting
Tools don't appear after restart: Check that the JSON is valid and the path to the binary is correct and absolute.
"Connection failed": Make sure journal --help works in your terminal and the path in the config matches.
Ollama needs to be running: The search tools require Ollama for embedding queries. Start Ollama before asking Claude to search your notes.
Local MCP Clients
Claude Desktop is great, but it sends your note content to Anthropic's cloud for inference. If you want fully local AI — where your notes, the search, and the AI reasoning all stay on your machine — you need a different chat client.
Three clients work well with journal: LM Studio, Jan, and AnythingLLM.
All three follow the same basic pattern: connect them to Ollama (for the AI model) and add journal as an MCP server.
Before you start
A few things apply to all local clients:
-
You need a tool-capable model. Not every model knows how to call tools. Gemma 4 and Qwen3 work reliably. Older or fine-tuned models may ignore tool calls entirely.
-
Use absolute paths. Local GUI apps don't inherit your shell's PATH. Always use the full path to the journal binary (e.g.
/opt/homebrew/bin/journal, not justjournal). Find the path withwhich journal. -
Ollama must be running. Start it with
brew services start ollama(macOS) or check that the systemd service is active.
LM Studio — lowest friction
LM Studio is the closest thing to Claude Desktop for local models. It has a clean interface, MCP support since v0.3.17, and per-call confirmation dialogs.
Note: LM Studio runs its own inference engine (llama.cpp/MLX) — it doesn't use Ollama. You'll download a second copy of the chat model in LM Studio's model store. If you're already using Ollama for journal's synthesis, you'll have two copies of the model. That's the trade-off for LM Studio's polished UX.
Setup
- Install LM Studio from lmstudio.ai (macOS/Windows/Linux).
- Download a model in the Discover tab — Gemma 4 12B works well.
- Go to Program sidebar → Install → Edit
mcp.json:
{
"mcpServers": {
"journal": {
"command": "/opt/homebrew/bin/journal",
"args": ["mcp", "--repo", "/Users/you/journal"]
}
}
}
- Start a chat with your downloaded model and ask "What did I work on this week?"
Jan — best open-source option
Jan is AGPL-licensed and actively maintained. It can use Ollama as its model provider, so you get one shared model runtime for both journal synthesis and the chat client.
Jan has some quirks you need to know about upfront — they're easy to fix but will trip you up silently if you skip them.
Setup
-
Install Jan from jan.ai.
-
Connect it to Ollama: Settings → Model Providers → add an OpenAI-compatible provider, base URL
http://localhost:11434/v1. Enter any non-empty placeholder as the API key (e.g.ollama) — Jan requires the field but Ollama ignores it. -
Enable tool calling for your model. This is the most-missed step. In the model's settings, turn on Tools / Function Calling. Without this, Jan never sends tool definitions to the model, and the model narrates ("I'm looking at your tools...") instead of actually calling them. The tell in Jan's trace: "No tools are available."
-
Add journal as an MCP server: Settings → MCP Servers →
+. Enter the arguments one per line:mcpon the first line,--repoon the second,/path/to/your/journalon the third. Don't put them all on one line — Jan doesn't split on spaces.macOS gotcha: System-level Smart Dashes can silently convert
--repoto—repo(an em dash), which breaks the flag. Paste instead of typing, or disable Smart Dashes in System Settings → Keyboard → Text Input → Edit. -
Use a minimal assistant. Jan's default assistant may have a web-search system prompt that competes with your journal tools. Create a simple assistant with a prompt like: "Use the available journal tools to answer questions about my notes."
-
Allow
Origin: nullin Ollama. Jan's chat requests includeOrigin: null, which Ollama rejects by default (you get "Generation failed: Forbidden"). SetOLLAMA_ORIGINS=nullin your environment. On macOS, set it in the launchd environment (not.zshrc) so the Ollama menubar app picks it up:
launchctl setenv OLLAMA_ORIGINS "null,http://tauri.localhost,https://tauri.localhost"
Then restart Ollama. Verify it worked: curl -s -o /dev/null -w '%{http_code}\n' -H 'Origin: null' http://localhost:11434/v1/models should print 200.
AnythingLLM — best if you're already Ollama-centric
AnythingLLM has first-class Ollama support and stdio MCP via a config file using the same JSON shape as Claude Desktop.
One important difference from the other clients: MCP tools only fire in @agent mode, not plain chat. Start messages with @agent to use your journal tools.
{
"mcpServers": {
"journal": {
"command": "/opt/homebrew/bin/journal",
"args": ["mcp", "--repo", "/Users/you/journal"]
}
}
}
Enabling local-only mode
If you want to be certain nothing leaves your machine, set local_only: true in your journal config and local_only_mcp: allow to permit the MCP server:
local_only: true
local_only_mcp: allow # "allow" is your attestation that the MCP client is local
Then run journal doctor — the egress line should confirm the fully-local posture.