Obsidian, Notion, Roam — there’s no shortage of good note-taking apps. But I wanted something where AI could not just read my notes, but write tooling for them. So I built a system that’s about 50 lines of bash, and AI can extend it trivially.
The Idea
This isn’t anti-Obsidian. Obsidian is great, and it uses markdown too. The difference is what happens when you want a new feature.
In most apps, you either wait for it, find a plugin, or learn the plugin API and build it yourself. In a system that’s just files and scripts, you ask Claude “write me a script that does X” and it works. No plugins to learn, no API to integrate—just scripts that operate on files.
When the whole system is transparent, AI can reason about it completely.
The Structure
Notes are plain markdown files with date-prefixed names:
20251210--project-kickoff.md
20251211--api-design-notes.mdOrganized in folders by project or area:
notes/
├── active/
│ ├── client-project/
│ ├── side-project/
│ └── personal/
├── archive/
└── scripts/Active projects live in active/. When a project wraps up, it moves to archive/. The scripts/ folder holds the tooling.
Dependencies: fzf for fuzzy selection, bat for syntax-highlighted previews, ripgrep for fast search. Standard Unix tools.
Five Scripts I Found Useful
These are the actions I needed. Your list might be different — that’s the point.
nn — New Note
Pick a folder, enter a title, creates a dated markdown file and opens it.
#!/bin/bash
NOTES_DIR="/Users/daniel/notes"
ACTIVE_DIR="$NOTES_DIR/active"
PICKER="fzf"
folders=$(find "$ACTIVE_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
if [ -z "$folders" ]; then
echo "No folders found in $ACTIVE_DIR"
exit 1
fi
folder=$(echo "$folders" | $PICKER --prompt="Select folder: ")
if [ -z "$folder" ]; then
echo "No folder selected"
exit 1
fi
read -p "Title: " title
if [ -z "$title" ]; then
echo "Title cannot be empty"
exit 1
fi
date_stamp=$(date +%Y%m%d)
slug=$(echo "$title" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
filename="${date_stamp}--${slug}.md"
filepath="${ACTIVE_DIR}/${folder}/${filename}"
echo "# ${title}" > "$filepath"
echo "" >> "$filepath"
echo "Created: $filepath"
${EDITOR:-vim} "$filepath"nf — Find Note
Fuzzy-find notes by filename with preview. Use -p to pick a specific folder first.
#!/bin/bash
NOTES_DIR="/Users/daniel/notes"
ACTIVE_DIR="$NOTES_DIR/active"
PICKER="fzf"
pick_folder=false
for arg in "$@"; do
case $arg in
-p|--pick) pick_folder=true ;;
esac
done
if $pick_folder; then
folders=$(find "$ACTIVE_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
if [ -z "$folders" ]; then
echo "No folders found in $ACTIVE_DIR"
exit 1
fi
folder=$(echo "$folders" | $PICKER --prompt="Select folder: ")
if [ -z "$folder" ]; then
echo "No folder selected"
exit 1
fi
search_dir="$ACTIVE_DIR/$folder"
else
search_dir="$ACTIVE_DIR"
fi
notes=$(find "$search_dir" -name "*.md" -type f | sort)
if [ -z "$notes" ]; then
echo "No notes found"
exit 1
fi
filepath=$(echo "$notes" | $PICKER --prompt="Select note: " --preview="bat --color=always --style=plain {}")
if [ -z "$filepath" ]; then
echo "No note selected"
exit 1
fi
${EDITOR:-vim} "$filepath"ns — Search Content
Full-text search across all notes. Opens at the matching line.
#!/bin/bash
NOTES_DIR="/Users/daniel/notes"
ACTIVE_DIR="$NOTES_DIR/active"
PICKER="fzf"
pick_folder=false
for arg in "$@"; do
case $arg in
-p|--pick) pick_folder=true ;;
esac
done
if $pick_folder; then
folders=$(find "$ACTIVE_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
if [ -z "$folders" ]; then
echo "No folders found in $ACTIVE_DIR"
exit 1
fi
folder=$(echo "$folders" | $PICKER --prompt="Select folder: ")
if [ -z "$folder" ]; then
echo "No folder selected"
exit 1
fi
search_dir="$ACTIVE_DIR/$folder"
else
search_dir="$ACTIVE_DIR"
fi
result=$(rg --line-number --color=always . "$search_dir" | $PICKER --ansi --prompt="Search content: " --preview="bat --color=always --style=plain --highlight-line {2} {1}" --delimiter=: --preview-window=+{2}-10)
if [ -z "$result" ]; then
echo "No match selected"
exit 1
fi
filepath=$(echo "$result" | cut -d: -f1)
line=$(echo "$result" | cut -d: -f2)
${EDITOR:-vim} "+$line" "$filepath"np — New Project
Create a new folder in active/.
#!/bin/bash
NOTES_DIR="/Users/daniel/notes"
ACTIVE_DIR="$NOTES_DIR/active"
read -p "Folder name: " folder
if [ -z "$folder" ]; then
echo "Folder name cannot be empty"
exit 1
fi
slug=$(echo "$folder" | tr '[:upper:]' '[:lower:]' | tr ' ' '-' | tr -cd 'a-z0-9-')
mkdir -p "$ACTIVE_DIR/$slug"
echo "Created: $ACTIVE_DIR/$slug"na — Archive Project
Move a folder from active/ to archive/.
#!/bin/bash
NOTES_DIR="/Users/daniel/notes"
ACTIVE_DIR="$NOTES_DIR/active"
ARCHIVE_DIR="$NOTES_DIR/archive"
PICKER="fzf"
folders=$(find "$ACTIVE_DIR" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort)
if [ -z "$folders" ]; then
echo "No folders found in $ACTIVE_DIR"
exit 1
fi
folder=$(echo "$folders" | $PICKER --prompt="Select folder to archive: ")
if [ -z "$folder" ]; then
echo "No folder selected"
exit 1
fi
mkdir -p "$ARCHIVE_DIR"
mv "$ACTIVE_DIR/$folder" "$ARCHIVE_DIR/"
echo "Archived: $folder"I also have Neovim commands that mirror these :NoteNew, :NoteFind, :NoteSearch, and so on. AI generated those too.
AI Can Write the Rest
This is the key part. When you need a new feature, you don’t search for plugins or learn an API. You describe what you want.
“Write me a script that generates a weekly summary of notes created in the last 7 days.”
“Write me a script that finds notes containing a specific phrase and lists them with context.”
“Write me a script that exports a project folder to a single markdown file for sharing.”
Claude can see the folder structure, understand the naming convention, and write a working script in seconds. No abstractions to navigate, no plugin architecture to satisfy. Just files and bash.
You don’t need to anticipate every feature. When you need something, ask.
Tradeoffs
There are real downsides:
- No native mobile app. A simple markdown viewer / editor like 1Writer is enough for me though.
- No sync built-in. I use git. Syncthing would also work. But you have to set it up.
- No graph views. I never found these useful, but if you did — AI could generate one.
- Requires terminal comfort. If fzf and vim aren’t in your vocabulary, there’s friction.
Why This Works
The system isn’t better because it’s minimal. It’s better because it’s transparent.
AI works best when it can see and modify the full system. No hidden state, no proprietary formats, no plugin APIs to reverse-engineer. Just files and scripts that do exactly what they say.
Five scripts was enough for me. Yours might be different. That’s the point.