Yang

October 31, 2026

Set Up a Neovim Note-Taking System with Obsidian

Obsidian is the vault. Neovim is the editor. Two fish scripts and a short keymap file are the whole system.

I wanted notes I could capture in one command, shape without leaving the editor, and file by topic later. Obsidian already had the vault, the links, and the graph. I did not want it to be the place I write. This setup keeps writing in Neovim and uses the vault only as the store.

Four pieces do the work:

  1. An environment variable that points every tool at the same vault.
  2. obsidian.nvim for templates and wiki-link navigation.
  3. A keymap file for the actions I repeat.
  4. Two shell scripts, on and og, for creating a note and filing it.

The vault layout

OBSIDIAN_DIR is the vault root. Mine is set in the shared shell env so fish and Neovim see the same path.

export OBSIDIAN_DIR="/path/to/your-vault-folder"

Inside that directory, four folders matter:

FolderRole
inbox/Every new note lands here, unread and unfiled
jarvis/Notes I have decided to keep, waiting to be sorted
notes/<tag>/The long-term home. One folder per tag
templates/The note template Neovim inserts

inbox is a holding pen. Nothing is organized until I say so. jarvis is the staging area between "keep this" and "file this under a topic." og is what moves a kept note from jarvis into notes/.

Plugin

The plugin is lazy-loaded for markdown. New notes are forced into inbox, and the plugin is told not to manage frontmatter. The template owns the YAML. If both tried to write it, they would fight.

ui.enable is off. I do not want the plugin restyling checkboxes or concealing syntax while I edit.

gf is passed through to the plugin so a wiki link such as [[learn-anything]] opens the target note. Completion goes through nvim-cmp and starts after two characters.

{
  "epwalsh/obsidian.nvim",
  version = "*",
  lazy = true,
  ft = "markdown",
  dependencies = { "nvim-lua/plenary.nvim" },
  opts = {
    workspaces = {
      {
        name = "waterfox",
        path = vim.env.OBSIDIAN_DIR or "/path/to/your-vault-folder",
      },
    },
    ui = { enable = false },
    notes_subdir = "inbox",
    new_notes_location = "notes_subdir",
    disable_frontmatter = true,
    templates = {
      subdir = "templates",
      date_format = "%Y-%m-%d",
      time_format = "%H:%M:%S",
    },
    mappings = {
      ["gf"] = {
        action = function()
          return require("obsidian").util.gf_passthrough()
        end,
        opts = { noremap = false, expr = true, buffer = true },
      },
    },
    completion = {
      nvim_cmp = true,
      min_chars = 2,
    },
  },
}

The plugin can also name new notes itself. That function is left commented out. The on script already names the file, and I want one naming rule, not two.

Leader maps

The leader key is Space. These maps live in lua/config/workflows.lua, which init.lua loads on startup. The vault path is read from OBSIDIAN_DIR, with the same fallback the plugin uses.

KeysAction
Space oo:cd into the vault
Space onInsert the note template, then strip leading blank lines
Space ofOn the current line, drop the date prefix and turn dashes into spaces
Space okMove the current file into jarvis/ and close the buffer
Space odkDelete the current file and close the buffer
Space osTelescope file search under notes/
Space ozTelescope live grep under notes/

The template map is two commands. :ObsidianTemplate note inserts templates/note.md. The plugin leaves a blank line at the top, so a follow-up substitute deletes leading empty lines until the first line that has text:

vim.keymap.set(
  "n",
  "<leader>on",
  ":ObsidianTemplate note<cr> :lua vim.cmd([[1,/^\\S/s/^\\n\\{1,}//]])<cr>"
)

The title map only touches the line under the cursor. Put the cursor on the heading first.

vim.keymap.set("n", "<leader>of", ":s/\\(# \\)[^_]*_/\\1/ | s/-/ /g<cr>")

A heading produced from the filename looks like this:

# 2026-10-31_Set-Up-a-Neovim-Note-Taking-System-with-Obsidian

The first substitute deletes everything from after # up to and including the first underscore. The second turns the remaining dashes into spaces:

# Set Up a Neovim Note Taking System with Obsidian

Keep and delete both run a shell command, then close the buffer. Neovim's shell is fish, which matches the scripts.

vim.keymap.set("n", "<leader>ok", ":!mv '%:p' " .. obsidian_dir .. "/jarvis<cr>:bd<cr>")
vim.keymap.set("n", "<leader>odk", ":!rm '%:p'<cr>:bd<cr>")

Search is limited to notes/, so the inbox and jarvis stay out of everyday lookup. Those two folders are for processing, not for finding something I already filed.

Create a note: on

on lives at ~/.local/bin/on. It refuses to run without a title or without OBSIDIAN_DIR. Spaces in the title become dashes. The date is prepended so the inbox sorts in chronological order.

#!/usr/bin/env fish
 
if test -z $argv[1]
    echo "Error: A file name must be set, e.g. on \"the wonderful thing about tiggers\"."
    exit 1
end
 
if not set -q OBSIDIAN_DIR
    echo "Error: OBSIDIAN_DIR is not set." >&2
    exit 1
end
 
set file_name (echo $argv[1] | tr ' ' '-')
set formatted_file_name (date "+%Y-%m-%d")_$file_name.md
 
cd "$OBSIDIAN_DIR" || exit
touch "inbox/$formatted_file_name"
nvim "inbox/$formatted_file_name"
on "Set Up a Neovim Note-Taking System with Obsidian"

That creates inbox/2026-10-31_Set-Up-a-Neovim-Note-Taking-System-with-Obsidian.md and opens it in Neovim. The file is empty until Space on applies the template.

File a kept note: og

og lives at ~/.local/bin/og. It walks every markdown file in jarvis/, reads the first tag, and moves the file to notes/<tag>/. The target folder is created if it is missing.

#!/usr/bin/env fish
 
if not set -q OBSIDIAN_DIR
    echo "Error: OBSIDIAN_DIR is not set." >&2
    exit 1
end
 
set SOURCE_DIR jarvis
set DEST_DIR notes
 
find "$OBSIDIAN_DIR/$SOURCE_DIR" -type f -name "*.md" | while read -la file
    set tag (awk '/tags:/{getline; print; exit}' "$file" | sed -e 's/^ *- *//' -e 's/^ *//;s/ *$//')
 
    if test -n "$tag"
        set TARGET_DIR "$OBSIDIAN_DIR/$DEST_DIR/$tag"
        mkdir -p "$TARGET_DIR"
        mv "$file" "$TARGET_DIR/"
    else
        echo "No tag found for $file"
    end
end

The tag it reads is the line immediately after tags:. A note tagged blog first lands in notes/blog/. Later tags are ignored by the script, so the first tag is the folder name. A note with no tag is left in jarvis and reported.

The template

templates/note.md is small on purpose. Date, one tag, one hub, an empty URL list, then the title:

---
date: {{date}}
tags:
  - fact
hubs:
  - "[[learn-anything]]"
urls:
  -
---
 
# {{title}}

{{date}} uses the plugin's %Y-%m-%d format. {{title}} comes from the filename, which is why Space of exists: the heading starts as a dated, dashed filename and needs to become a sentence.

Change the default tag before you run og. fact would file the note under notes/fact/. This post uses blog so it files under notes/blog/.

What is wired, and what is not

A fish function, obr, changes to the vault and opens inbox/*.md for review. The files land on Vim's argument list, starting at the first note. :n moves to the next note, :N to the previous one, :first and :last jump to the ends, and :args shows the list with the current note in brackets. :n stops on unsaved changes; :w then :n continues, and :n! discards the changes. :qa closes the whole review. obo only changes to the vault. Inside Neovim, Space oo does the same directory change.

An older alias synced the vault to Notion. It is commented out. This system stops at the local vault.

That is the whole install: one env var, one plugin block, one keymap file, one template, and two scripts on PATH. The daily loop that uses them is a separate note.