DEV Community

Cover image for How to escape note-taking lock-in with plain markdown and git
Alan West
Alan West

Posted on

How to escape note-taking lock-in with plain markdown and git

When your notes outlive your note-taking app

A few months ago I tried to export 4 years of notes from a popular note-taking app. The export gave me a .zip of "markdown" files — except every link was rewritten to use the app's proprietary [[uuid-7f3a...]] syntax, every attachment was renamed to a hash, and frontmatter was packed with app-specific fields nothing else could parse.

I'd been telling myself "it's just markdown, I can leave whenever." Turns out I couldn't. Not without spending a weekend writing a migration script.

This isn't a rant about that one app. It's a problem-solving article about a pattern I've watched bite developers over and over: trusting that the "open format" sticker on a tool means your data is portable. Below is how to set up a notes system that's actually portable — and how to verify it stays that way.

The root cause: proprietary syntax inside open file extensions

The trick almost every note-taking app pulls is this:

  • Files are saved as .md. Marketing says "your notes are just markdown."
  • But the content uses app-specific extensions: custom block IDs, embeds, callouts, query languages, plugin metadata.
  • Open the file in a plain editor and you'll see roughly 60% standard markdown and 40% syntax that looks like markdown but isn't.

Standard CommonMark and GitHub Flavored Markdown are well-defined specs. Anything outside those is, technically, just text the app happens to render specially.

When you try to migrate, the new tool reads the file fine — and silently drops everything that isn't standard markdown. Links break. Embeds disappear. Math blocks lose half their content. The migration looks successful right up until you actually try to use the imported notes.

Step 1: Set boundaries with a vault structure

The fix is to treat your notes like a small codebase. Plain markdown, folders for organization, git for history. Here's the layout I've used across three migrations now:

notes/
├── .git/
├── .gitignore
├── README.md              # entry point — what's here, how it's organized
├── inbox/                 # quick captures, unprocessed
├── daily/                 # YYYY-MM-DD.md
├── projects/
│   ├── project-a.md
│   └── project-b.md
├── topics/                # long-lived reference notes
│   ├── postgres.md
│   └── linux-networking.md
└── attachments/           # images, PDFs — referenced by relative path
Enter fullscreen mode Exit fullscreen mode

Three rules I follow strictly:

  • Links are relative file paths, not app-specific wikilinks. [postgres notes](../topics/postgres.md) works everywhere — on GitHub, in VS Code, on the filesystem.
  • Attachments live alongside the notes that reference them. ![diagram](./attachments/2026-02-pipeline.png).
  • No plugin-specific frontmatter. If a field isn't useful when grep'd as plain text, don't add it.

Step 2: Replace "features" with Unix tools

Most app features developers actually need — search, backlinks, tag listings — can be replaced with command-line tools you already have.

For full-text search, ripgrep is faster than any in-app search I've used:

# Search all notes for a phrase, with 2 lines of context
rg -i "connection pool" -C 2 notes/

# Find every note tagged #postgres (tags as inline #hashtags)
rg -l "#postgres\b" notes/

# Find broken relative links: files referenced that don't exist on disk
rg -oP '\]\(\.\/[^)]+\)' notes/ | while IFS=: read -r src link; do
  target=$(dirname "$src")/$(echo "$link" | sed 's/^](\.\///; s/)$//')
  [ -f "$target" ] || echo "BROKEN: $src -> $link"
done
Enter fullscreen mode Exit fullscreen mode

For backlinks — which note mentions which — a one-liner does the job:

# Find every note that links to topics/postgres.md
rg -l "topics/postgres\.md" notes/
Enter fullscreen mode Exit fullscreen mode

Less ergonomic than a sidebar panel in a GUI? Sure. But it works on every machine I'll ever own, in every editor, forever. That's the tradeoff.

Step 3: Version control as the safety net

This is the step most "just use markdown" guides skip, and it's the one that actually makes the system durable. Initialize the directory as a git repo and commit anything that survives more than a day in the inbox.

cd notes/
git init
git add .
git commit -m "initial vault"

# A tiny pre-commit hook that rejects accidental app-specific syntax
cat > .git/hooks/pre-commit <<'HOOK'
#!/usr/bin/env bash
# Block wikilink-style references — they don't render outside specific apps
if git diff --cached --name-only -z | xargs -0 grep -lE '\[\[[^]]+\]\]' 2>/dev/null; then
  echo "Found wikilink syntax. Use relative paths instead." >&2
  exit 1
fi
HOOK
chmod +x .git/hooks/pre-commit
Enter fullscreen mode Exit fullscreen mode

The hook is the boring-but-critical piece. Without it, you'll absentmindedly type [[some note]] once a week and slowly recreate the lock-in problem inside your supposedly portable system. Found that out the hard way last year.

Step 4: A sync script you actually understand

If you want notes on multiple devices, resist the urge to bolt on a sync service. A git remote is enough for 99% of single-user workflows:

# sync.sh — call from cron or a keybinding
set -euo pipefail
cd "$HOME/notes"

git add -A
# Skip empty commits when nothing has changed since last sync
if ! git diff --cached --quiet; then
  git commit -m "sync $(date -u +%FT%TZ)"
fi
git pull --rebase --autostash
git push
Enter fullscreen mode Exit fullscreen mode

I've run this exact script across a laptop, a desktop, and a server for about 18 months. Total merge conflicts: maybe a dozen, all resolved in under a minute because the files are plain text.

Prevention: how to audit a tool before you commit

Before adopting any new note-taking tool, run this checklist. Took me three migrations to learn it:

  • Create a test note that uses every feature you care about (links, tags, attachments, embeds, code blocks).
  • Open the raw file in cat. Does it contain only standard markdown? If you see custom block syntax, that's your future lock-in.
  • Move that file out of the tool's directory. Open it in a different markdown viewer. Does it still render correctly, with working links?
  • Delete the tool entirely. Are your files still useful as plain text in a git repo?

If any answer is "no" or "kind of", you're not adopting a markdown editor — you're adopting a database that happens to use .md as a file extension.

When you actually need a GUI

To be fair: a folder of markdown plus ripgrep won't replace every workflow. For graph views, daily review templates, or kanban boards on top of notes, you'll want some kind of editor or viewer. The fix isn't to avoid GUIs — it's to pick ones that read a directory of plain files instead of owning a vault. If the tool insists on importing your files into its own format, walk away. If it sits on top of the directory and treats your files as the source of truth, you can swap it out next year without losing a thing.

That single distinction — does the tool own your files, or just read them — is the whole game.

Top comments (0)