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 @todo anywhere 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.

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.


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 init works 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.

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.

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

CommandWhat it generates
journal synth weeklyA summary of your ISO week's notes → reflections/YYYY-Www.md
journal synth dailyToday's notes at a glance → reflections/daily-YYYY-MM-DD.md
journal synth daily --date 2026-06-02A specific day
journal synth meetingsDigest of recent meeting transcripts (last 7 days by default)
journal synth decisions --project acmeA rollup of @decision notes for a project
journal synth stale --days 21Surface 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 d to 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

KeyDefaultWhat it does
embed_providerollamaWhere embeddings come from: ollama (local, recommended) or openai (any OpenAI-compatible endpoint).
embed_modelqwen3-embedding:4bThe Ollama model used to embed your notes and queries. Pull it with ollama pull qwen3-embedding:4b.
embed_dim2560Must 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_urlhttp://localhost:11434Where Ollama is running. Change this only if you've moved Ollama to a different port or host.
store_path.journal/index/journal.dbPath 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

KeyDefaultWhat 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

KeyDefaultWhat it does
synth_provideranthropicWho runs synthesis: anthropic (cloud Claude), ollama (local), or openai (any OpenAI-compatible endpoint like OpenRouter or Groq).
synth_modelclaude-sonnet-4-6Anthropic model, used when synth_provider: anthropic.
synth_ollama_modelgemma4:12bLocal model, used when synth_provider: ollama. Pull it with ollama pull gemma4:12b.
synth_openai_base_urlhttps://api.openai.com/v1API 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_ctx32768Context window for Ollama synthesis calls. Always set explicitly — Ollama's default is 4096 and it truncates silently.
voice_profiledocs/VOICE_PROFILE.mdPath to a style reference that shapes how synthesis sounds like you. Optional — synthesis works without it.

Privacy & egress

KeyDefaultWhat it does
local_onlyfalseWhen 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_mcpblockUnder 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

KeyDefaultWhat it does
git_autocommittrueAuto-commit notes after capture and index. Turn off if you prefer to manage commits yourself.
git_autocommit_signfalseSign auto-commits. Off by default so the watcher doesn't prompt for a signing passphrase.

Remote backup

KeyDefaultWhat it does
sync_enabledfalseGates journal sync. Nothing happens until you set this to true. See Backup & Sync.
sync_conflictmanualHow sync handles a divergence: manual (aborts, lets you resolve), prefer-upstream (takes the remote), prefer-local (keeps local).

Meetings & transcripts

KeyDefaultWhat it does
transcripts.enabledtrueGates the transcript feature.
transcripts.pathtranscriptsWhere rendered transcripts are written. Gitignored.
transcripts.auto_indextrueEmbed new transcripts as the watcher detects them.
transcripts.tagmeetingTag applied to every transcript chunk. Find them with --tag meeting or --source transcript.
quill.enabledtrueGates journal quill-sync.
quill.db_pathOS defaultPath to Quill's local database. macOS: ~/Library/Application Support/Quill/quill.db. Windows: ~/AppData/Roaming/Quill/quill.db.
quill.accept_qm_importstrueRender 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 — for synth_provider: anthropic
  • OPENAI_API_KEY — for synth_provider: openai or embed_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 RAMEmbedding modelSynthesis model
16 GBqwen3-embedding:4b (~2.5 GB)llama3.1:8b (~5 GB)
32–48 GBsamegemma4:12b (~8–10 GB) — recommended
64 GB+samegemma4: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:

  1. Commits any pending note changes
  2. Fetches the remote
  3. Pushes your local commits if you're ahead
  4. Merges remote changes if you're behind (and re-indexes new notes)
  5. Handles divergence according to your sync_conflict setting

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:

ModeWhat happens on a conflictBest for
manual (default)Aborts cleanly, tells you to run git pullMultiple machines, you want control
prefer-upstreamTakes the remote version on any conflictSingle authoritative remote
prefer-localKeeps your local version on any conflictThis 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 journal won'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:

ToolWhat it does
searchSemantic search over your notes
showRead the full content of a specific note
recentSee your most recent notes
decisionsFind notes marked @decision
threadsSee project activity and stale threads
meetingsList recent meeting transcripts
todosList open @todo items
doneComplete a todo
captureAdd a new note
statsJournal metrics: note volume, streaks, open todos, decisions, top tags
todayYour day at a glance: today's note path, open todos, and meetings
askAsk a question answered from your notes, with path:line citations
synthRun 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 URIContents
journal://todayToday's daily note (raw Markdown)
journal://recentThe 50 most recent note chunks, newest first
journal://projects/{slug}/indexA 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:

PromptArgumentsWhat it does
weekly-reflectionGathers this week's notes and assembles a weekly reflection prompt
decisions-reviewproject (optional)Gathers @decision notes and assembles a review prompt
project-statusproject (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:

  1. 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.

  2. 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 just journal). Find the path with which journal.

  3. 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

  1. Install LM Studio from lmstudio.ai (macOS/Windows/Linux).
  2. Download a model in the Discover tab — Gemma 4 12B works well.
  3. Go to Program sidebar → Install → Edit mcp.json:
{
  "mcpServers": {
    "journal": {
      "command": "/opt/homebrew/bin/journal",
      "args": ["mcp", "--repo", "/Users/you/journal"]
    }
  }
}
  1. 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

  1. Install Jan from jan.ai.

  2. 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.

  3. 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."

  4. Add journal as an MCP server: Settings → MCP Servers → +. Enter the arguments one per line: mcp on the first line, --repo on the second, /path/to/your/journal on 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 --repo to —repo (an em dash), which breaks the flag. Paste instead of typing, or disable Smart Dashes in System Settings → Keyboard → Text Input → Edit.

  5. 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."

  6. Allow Origin: null in Ollama. Jan's chat requests include Origin: null, which Ollama rejects by default (you get "Generation failed: Forbidden"). Set OLLAMA_ORIGINS=null in 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.