dither
Concepts

Collections

Folders under the library, the frontmatter spec, and how the index picks them up.

A collection is a top-level folder under your configured library — or a pre-existing folder elsewhere on disk that you've registered as an external collection via dither collection add. No schema, no meta.json. If a directory exists at <library>/notes/, you have a notes collection. If you ran dither collection add ~/Documents/work-notes, you have a work-notes collection that lives outside the library.

<library>/
  notes/             ← collection "notes"
  twitter/           ← collection "twitter"
  gmail/             ← collection "gmail"

The library path is recorded in <dither-home>/config.json and chosen at dither init. By default it's <dither-home>/library/; with dither init --library <path> it's wherever you point it. See Storage layout for the dither-home / library split.

To create a new collection, make the directory under the library. To delete one, remove the directory. The next dither index update reflects the change. To register an existing folder outside the library (e.g. ~/Documents/work-notes/), use dither collection add <path>; the folder stays where it is, and dither indexes and grants against it under the registered name. See Adding external collections below.

Subdirectories are yours

Inside a collection, the layout is unrestricted. dither doesn't care if you bucket by date, by status, by topic, or not at all:

<library>/gmail/
  2026/04/25/<thread>.md
  2026/04/26/<thread>.md
<library>/notes/
  drafts/
  archive/
  pinned.md

The qmd index walks the whole tree under each collection (**/*.md), so subfolders are searchable just by existing.

Frontmatter

Every entry is a markdown file with YAML frontmatter on top.

Host-stamped fields

These are managed (or validated) by dither when a plugin promotes an entry. You can also set them by hand on entries you author yourself.

---
id: <stable-id> # used for `dither get <id>`; survives moves
source: gmail-ingest # plugin name, or your own label for hand-written entries
collection: gmail # must match the folder the file lives under
---

For plugin output, source and collection are enforced at promote time — a plugin's file is rejected if source doesn't match the plugin's name or collection isn't one the plugin was granted to write. See Security.

Plugins and collections

A plugin's manifest declares the collections it wants to write to. Entries are glob patterns over nestable path identifiers:

{
  "dither": {
    "collections": ["notes", "messages/**"],
  },
}

That's the install-time default. At install you can override or extend it via --allow-collection:

dither plugin install ./my-plugin --allow-collection 'messages/tom/**'

Glob semantics: notes matches only the literal notes collection, messages/* matches direct children only, messages/2026-* is partial-segment match, and messages/** matches both messages itself and any descendant — the runner special-cases the <X>/** form to also cover the bare <X> parent, since plugin authors granting a subtree almost always want the parent included.

If you pass no flag, the grant defaults to whatever the manifest declared. The manifest is not a ceiling — your --allow-collection flag is what actually ends up in the grants file, and that's what gates promote. Promote validates each output's collection: frontmatter against the grant set; values matching no glob are rejected.

Plugins can write to nested paths: writeEntry({ collection: "messages/tom" }) lands at <library>/messages/tom/<id>.md. Path identifiers must use [a-zA-Z0-9._-] per segment, must not start a segment with . (no dotfile-style folders like .git), joined by /, with no .., no leading/trailing /, no empty segments, and no .md suffix on the path itself.

Plugins do not "auto-create" collections. The directory under <library>/<...path>/ is created lazily on the first promote into it; nothing else changes — collections remain just folders.

Optional fields

These are part of the frontmatter convention. dither doesn't require them, but plugins and search clients look for them when present.

---
external_id: 1789234567890 # provider-side id (tweet id, gmail thread id, …)
external_url: https://… # round-trip link back to the source
created: 2026-04-25T11:14:00Z # original creation time
tags: [pr, dither, review]
attachments:
  - id: 7f3a8c10-…
    type: image/png
    name: screenshot.png
---

attachments[] is reserved in the spec but not yet promoted to disk by the host — the field parses but won't move blob files into a content-addressed store. Treat it as forward-compatible: emit it now, attachments will land later without a frontmatter migration.

External collections

A folder anywhere on disk can be registered as a collection without copying or moving its files:

dither collection add ~/Documents/work-notes              # name defaults to 'work-notes'
dither collection add ~/Notes/journal --name journal      # explicit name
dither collection list                                    # library subdirs + externals, with --verbose for paths + counts
dither collection remove journal                          # unregister (files untouched)

A registered external collection behaves identically to a library subdir for search, grants, and plugin writes — --allow-collection work-notes/** works the same way regardless of whether work-notes is on the library tree or somewhere else. The on-disk path is recorded under collections.external in config.json. Names are flat (no /), case-insensitive against library subdirs and other externals, and the path must not overlap the library or any other registered external. If the external path goes missing at runtime (drive unplugged, folder moved), dither collection list flags it and operations against other collections keep working.

Auto-create is library-only: a plugin writing collection: "brand-new-name" always creates <library>/brand-new-name/. External mounts must be registered explicitly.

How the index sees collections

dither doesn't store entries in a database. Search is provided by qmd, which builds a SQLite index over the directory tree. Every top-level subdirectory of the library AND every registered external collection is registered as a qmd collection on open, and qmd indexes **/*.md underneath each.

The index file lives at <dither-home>/qmd-index.sqlite — in dither home, not the library. Don't edit it; it's regenerable from your markdown. See Storage.

To (re)scan the tree after manual edits or external file moves, run:

dither index update

Plugin runs trigger this automatically when they promote new entries (scoped to just the touched collections — a one-collection promote doesn't trigger a full library rescan).

Hand-editing entries

dither doesn't fight you. Open any .md under your library in your editor, change the body, change the frontmatter, rename the file — it's all fine. The host doesn't lock files and doesn't impose body conventions. After a manual edit, run dither index update to refresh the search index.

If you have an installed plugin with a watch block that points at the collection you're editing, the daemon will pick up your changes via chokidar and fire that plugin — useful when intentional, surprising when not. Plugins with no watch block (the common case for ingest plugins) don't react to hand edits.

If you delete or move a file by hand, run dither index update to drop stale entries from the index.

For the commands themselves, see the CLI reference.