Keyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration.
See the code
[!NOTE] I build ZenNotes with a lot of help from AI tools like Claude and Codex. They let me move much faster, but I still go through some of the code before I ship it. The architecture, the technical decisions, and the trade-offs are things I still spend a lot of time thinking about. AI helps me write code faster, but it doesn't replace understanding how the system works.
ZenNotes is a keyboard-first Markdown notes app with a shared product core and multiple runtimes:
ZenNotes keeps your notes as ordinary Markdown files on disk. It adds Vim-friendly editing, split and preview workflows, tasks, tags, archive/trash, diagrams, search, daily notes, CSV databases (Notion-style Table + Board views over plain .csv files), and MCP integration on top of the files you already own.
On macOS, the first-party zen CLI also powers launcher workflows such as the Raycast extension.
Grab the latest build from GitHub Releases — see Install below.
Website: zennotes.org
Support development: GitHub Sponsors
Detailed in-repo documentation lives under docs/README.md.
All desktop installers are attached to each GitHub Release. The app auto-updates, so you only download once. (Replace <version> with the current release, e.g. 2.2.0.)
Homebrew (recommended):
brew install --cask zennotes/tap/zennotes
Or download the .dmg for your chip, open it, and drag ZenNotes to Applications. Builds are signed and notarized.
ZenNotes-<version>-mac-arm64.dmgZenNotes-<version>-mac-x64.dmgDownload and run ZenNotes-<version>-win-x64.exe.
Pick whatever suits your distro:
yay -S zennotes-bin # or: paru -S zennotes-bin
Installs cleanly without libfuse2.sudo pacman -U ZenNotes-<version>-linux-x86_64.pacman
sudo apt install ./ZenNotes-<version>-linux-amd64.deb
chmod +x ZenNotes-<version>-linux-x86_64.AppImage
./ZenNotes-<version>-linux-x86_64.AppImage
AppImages need FUSE 2. On distros that ship only FUSE 3 (Arch, CachyOS, Fedora), either install it (sudo pacman -S fuse2, sudo dnf install fuse) or run without FUSE:
./ZenNotes-<version>-linux-x86_64.AppImage --appimage-extract-and-run
(Or just use the AUR / .pacman / .deb package, which sidestep this entirely.)zen CLIThe desktop app installs a zen command-line companion from Settings → CLI — list, read, search, capture, edit, archive/trash notes, plus tasks, folders, and MCP. On macOS it can also install the Raycast extension locally.
ZenNotes also runs as a self-hosted web app backed by a Go server. See Local development to run the web client and server.
ZenNotes now ships from one monorepo with one shared app core.
desktop: Electron shell, native menus, updater, floating windows, desktop packagingself-hosted: browser frontend plus Go server, suitable for home servers and LAN usehosted: planned as the same web/server stack with auth and multi-user storage added laterThe source of truth for user-facing features is the shared UI in packages/app-core.
Every note is a normal .md file inside a chosen vault. ZenNotes does not store note content in a hidden database.
ZenNotes assumes you want to move fast:
ZenNotes supports:
ZenNotes includes a first-party MCP server and desktop install flows for compatible clients, so tools can work on the same vault you do instead of a copy.
ZenNotes can:
System folders still exist, but the vault model is more flexible now:
quick, archive, and trash remain built-in lifecycle areasinbox/The built-in folder labels are also customizable in the UI without changing the underlying internal ids.
Daily and weekly notes are optional and can be enabled from Settings.
2026-04-212026-W24Daily Notes and Weekly Notesyyyy/MM-MMM and yyyy-MM-dd-EEEyyyy-'W'wwsystem, en-US, pt-BR, or another BCP 47 localeSupported date note pattern tokens:
yyyy / yy for 2026 / 26M / MM / MMM / MMMM for 6 / 06 / Jun / Juned / dd for 9 / 09EEE / EEEE for Tue / Tuesdayw / ww for ISO week numbers like 24'Daily Notes'/yyyy/MM-MMMWeekly notes render date tokens from the ISO week's Monday, and yyyy is the ISO week-year for weekly patterns.
The editor stack is CodeMirror 6 with a Markdown-oriented workflow:
Preview and split mode support:
ZenNotes includes:
Vault text search can use the built-in engine, ripgrep, or fzf, with auto-detection and optional custom binary paths.
The desktop app also ships a zen command-line companion for list, read, search, capture, edit, archive/trash, task, folder, and MCP workflows. On macOS, ZenNotes can install its Raycast extension locally from Settings -> CLI, avoiding the Raycast Store review path. The integration uses the CLI plus zennotes:// deep links to search notes, open them in the main app, open them in floating windows, archive/unarchive, move notes to Trash, reveal files in Finder, and copy note paths or wikilinks.
ZenNotes now works better with existing Obsidian-style vaults.
inbox/![[image.png]] resolve more like Obsidianattachements/ and _assets/ folders are still recognizedThis means imported vaults with top-level notes, folders, and loose images/files behave much more naturally.
ZenNotes supports local files in notes and in the sidebar/list views.
Sidebar multi-select supports platform modifiers: use Cmd/Ctrl-click to toggle individual notes or folders, Shift-click to select a visible range, then use the context menu to apply actions such as open in tabs, move, archive, trash, restore, delete folders, copy paths, or drag the selected group to a folder.
The settings surface includes:
Both runtimes share the same core app, but they do not expose identical platform features.
Desktop-only features include:
zen CLI install/uninstall flowWeb/self-hosted mode includes:
ZenNotes now uses a single monorepo.
apps/
desktop/ Electron shell, preload, updater, packaging
web/ Vite/PWA shell and HTTP bridge
packages/
app-core/ Shared React application and renderer logic
bridge-contract/ Typed runtime contract between UI and host
shared-domain/ Shared types and note/task/view models
shared-ui/ Reusable UI primitives
tooling/
scripts/ Shared tooling hooks and migration scripts
docs/
Read docs/monorepo-architecture.md for the architectural boundary between the shared app core and the platform-specific shells.
npm ci
npm run dev:desktop
or:
make desktop
npm run dev:web
or:
make web-dev
The server lives in its own repository,
ZenNotes/znserver. For browser work
you do not need it checked out: npm run dev:server downloads the release
pinned in tooling/server-release.json, verifies its checksum, and runs it.
To work on the server itself, point ZENNOTES_SERVER_DIR at a znserver
checkout and the same command runs it with go run.
npm run dev:server
or:
make server-dev
npm run dev:web-stack
or:
make web-stack
Important dev note:
From the repository root:
| Script | Purpose |
|---|---|
npm run dev | Alias for npm run dev:desktop |
npm run dev:desktop | Run the Electron desktop app in development |
npm run dev:web | Run the Vite web client |
npm run dev:server | Run the Go server |
npm run dev:web-stack | Run web + server development together |
npm run start | Start the built desktop app |
npm run typecheck | Run monorepo typechecks |
npm run test | Run monorepo tests |
npm run test:run | Run the full test suite |
npm run build | Build the monorepo and then build the Go server |
npm run build:prod | Typecheck + test + build |
npm run pack | Desktop packaged output |
npm run dist:mac | Build macOS desktop distributables |
npm run dist:win | Build Windows desktop distributables |
npm run dist:linux | Build Linux desktop distributables |
The root Makefile provides a simpler interface:
| Command | Purpose |
|---|---|
make install | Install workspace dependencies |
make desktop | Run the Electron app in dev mode |
make web-dev | Run the web client |
make server-dev | Run the pinned Go server release (or a checkout) |
make web-stack | Run web + server together |
make build | Build the full monorepo |
make desktop-build | Build the Electron app |
make web-build | Build apps/web |
make up | Start the self-hosted Docker stack from the published image |
make down | Stop the Docker stack |
make restart | Restart the Docker stack |
make logs | Follow Docker logs |
make status | Show Docker status |
make open | Open the self-hosted app in a browser |
make rebuild | Pull the newest image and restart |
make nuke | Remove the Docker image, data, and build output |
make clean | Remove web build output and downloaded server binaries |
Run make help to print the same summary.
The image, adibhanna/zennotes, is built and published from the server's own
repository, ZenNotes/znserver. This
repository only ships the Compose file and Makefile that run it.
make up
Then open:
When you start Docker with make up, ZenNotes mounts:
./vault -> container ./vault's absolute host path./data -> container /dataIn practice, that means the container sees the vault at the same absolute path you chose on the host, instead of rewriting it to /workspace.
The server stores its config under /data/server.json by default.
The self-hosted Docker flow is now secure by default:
127.0.0.1 unless you override itmake up and stores it in ./data/auth-tokenHttpOnly session cookieno-new-privileges, and dropped Linux capabilitiesUseful env vars:
ALLOW_INSECURE_NOAUTH=1: only use this if you intentionally want to disable authZENNOTES_ALLOWED_ORIGINS: explicit browser origin allowlistZENNOTES_BROWSE_ROOTS: restricts which server-side directories the picker can browseRecommended deployment model:
You can mount a different host content root:
CONTENT_ROOT="$HOME/Library/Mobile Documents/com~apple~CloudDocs" make up
That works for paths with spaces too.
Useful variables:
CONTENT_ROOT: host folder used as the live vault rootDATA: host directory used for persisted server configPORT: published host portIMAGE: Docker image to run (default adibhanna/zennotes:latest; pin with IMAGE=adibhanna/zennotes:2.50.5)ALLOW_INSECURE_NOAUTH: disable the default auth requirementImportant limitation:
So if you want to browse an Obsidian vault, iCloud Drive, or another directory from the web picker, that directory needs to be mounted into the container first.
The current compose/runtime flow supports:
ZENNOTES_BINDZENNOTES_CONFIG_PATHZENNOTES_DEFAULT_VAULT_PATHZENNOTES_BROWSE_ROOTSZENNOTES_VAULT_PATHBehavior notes:
ZENNOTES_VAULT_PATH hard-locks the server to a specific vault pathZENNOTES_DEFAULT_VAULT_PATH sets the starting vault when no saved selection existsZENNOTES_BROWSE_ROOTS limits what the web picker can browseZENNOTES_ALLOWED_ORIGINS restricts which browser origins can connectZENNOTES_ALLOW_UNSCOPED_BROWSE=1 removes browse-root enforcementZENNOTES_ALLOW_INSECURE_NOAUTH=1 disables the default auth guardrailThe self-hosted web build now includes a server-backed vault chooser.
If you start the server with ZENNOTES_VAULT_PATH, manual vault switching is intentionally disabled.
If auth is enabled, the browser asks for the bootstrap token once and then switches to a secure session cookie. ZenNotes no longer relies on auth tokens in browser URLs or local storage.
ZenNotes ships a dedicated MCP server and desktop install flows for:
The desktop app can:
The MCP server exposes vault operations such as:
npm run build:prod
npm run pack
npm run dist:mac
npm run dist:win
npm run dist:linux
Public macOS releases are wired for hardened runtime signing and notarization.
The GitHub Actions release workflow expects:
MACOS_CERTIFICATE_P12MACOS_CERTIFICATE_PASSWORDAPPLE_IDAPPLE_APP_SPECIFIC_PASSWORDAPPLE_TEAM_IDOptional Windows signing can be supplied with:
WINDOWS_CERTIFICATE_P12WINDOWS_CERTIFICATE_PASSWORDTagged releases fail the macOS release job if the required Apple signing or notarization secrets are missing. That prevents accidentally shipping an unsigned public mac build.
ZenNotes is actively evolving. The desktop app is the more mature runtime today, while the self-hosted web/server path is being brought into parity through the shared monorepo core.
That means:
MIT
443 followers · starred Jun 2026
740 followers · starred Jun 2026
174 followers · starred Jun 2026
419 followers · starred Apr 2026
Keyboard-first local Markdown notes with Vim motions, diagrams, and MCP integration.
See the code
[!NOTE] I build ZenNotes with a lot of help from AI tools like Claude and Codex. They let me move much faster, but I still go through some of the code before I ship it. The architecture, the technical decisions, and the trade-offs are things I still spend a lot of time thinking about. AI helps me write code faster, but it doesn't replace understanding how the system works.
ZenNotes is a keyboard-first Markdown notes app with a shared product core and multiple runtimes:
ZenNotes keeps your notes as ordinary Markdown files on disk. It adds Vim-friendly editing, split and preview workflows, tasks, tags, archive/trash, diagrams, search, daily notes, CSV databases (Notion-style Table + Board views over plain .csv files), and MCP integration on top of the files you already own.
On macOS, the first-party zen CLI also powers launcher workflows such as the Raycast extension.
Grab the latest build from GitHub Releases — see Install below.
Website: zennotes.org
Support development: GitHub Sponsors
Detailed in-repo documentation lives under docs/README.md.
All desktop installers are attached to each GitHub Release. The app auto-updates, so you only download once. (Replace <version> with the current release, e.g. 2.2.0.)
Homebrew (recommended):
brew install --cask zennotes/tap/zennotes
Or download the .dmg for your chip, open it, and drag ZenNotes to Applications. Builds are signed and notarized.
ZenNotes-<version>-mac-arm64.dmgZenNotes-<version>-mac-x64.dmgDownload and run ZenNotes-<version>-win-x64.exe.
Pick whatever suits your distro:
yay -S zennotes-bin # or: paru -S zennotes-bin
Installs cleanly without libfuse2.sudo pacman -U ZenNotes-<version>-linux-x86_64.pacman
sudo apt install ./ZenNotes-<version>-linux-amd64.deb
chmod +x ZenNotes-<version>-linux-x86_64.AppImage
./ZenNotes-<version>-linux-x86_64.AppImage
AppImages need FUSE 2. On distros that ship only FUSE 3 (Arch, CachyOS, Fedora), either install it (sudo pacman -S fuse2, sudo dnf install fuse) or run without FUSE:
./ZenNotes-<version>-linux-x86_64.AppImage --appimage-extract-and-run
(Or just use the AUR / .pacman / .deb package, which sidestep this entirely.)zen CLIThe desktop app installs a zen command-line companion from Settings → CLI — list, read, search, capture, edit, archive/trash notes, plus tasks, folders, and MCP. On macOS it can also install the Raycast extension locally.
ZenNotes also runs as a self-hosted web app backed by a Go server. See Local development to run the web client and server.
ZenNotes now ships from one monorepo with one shared app core.
desktop: Electron shell, native menus, updater, floating windows, desktop packagingself-hosted: browser frontend plus Go server, suitable for home servers and LAN usehosted: planned as the same web/server stack with auth and multi-user storage added laterThe source of truth for user-facing features is the shared UI in packages/app-core.
Every note is a normal .md file inside a chosen vault. ZenNotes does not store note content in a hidden database.
ZenNotes assumes you want to move fast:
ZenNotes supports:
ZenNotes includes a first-party MCP server and desktop install flows for compatible clients, so tools can work on the same vault you do instead of a copy.
ZenNotes can:
System folders still exist, but the vault model is more flexible now:
quick, archive, and trash remain built-in lifecycle areasinbox/The built-in folder labels are also customizable in the UI without changing the underlying internal ids.
Daily and weekly notes are optional and can be enabled from Settings.
2026-04-212026-W24Daily Notes and Weekly Notesyyyy/MM-MMM and yyyy-MM-dd-EEEyyyy-'W'wwsystem, en-US, pt-BR, or another BCP 47 localeSupported date note pattern tokens:
yyyy / yy for 2026 / 26M / MM / MMM / MMMM for 6 / 06 / Jun / Juned / dd for 9 / 09EEE / EEEE for Tue / Tuesdayw / ww for ISO week numbers like 24'Daily Notes'/yyyy/MM-MMMWeekly notes render date tokens from the ISO week's Monday, and yyyy is the ISO week-year for weekly patterns.
The editor stack is CodeMirror 6 with a Markdown-oriented workflow:
Preview and split mode support:
ZenNotes includes:
Vault text search can use the built-in engine, ripgrep, or fzf, with auto-detection and optional custom binary paths.
The desktop app also ships a zen command-line companion for list, read, search, capture, edit, archive/trash, task, folder, and MCP workflows. On macOS, ZenNotes can install its Raycast extension locally from Settings -> CLI, avoiding the Raycast Store review path. The integration uses the CLI plus zennotes:// deep links to search notes, open them in the main app, open them in floating windows, archive/unarchive, move notes to Trash, reveal files in Finder, and copy note paths or wikilinks.
ZenNotes now works better with existing Obsidian-style vaults.
inbox/![[image.png]] resolve more like Obsidianattachements/ and _assets/ folders are still recognizedThis means imported vaults with top-level notes, folders, and loose images/files behave much more naturally.
ZenNotes supports local files in notes and in the sidebar/list views.
Sidebar multi-select supports platform modifiers: use Cmd/Ctrl-click to toggle individual notes or folders, Shift-click to select a visible range, then use the context menu to apply actions such as open in tabs, move, archive, trash, restore, delete folders, copy paths, or drag the selected group to a folder.
The settings surface includes:
Both runtimes share the same core app, but they do not expose identical platform features.
Desktop-only features include:
zen CLI install/uninstall flowWeb/self-hosted mode includes:
ZenNotes now uses a single monorepo.
apps/
desktop/ Electron shell, preload, updater, packaging
web/ Vite/PWA shell and HTTP bridge
packages/
app-core/ Shared React application and renderer logic
bridge-contract/ Typed runtime contract between UI and host
shared-domain/ Shared types and note/task/view models
shared-ui/ Reusable UI primitives
tooling/
scripts/ Shared tooling hooks and migration scripts
docs/
Read docs/monorepo-architecture.md for the architectural boundary between the shared app core and the platform-specific shells.
npm ci
npm run dev:desktop
or:
make desktop
npm run dev:web
or:
make web-dev
The server lives in its own repository,
ZenNotes/znserver. For browser work
you do not need it checked out: npm run dev:server downloads the release
pinned in tooling/server-release.json, verifies its checksum, and runs it.
To work on the server itself, point ZENNOTES_SERVER_DIR at a znserver
checkout and the same command runs it with go run.
npm run dev:server
or:
make server-dev
npm run dev:web-stack
or:
make web-stack
Important dev note:
From the repository root:
| Script | Purpose |
|---|---|
npm run dev | Alias for npm run dev:desktop |
npm run dev:desktop | Run the Electron desktop app in development |
npm run dev:web | Run the Vite web client |
npm run dev:server | Run the Go server |
npm run dev:web-stack | Run web + server development together |
npm run start | Start the built desktop app |
npm run typecheck | Run monorepo typechecks |
npm run test | Run monorepo tests |
npm run test:run | Run the full test suite |
npm run build | Build the monorepo and then build the Go server |
npm run build:prod | Typecheck + test + build |
npm run pack | Desktop packaged output |
npm run dist:mac | Build macOS desktop distributables |
npm run dist:win | Build Windows desktop distributables |
npm run dist:linux | Build Linux desktop distributables |
The root Makefile provides a simpler interface:
| Command | Purpose |
|---|---|
make install | Install workspace dependencies |
make desktop | Run the Electron app in dev mode |
make web-dev | Run the web client |
make server-dev | Run the pinned Go server release (or a checkout) |
make web-stack | Run web + server together |
make build | Build the full monorepo |
make desktop-build | Build the Electron app |
make web-build | Build apps/web |
make up | Start the self-hosted Docker stack from the published image |
make down | Stop the Docker stack |
make restart | Restart the Docker stack |
make logs | Follow Docker logs |
make status | Show Docker status |
make open | Open the self-hosted app in a browser |
make rebuild | Pull the newest image and restart |
make nuke | Remove the Docker image, data, and build output |
make clean | Remove web build output and downloaded server binaries |
Run make help to print the same summary.
The image, adibhanna/zennotes, is built and published from the server's own
repository, ZenNotes/znserver. This
repository only ships the Compose file and Makefile that run it.
make up
Then open:
When you start Docker with make up, ZenNotes mounts:
./vault -> container ./vault's absolute host path./data -> container /dataIn practice, that means the container sees the vault at the same absolute path you chose on the host, instead of rewriting it to /workspace.
The server stores its config under /data/server.json by default.
The self-hosted Docker flow is now secure by default:
127.0.0.1 unless you override itmake up and stores it in ./data/auth-tokenHttpOnly session cookieno-new-privileges, and dropped Linux capabilitiesUseful env vars:
ALLOW_INSECURE_NOAUTH=1: only use this if you intentionally want to disable authZENNOTES_ALLOWED_ORIGINS: explicit browser origin allowlistZENNOTES_BROWSE_ROOTS: restricts which server-side directories the picker can browseRecommended deployment model:
You can mount a different host content root:
CONTENT_ROOT="$HOME/Library/Mobile Documents/com~apple~CloudDocs" make up
That works for paths with spaces too.
Useful variables:
CONTENT_ROOT: host folder used as the live vault rootDATA: host directory used for persisted server configPORT: published host portIMAGE: Docker image to run (default adibhanna/zennotes:latest; pin with IMAGE=adibhanna/zennotes:2.50.5)ALLOW_INSECURE_NOAUTH: disable the default auth requirementImportant limitation:
So if you want to browse an Obsidian vault, iCloud Drive, or another directory from the web picker, that directory needs to be mounted into the container first.
The current compose/runtime flow supports:
ZENNOTES_BINDZENNOTES_CONFIG_PATHZENNOTES_DEFAULT_VAULT_PATHZENNOTES_BROWSE_ROOTSZENNOTES_VAULT_PATHBehavior notes:
ZENNOTES_VAULT_PATH hard-locks the server to a specific vault pathZENNOTES_DEFAULT_VAULT_PATH sets the starting vault when no saved selection existsZENNOTES_BROWSE_ROOTS limits what the web picker can browseZENNOTES_ALLOWED_ORIGINS restricts which browser origins can connectZENNOTES_ALLOW_UNSCOPED_BROWSE=1 removes browse-root enforcementZENNOTES_ALLOW_INSECURE_NOAUTH=1 disables the default auth guardrailThe self-hosted web build now includes a server-backed vault chooser.
If you start the server with ZENNOTES_VAULT_PATH, manual vault switching is intentionally disabled.
If auth is enabled, the browser asks for the bootstrap token once and then switches to a secure session cookie. ZenNotes no longer relies on auth tokens in browser URLs or local storage.
ZenNotes ships a dedicated MCP server and desktop install flows for:
The desktop app can:
The MCP server exposes vault operations such as:
npm run build:prod
npm run pack
npm run dist:mac
npm run dist:win
npm run dist:linux
Public macOS releases are wired for hardened runtime signing and notarization.
The GitHub Actions release workflow expects:
MACOS_CERTIFICATE_P12MACOS_CERTIFICATE_PASSWORDAPPLE_IDAPPLE_APP_SPECIFIC_PASSWORDAPPLE_TEAM_IDOptional Windows signing can be supplied with:
WINDOWS_CERTIFICATE_P12WINDOWS_CERTIFICATE_PASSWORDTagged releases fail the macOS release job if the required Apple signing or notarization secrets are missing. That prevents accidentally shipping an unsigned public mac build.
ZenNotes is actively evolving. The desktop app is the more mature runtime today, while the self-hosted web/server path is being brought into parity through the shared monorepo core.
That means:
MIT
443 followers · starred Jun 2026
740 followers · starred Jun 2026
174 followers · starred Jun 2026
419 followers · starred Apr 2026