Renames Untitled Obsidian notes from their content using a local Ollama LLM. Wikilinks update automatically; empty Untitled notes are cleaned up.
- Renames Untitled notes from their content. Each note's body becomes a short descriptive title; the file is renamed in place.
- Updates wikilinks automatically. Any
[[Untitled N]]references elsewhere in your vault get rewritten to the new filename — no dangling links. - Cleans up empty Untitled notes. Notes you opened and never wrote in are deleted rather than left to clutter the vault.
- Per-note opt-out. Add
auto_name: falseto a note's frontmatter and the script will skip it forever. - Designed to run on its own. Point a nightly cron at your vault and forget it.
You need:
- Ollama running locally with
qwen2.5:7bpulled (or whichever model you configure). - Python 3.11+ on Linux, or 3.13 specifically on macOS (3.14+ has a Local Network privacy quirk — see Design notes).
git clone https://github.com/undergroundpost/obsidian-auto-name.git
cd obsidian-auto-name
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
cp config.yaml.example config.yaml
# edit config.yaml — at minimum, set INPUT_FOLDER to your vault pathPreview the next run without touching any files:
.venv/bin/python rename_notes.py --dry-run --limit 5Drop --dry-run once you're happy with the output.
Pick the path that matches your setup.
For a vault that lives on the same machine the cron runs on:
30 0 * * * /path/to/obsidian-auto-name/.venv/bin/python /path/to/obsidian-auto-name/rename_notes.pyThe script writes its own dated log to logs/rename_notes_YYYY-MM-DD.log.
If your vault lives in Obsidian Sync and you want this script to run on an always-on server, use Obsidian's official headless client to pull and push around each run.
If you've already set up obsidian-auto-tagger on the same server, obsidian-headless is already installed and paired — skip to the cron step. Otherwise:
npm install -g obsidian-headless
ob sync-setup # interactive — pair to your Obsidian Sync vaultThen schedule the bundled wrapper, which does ob sync → rename → ob sync:
30 0 * * * /path/to/obsidian-auto-name/run-daily.shIf you're running the tagger on the same server, stagger the two — they share ob sync and Ollama and shouldn't compete.
.venv/bin/python rename_notes.py # default: process all matching files
.venv/bin/python rename_notes.py --dry-run --limit 5 # preview without writing
.venv/bin/python rename_notes.py --limit 5 # process at most 5
.venv/bin/python rename_notes.py --debug # verbose logging| Key | Default | Notes |
|---|---|---|
INPUT_FOLDER |
~/Documents/Notes |
Vault root |
EXCLUDE_FOLDERS |
[] |
Subtrees to skip |
LLM_PROVIDER |
ollama |
Only ollama supported |
OLLAMA_MODEL |
qwen2.5:7b |
Title-generation model |
OLLAMA_SERVER_ADDRESS |
http://localhost:11434 |
Ollama endpoint |
OLLAMA_CONTEXT_WINDOW |
32000 |
num_ctx |
RENAME_PATTERNS |
["Untitled","New Note"] |
Literal basenames that trigger renaming; N suffix auto-handled |
MAX_TITLE_WORDS |
3 |
Hard cap on words in the title; extras trimmed |
TITLE_CASE |
title |
title | sentence | lower (preserves acronyms) |
TITLE_TEMPLATE |
"{title}" |
Filename template; {title} and {date} are substituted |
CONFIDENCE_THRESHOLD |
0.5 |
LLM confidence below this triggers first-line fallback |
MAX_FILENAME_CHARS |
50 |
Hard cap on final filename length (excluding .md) |
OPT_OUT_FRONTMATTER_KEY |
"auto_name" |
Frontmatter key — value false opts a note out entirely |
DELETE_EMPTY_UNTITLED |
true |
Delete Untitled notes whose body is below the threshold |
EMPTY_NOTE_BODY_MIN_CHARS |
1 |
Threshold for "empty" — body chars after frontmatter strip |
MAX_NOTE_AGE_DAYS |
0 |
If > 0, only process notes modified in last N days |
-
Title-resolution cascade. The script tries four sources in priority order: (1) an explicit
title:field in frontmatter — skipped if it itself looks like an Untitled name, defending against templating plugins that auto-fill it; (2) an H1 heading on the first non-blank line — fast path, no LLM call; (3) the LLM, returning{title, confidence}under a grammar-constrained JSON schema; (4) the note's first non-blank line as a Notion-style fallback when LLM confidence is belowCONFIDENCE_THRESHOLD. The cascade keeps obvious cases cheap and degrades gracefully when the model isn't sure. -
Schema-enforced output. Same pattern as the sibling
obsidian-auto-tagger: Ollama'sformatfield with a JSON schema means the model can't return prose or skip the title field. Word-count enforcement happens in Python after parsing rather than in the schema — regex-based length constraints in JSON schemas are inconsistent across models. -
Wikilink rewrite is per-file. For each rename, the script greps the vault for
[[<old-basename>]]and[[<old-basename>|Display]]references and rewrites them (preserving display text) before renaming the file. In practice users rarely link to "Untitled N" by name, but the pass is cheap and prevents the dangling-link case. -
Filename collisions resolved by suffix. If the LLM picks "Meeting Notes" but that file already exists, the new name becomes "Meeting Notes 2.md", then "Meeting Notes 3.md", and so on.
-
The filename itself is the idempotency marker. Once a note is no longer named "Untitled", subsequent runs ignore it. No frontmatter timestamp needed — by definition the work has already been done.
-
macOS Local Network privacy is per-binary. Each Homebrew Python minor version is treated as a separate binary by the macOS permission system. New venvs that talk to Ollama on a non-localhost address must be created with the same
python@3.13binary that has the Local Network grant, or LAN calls will silently fail withEHOSTUNREACH. This quirk only applies on macOS; on Linux any Python 3.11+ works.
rename_notes.py # main script (entry point for cron)
rename_notes.md # prompt template
config.yaml.example # config template (commit this)
config.yaml # your local config (gitignored, copied from .example)
requirements.txt # Python deps
run-daily.sh # cron wrapper: ob sync → python → ob sync
.venv/ # gitignored
logs/ # dated log per run
