All projects
04OPEN SOURCE2026
Kanso app icon: a dark ensō brush circle with an orange dot on a cream tile

Kanso

A calm, local-first Markdown notes app in Rust — with sync the server can't read, and notes you can lock with their own password.

Visit kansoapp.appmacOS · Windows · Linux
kansoapp.app
Kanso in light mode: notebooks and tags in the sidebar, a note list, and a note shown as Markdown source beside its rendered preview

Overview

What is Kanso

Kanso is a local-first Markdown notes app for macOS, Windows and Linux, written in Rust on GPUI: wiki-links and backlinks, tasks with due dates, on-device dictation, version history, and end-to-end encrypted sync through a Cloudflare Worker that only ever holds ciphertext. It is Ngonidzashe Mangudya's project (AGPL-3.0); it also exposes your notes to AI agents through a local REST and MCP API, on your terms.

Role
Open-source contributor — I built locked notes (sealed on the device and through sync), Touch ID unlock, and the notebook accordion, icon picker and colour washes. The product and its architecture are the creator's work.
Created by
Ngonidzashe Mangudya
Platform
macOS · Windows · Linux
Stack
Rust · GPUI · SQLite · Cloudflare Workers · D1 · R2
Crypto
Argon2id · XChaCha20-Poly1305 · AES-256-GCM
Status
v0.8.4 · AGPL-3.0 · signed auto-updates
Web
kansoapp.app

The project

What it does

  1. 01Writing that stays out of the way: Markdown pairs and continues as you type, [[wiki-links]] autocomplete into backlinks and unlinked mentions, - [ ] task @2026-10-25 becomes a dated task with reminders, and the mic dictates into the cursor with NVIDIA's Parakeet model running on the CPU — nothing leaves the device.
  2. 02A SQLite library with trigram full-text search, every replaced version kept in history (whether the edit came from you, the API, sync or an import) and a daily backup kept for two weeks.
  3. 03Sync where the server is blind: notes, titles, IDs and attachments are sealed with XChaCha20-Poly1305 under keys derived from an Argon2id master key that never leaves the device; a Worker stores ciphertext in D1 and R2, and on a conflict the newer edit wins while the loser stays in the note's history.
  4. 04Publishing as an encrypted link: the page is sealed with its own AES-256-GCM key and the key rides in the URL fragment, which browsers never send — the server keeps a copy it cannot read.
  5. 05Agents on a short leash: a local REST API compatible with Inkdrop's plus a native MCP endpoint on port 19840, short-lived tokens scoped to chosen notebooks, and suggested edits that wait in a review queue for Apply. Locked, trashed and #ai-exclude notes are never sent.
  6. 06Locked notes (my contribution): a note sealed with its own password — Argon2id at 256 MiB into XChaCha20-Poly1305, bound to the note's ID — whose plaintext history is erased on lock, which travels through sync unchanged and opens on any device with the same password, or with Touch ID on a Mac.

Screens

Typing [[s in a note opens an autocomplete list of note titles, ending with an option to create a new note
Typing [[ suggests notes to link — or creates one.
The inspector panel listing notes that link to the current note, and an unlinked mention with a Link button
Backlinks with context, plus unlinked mentions one click from becoming links.
Kanso in dark mode: a Tasks view grouping overdue, today, upcoming and undated tasks beside a launch-plan note with dated checkboxes
Tasks with @dates gather into overdue, today and upcoming, across every note.
Note history dialog listing versions edited in Kanso, replaced through the API, replaced by sync and replaced by an import, with a line diff
Every replaced version is kept — including ones replaced by the API, sync or an import.
Publish to the web dialog with a share link, an expiry choice of always, 1, 7 or 30 days, and an Unpublish button
Publishing keeps only ciphertext on the server; the key travels in the link.
An Excalidraw sketch embedded in a dark-mode note, showing Kanso sending a note to a Worker
Excalidraw drawings render inside notes, in the note's theme.

How it works

How the data moves

The core paths through the system — a dot travels each hop, and the caption says what happens there.

Lock a note, open it on another device

The password never leaves your head; the note travels sealed twice over.

YESNOStart
Lock note… with a password (8–256 chars)
Argon2id · random salt · 256 MiB
Seal the body into extra.kansoLock
Drop old versions · secure_delete
Sync seals the document again and pushes it
Another device pulls it — body still sealed
Right password or Touch ID?
Same salt + cost → same key · note opens
End
Wrong password — nothing opens

Swipe sideways to see the whole chart.

01 / 11Lock note…

You choose Lock note… from the palette or the Note menu.

From the code

A few lines that matter

Sealing a locked notecrates/kanso-sync/src/lock.rs
fn seal_with_nonce(&self, note_id: &str, body: &str, nonce: &[u8; NONCE_LEN]) -> Envelope {
    let plaintext = Zeroizing::new(
        serde_json::to_vec(&Sealed {
            body: body.to_owned(),
        })
        .expect("a string serializes"),
    );
    Envelope {
        v: FORMAT_V1,
        salt: b64(&self.salt),
        m: self.params.m_kib,
        t: self.params.t,
        p: self.params.p,
        data: b64(&seal_raw(&self.key, nonce, &aad(note_id), &plaintext)),
    }
}

The envelope carries its own salt and Argon2 cost, and the note ID is bound in as associated data — a sealed body can't be moved onto another note.

Notebook colours from hexcrates/kanso-app/src/book_colors.rs
pub fn parse_hex(text: &str) -> Option<u32> {
    let text = text.trim();
    let digits = text.strip_prefix('#').unwrap_or(text);
    if !digits.chars().all(|c| c.is_ascii_hexdigit()) {
        return None;
    }
    let value = u32::from_str_radix(digits, 16).ok()?;
    match digits.len() {
        6 => Some(value),
        // Each digit doubled: `#fa0` is `#ffaa00`.
        3 => {
            let (r, g, b) = ((value >> 8) & xf, (value >> 4) & xf, value & xf);
            Some(((r * x11) << 16) | ((g * x11) << 8) | (b * x11))
        }
        _ => None,
    }
}

Typed hex, a swatch or the picker all end as one #rrggbb stored on the notebook, which syncs with it.

Decisions

Calls that shaped it

Seal locked notes inside the note, not beside it

The envelope — salt, Argon2 parameters, ciphertext — lives in the note's own extra.kansoLock field, so sync carries it with no protocol change and another device derives the same key from the same password.

Erase the plaintext when a note is locked

Locking drops earlier versions and the note's embedding in the same transaction, and SQLite's secure_delete overwrites the freed pages — a test scans the database and WAL for the old text.

Touch ID stores keys, never the password

The derived lock keys go in the login Keychain behind a biometric prompt, with the password as the fallback — and the README says plainly that this guards against someone at your unlocked Mac, not software running as you.

Notebook looks are just synced fields

A notebook's icon is a Lucide name in kansoIcon and its colour a hex in kansoColor, written in one document update — so they sync end to end like any note, with no new server schema.

Stack

  • Rust
  • GPUI
  • SQLite
  • Cloudflare Workers
  • D1
  • R2
  • Argon2id
  • XChaCha20-Poly1305
  • MCP