Dark Oak is a local-first, single-binary Markdown notebook intended to
replace Obsidian for users who want a small, auditable, dependency-light
alternative. It runs a local web server on 127.0.0.1, serves a minimal
SvelteKit UI, edits Markdown files in CodeMirror 6 with a live preview,
and auto-commits every save to a local git repository.
The goal is to keep the attack surface small, the dependency set small, and the codebase small.
- Single static binary to run; no daemon, no install step beyond copying the binary.
- Markdown files are the only source of truth and live on the filesystem.
- Every save is a git commit. History is plain
git log. - The interface is minimal: a file tree on the left, an editor and a live preview on the right.
- Renames and moves are tracked correctly in git (
git mv).
- Cloud sync, mobile apps, real-time collaboration, accounts.
- A plugin system or theming engine.
- Non-Markdown file editing.
- Network exposure. The server binds
127.0.0.1only.
- File tree in a left side-pane (toggleable). Files and folders can be created, renamed, moved, and deleted. Drag-and-drop reorders within the tree, and a per-item menu (⋯) offers rename / delete / new file / new folder. ⊞ All / ⊟ All expand or collapse every folder.
- Markdown editor in the main pane, using CodeMirror 6 with a Markdown mode and syntax highlighting in fenced code blocks.
- Live preview pane to the right of the editor, rendered client-side
with
markedand sanitized withDOMPurify(100 ms debounce). Ctrl+Ssaves the current file. Every save is also a git commit, with messageupdate: <relative path>. The footer shows the most recent commit hash. Renaming via the title bar usesgit mv, sogit log --followworks across renames.- Wikilinks —
[[note name]]links are highlighted in the editor and rendered as clickable links in the preview. Links resolve relative to the current file first, then the vault root. Unresolved links are styled distinctly. - Full-text search (
Ctrl+Shift+F) backed bygit grep, returning matching lines across the whole vault. - File switcher (
Ctrl+P) for fuzzy-jumping to any note by name. - Dark / light theme (default dark, matching the project name),
persisted in
localStorage. Side-pane visibility is also persisted. - Footer showing the most recent commit hash.
- Backend: Go, stdlib
net/httponly. No web framework.os/execis used to call the systemgitbinary for commits and moves. SvelteKit static assets are embedded via//go:embedand served by the same binary. - Frontend: Svelte 5 (runes) + SvelteKit, built to a static bundle.
CodeMirror 6 is the editor.
marked+DOMPurifyrender the preview. - Storage: a vault directory (a plain folder) is passed via
--vault. On first run, Dark Oak runsgit initin it if needed and creates an initial commit. All subsequent saves commit on top.
darkoak/
├── README.md
├── LICENSE
├── go.mod
├── main.go # flag parsing, embed, server bootstrap
├── Makefile
├── darkoak.service # optional systemd user unit
├── internal/
│ └── server/ # http handlers, embed, routes, git, path
│ ├── handlers.go # API routes + security headers + SPA fallback
│ ├── git.go # EnsureRepo, commitFile, movePath, removePath, gitLog, gitGrep
│ └── path.go # resolvePath — vault-relative, no traversal, no symlink escape
├── frontend/
│ ├── src/
│ │ ├── routes/ # SvelteKit pages (+layout, +page)
│ │ └── lib/ # FileTree, Editor, Preview, Switcher, Search, store, api
│ └── tests/ # vitest
├── scripts/
│ └── smoke.sh # end-to-end smoke test
└── .github/
└── workflows/
└── build.yml # CI: test + cross-compile + release on tags
| Method | Path | Body | Effect |
|---|---|---|---|
| GET | /api/tree |
— | JSON tree of the vault |
| GET | /api/file?path= |
— | Raw text content of a .md file |
| PUT | /api/file?path= |
text/plain | Write + auto-commit |
| POST | /api/file?path= |
— | Create empty .md file |
| POST | /api/folder?path= |
— | Create folder |
| POST | /api/move |
{from, to} |
git mv (file or folder) |
| DELETE | /api/file?path= |
— | git rm (recursive for folders) |
| GET | /api/log?limit= |
— | Recent commits |
| GET | /api/search?q= |
— | Full-text search via git grep |
| GET | /api/status |
— | Whether the vault has a git repo |
| POST | /api/init |
— | git init + initial commit (idempotent) |
- The server binds
127.0.0.1only. There is no auth; the threat model is a single user on a single machine. - All file paths are validated relative to the vault root.
..segments are rejected, and any symlink is resolved and re-validated. - The server only accepts
.mdfiles for write/create. Other files in the vault are listed in the tree but not editable. - Markdown is rendered client-side from a vetted library (
marked) and sanitized withDOMPurifybefore insertion. - A strict CSP is emitted by SvelteKit as a
<meta>tag inindex.html, with auto-computed hashes for inline scripts. No remote loads.X-Content-Type-Options: nosniffandReferrer-Policy: no-referrerare set by the Go server.
Prerequisites: Go 1.22+, Node 20+, git on $PATH.
make build # builds frontend, embeds it, builds ./darkoak
./darkoak --vault ~/notes
# open http://127.0.0.1:7331/
Flags:
--vault <path>vault directory (default./vault).--addr <host:port>listen address (default127.0.0.1:7331).
Development (two terminals):
# terminal 1
make dev-go # go run .
# terminal 2
make dev-fe # vite dev on :5173, proxies /api to the Go server
Smoke test (builds, runs the binary, exercises the API, prints git log):
make build && bash scripts/smoke.sh
An optional darkoak.service unit is included for running Dark Oak as a
systemd user service. Install the binary and register the unit:
install -m 0755 darkoak ~/.local/bin/darkoak
install -m 0644 darkoak.service ~/.config/systemd/user/darkoak.service
systemctl --user daemon-reload
systemctl --user enable --now darkoak
The unit runs darkoak --vault ~/notes --addr 127.0.0.1:7331. Edit the
ExecStart line to point at a different vault if needed.
- Go:
go test ./...— table-driven tests for path validation, handlers, and git operations against a temp git repo. - Frontend:
npm test -- --run --prefix frontend— Vitest unit tests forFileTree, the editor wrapper, and the store. (Drop--runfor watch mode.) - All:
make testruns both suites.
MIT — see LICENSE.
