# Hanji
> Everything in this knowledge base that is open to everyone, mirrored as plain Markdown for agents and people alike.
---
---
title: The core
order: 20
description: "@hanji/core: mounts, git, the index, search, permissions, propose, and history."
---
# The core
`@hanji/core` is the rail everything runs on. If the serializer is the paper, core is the press. It takes your git repositories and turns them into something you can read, search, and permission.
## What it holds
- **Mounts and git.** Core clones each mount, keeps it current, and writes through it. Git is driven as a subprocess, the same git you already have, so there is no reimplementation of version control to trust.
- **The index.** A SQLite database with full-text search and a table of wikilinks, split in two: a config database for mounts and tokens, and an index database for pages. It also keeps one fingerprint per mount standing for every non-Markdown file in it, which is how a changed image becomes visible both as a change and as a new asset URL. The index is a cache, rebuildable at any time from the clones.
- **Permissions.** One module, and one rule: every read the app serves flows through it, and the scope check is compiled in exactly one place. Search, page reads, and link resolution all pass through the same door. A page you cannot see is indistinguishable from one that does not exist. Read [[Permissions]].
- **Writes.** `saveFile` commits directly, with read-your-own-writes, guarded by a per-mount lock so two writes cannot race - and the lock holds across processes, so the web front, the MCP server, and the CLI sharing one data directory cannot wedge a clone between them. `proposeSave` instead opens a branch, and where GitHub is connected, a pull request.
- **History.** Because the store is git, a page's past is just its commits. Core reads them into a list of revisions, and can hand back any past version.
## Why SQLite, and why a subprocess git
Both are the boring, durable option. SQLite is a file, not a server, so the index has no daemon to run and nothing to operate. Driving the real `git` binary means Hanji inherits whatever your git already does, credentials and remotes and all, instead of reimplementing a fraction of it and getting the edge cases wrong.
Read [[The MCP server]] for how agents reach this, and [[The web front]] for how people do.
---
---
title: The editor
order: 30
description: "@hanji/editor: a ProseMirror WYSIWYG that edits Markdown and keeps byte-fidelity per block."
---
# The editor
`@hanji/editor` is where the hard promise gets harder. Byte-fidelity is one thing when a person edits raw Markdown. It is another thing entirely to put a full WYSIWYG editor in front of them, let them work in rich blocks, and still write back Markdown that touches only what they changed.
## How it keeps the promise
The editor is built on ProseMirror, with `prosemirror-markdown` translating between Markdown and the editing model, over the same [[The serializer]] underneath. The trick is that it tracks which blocks a person actually edited. On Update, only those dirty blocks are re-serialized. Every block left alone is written back from its original bytes, the same reconciliation the serializer does, carried up into a live editor.
So you get tables with real controls, task lists you can check, a slash menu, images, and embeds, and the diff of your one-word fix is still one line. The rich editing experience and the clean git history are not in tension here, because the editor was built to keep both.
## What is inside
The package is a handful of focused files: the schema that defines the editable blocks, the parse and serialize steps that bridge Markdown, the reconciler that decides what to rewrite, and the table handling, which is its own small world of hover controls and moves. Tables move with a built-in ProseMirror command rather than a custom transform, so a moved row carries its per-cell alignment for free.
Read [[The web front]] for how this editor is placed into the reading experience, and [[Byte-fidelity]] for the promise it protects.
---
---
title: How it's built
order: 30
description: The four packages and one app that make up Hanji, and how a read flows through them.
---
# How it's built
Hanji is a small pile of parts with clear seams. Four packages and one web app, each doing one thing, each testable on its own. Here is the whole map.
## The parts
| Part | What it is | Depends on |
|---|---|---|
| `@hanji/serializer` | The byte-fidelity Markdown engine: parse a file into blocks, splice edits back without touching what you left alone | nothing |
| `@hanji/core` | The rail: mounts, git, the SQLite index, search, permissions, propose, history | the serializer |
| `@hanji/editor` | The WYSIWYG surface: a ProseMirror editor that edits Markdown and preserves byte-fidelity per block | the serializer |
| `@hanji/mcp` | The agent surface: a small Streamable HTTP server exposing scoped tools | core |
| `apps/web` | The human front: a Next.js app that composes the editor and core into a reading and writing experience | core, editor |
Two things stand out on that table. The serializer depends on nothing, because byte-fidelity is the foundation everything else trusts. And nowhere in the column is a model, an AI SDK, or a metering client, because there is none.
## How a read flows
1. A repository is a **mount**. `@hanji/core` clones it and walks the tree, indexing each Markdown file into SQLite, with a full-text index and a table of wikilinks.
2. A read, from a person or an agent, names a mount and a path. It goes through the one permission check in core, which filters to the mounts the caller may see.
3. The bytes come back from the index, rendered on the front or handed to the agent as Markdown.
The index is a cache. Delete it, run `rebuild`, and it comes back from the clones, because git is the only thing that has to be trusted.
## How a write flows
A person edits on the front, and `apps/web` asks `@hanji/editor` for the new Markdown, block-fidelity intact, then core commits it to git. An agent instead calls `hanji_propose`, and core opens a branch. One path writes, the other suggests, and both end in git.
Read the parts in depth: [[The serializer]], [[The core]], [[The editor]], [[The MCP server]], and [[The web front]].
---
---
title: The MCP server
order: 40
description: "@hanji/mcp: a small Streamable HTTP server exposing scoped tools to agents."
---
# The MCP server
`@hanji/mcp` is the hosted half of the agent surface. The other half is git itself, which agents already read. This server is for the times a scoped, live read helps: an agent that should see some mounts and not others, searching and reading through the same permission check a person gets.
## How it works
It is deliberately small and stateless. It speaks MCP over Streamable HTTP, bound to `127.0.0.1` only, and every request carries a bearer token. There is no session to hold and no state to corrupt. Each request authenticates its token to a principal, builds a fresh server scoped to that principal, answers, and tears down.
The token's scopes decide everything. The server hands the same principal to `@hanji/core`, so an agent sees exactly what its token allows and no more, and the permission logic lives in core, not here. This surface only exposes it.
## The tools
Four for everyone, and a fifth you opt into - the surface stays small enough to hold in your head.
- `hanji_list_pages`, `hanji_get_page`, and `hanji_search` are the read side.
- `hanji_propose` is the write side, and it does not write. It proposes a branch.
- `hanji_list_comments` reads a page's comments, but only for a token minted with the opt-in `read+comments` capability - comments are a human side-channel, off by default.
That last line is the whole design in one tool. An agent contributes by suggesting, never by overwriting. Read [[Agents]] for how to connect one, and [[The core]] for what sits behind these tools.
## The curl aliases
The same four operations exist as plain HTTP endpoints beside `/mcp` - `GET /pages`, `GET /page`, `GET /search`, `POST /propose` - same bearer token, same principal, same permission check, only the wire dialect differs. They exist because the Streamable HTTP dialect is a poor fit for a shell one-liner, and half the point of an agent-native surface is that a bash script counts as an agent.
---
---
title: The serializer
order: 10
description: "@hanji/serializer: parse a Markdown file into blocks, splice edits back byte-for-byte."
---
# The serializer
`@hanji/serializer` is the foundation, and it depends on nothing. Its whole job is the promise in [[Byte-fidelity]]: read a Markdown file, let something edit part of it, and write it back so that every byte you did not touch is exactly where it was.
## Two functions
The public surface is small.
- `parseBlocks` reads a Markdown document into a list of **source blocks**, each one carrying the exact byte range it came from. The parse never loses the original. It keeps a map back to it.
- `spliceBlocks` takes that list and a set of edits, and produces the new document. Edited blocks are re-serialized from their new content. Untouched blocks are copied back byte-for-byte from the original source. Nothing you left alone is ever reformatted.
Because unchanged blocks are copied and not regenerated, a splice that changes nothing returns the input unchanged, to the byte. That is what makes a no-op save commit nothing.
## The corpus gate
A promise like this is only as good as its test. The serializer carries a corpus gate: point it at a directory of real Markdown with `KB_CORPUS_DIR`, and it round-trips every file through parse and splice, and asserts the output is byte-identical to the input.
```bash
KB_CORPUS_DIR=/path/to/markdown pnpm vitest run packages/serializer/test/corpus.test.ts
```
It has run green over 600-plus real files. The rule is simple and absolute: the gate does not get weakened to make a change pass. If a file does not round-trip, the engine is wrong, not the test. Read [[The editor]] for how this same guarantee survives a full WYSIWYG editor sitting on top.
---
---
title: The web front
order: 50
description: "apps/web: the Next.js human front that hides git and renders on warm paper."
---
# The web front
`apps/web` is the face a person sees. It is a Next.js application, and its entire job is to make a git-backed knowledge base feel like a calm editorial tool, with git nowhere in sight.
## What it does
- **Renders.** Pages are rendered per block through a sanitizing pipeline, so what reaches the browser is safe and clean. Wikilinks are resolved and scoped to what you can read. The result is the warm paper you are reading on.
- **Edits.** It places [[The editor]] into the page in place, and on Update it asks core to commit. A no-op change commits nothing. A collision with a newer version stops with a conflict rather than clobbering it.
- **Searches.** One FTS5 backend (bm25 with a heavy title boost, hit-marked snippets, multi-word coverage ranking) serves the `⌘K` palette, the results page, and the agents' `hanji_search` alike.
- **Organizes.** Sidebar drag & drop is optimistic: pure tree transforms apply instantly while core does the git work behind, and a failure reverts with a toast. A move is `git mv`; a reorder writes `order:` frontmatter as one commit.
- **Guards the door.** The front is single-owner today, behind a signed session. Every read it makes still goes through core's one permission check, so the front has no private path around permissions.
## What it is not
It is not the source of truth, and it is careful never to become one. It holds no content of its own. Everything it shows comes from core, which reads from git. Turn the front off, and your knowledge is exactly where it was, in your repositories. That is the point of building it this way. See [[The core]] for what it sits on, and [[The two faces]] for the design behind having a human front at all.
---
---
title: Changelog
order: 130
description: The road behind us, honestly logged.
---
# Changelog
Reverse chronological. Struck-through text is a decision we made and then moved past, kept visible on purpose, because a changelog that hides its own wrong turns is just marketing.
## 0.6, and more than one hand
- The number reads 0.6. What 0.5 could not do: hold a second person on a page, survive two edits landing at once without asking anyone to throw work away, or tell you what moved while you were gone. All three are here now - editors who commit straight to git, a collision that folds instead of dead-ending, presence beside the byline, and a periphery (Activity, the digest, the agent briefing channel) that answers "what happened" without anyone taking notes.
- The same jump that took 0.1 to 0.5 rather than a release ritual: the number keeps one home, the repository's own version, and every surface that prints it - the sidebar, the agent banner - takes it from there, so what you read can never drift from what shipped.
## The published site reads like the app
- The exported site now carries the app's own reading surface, not a copy of it. `packages/render/prose.css` is one file: the app imports it, `hanji export` inlines it. It had been a second stylesheet written in August and left behind while the app moved on, and the two had drifted for weeks - highlights in four colours and the accent-tinted code panel shipped in the app and never reached the published page at all.
- What you see change: prose set at the app's reading size and heading rhythm, links in ink with an accent-tinted underline rather than painted accent throughout, inline code and code blocks carrying the accent hue, table headers in small caps, the paper grain, and an accent selection wash. A keyboard focus ring too, which the published site never had.
- A three-state theme control sits at the top of every page - auto, light, dark - remembered in the browser. ~~The published site was deliberately script-free~~; it was also the one Hanji surface where a reader could not choose. It is ten inlined lines and nothing remote; without JavaScript the page still renders in full and follows your system, exactly as before, and the control hides itself rather than sitting there dead.
## Activity shows the edits nobody committed, and the note only speaks when it can point
- On a local mount that sits below its repo root (a `notes/` folder mounted out of a larger repo), every commit row on Activity linked to a page that does not exist: git names files from the repo root wherever it runs, the feed passed those names through, and the link carried the folder twice and landed on an empty shell. The feed now speaks the mount's own paths, and a commit that also touched files above the mount loses its subject the way a subpath mount's does - a message can describe what the mount never shows.
- Activity lists the edits still sitting uncommitted in a local mount's files as one row per mount - "the files", labeled outside, "changed, not yet committed" - timed by the newest file, pages only. The freshness note announces those edits the moment an agent writes them, and the page it pointed at used to answer "nothing today". `hanji_changes` and the digest read the same feed, so an agent's session-start briefing sees them too.
- A change that moved no page - a replaced picture, a deleted page - refreshes the page and says nothing. ~~It said "Updated just now · notes" and pointed at Activity~~, which cannot show a picture; during a burst of screenshots landing in a mount that was one toast every ten seconds. An open editor no longer gets the early conflict line for it either: no page moved, so nothing changed under the draft.
## A table cell can hold more than one line
- Enter inside a table cell used to do nothing at all. A cell is a single line, so there was nothing for the key to split, and it quietly declined. Enter now puts a line break in the cell, and `⇧Enter` does the same anywhere else in the page.
- A break is written to disk as ` `, the only spelling a Markdown table row can carry - and now the only spelling Hanji uses for a break anywhere, so one round-trips wherever it lives. It reads back as a break too: a cell written by an agent as `first second` showed you the raw angle brackets before, and now shows you two lines.
- A list inside a cell is those broken lines with their own dashes, which is as far as a Markdown table goes. Typing `- ` at the head of a cell no longer saves as `\- ` either: the escapes that guard block syntax do not belong in a cell, where no block syntax can occur.
## The note that says something changed now takes you there
- The freshness note used to name the mount and stop: "Updated just now · handbook", with no way to see what had actually moved. The sync knew the count and nothing else, so there was nothing to link to.
- Sync now reports which pages moved, capped at five and with deletions left out (a link to a page that no longer exists is worse than no link). When exactly one page moved, the note names it and offers a link straight to it. When several did, it names the mounts and points at Activity, which exists to answer "what moved". When the page that moved is the one you are reading, the refresh already showed you, so it says "this page" and offers nothing to click.
- A folder's `index.md` is named by its folder and a mount's by its mount, so the note reads "assetdemo" rather than "index".
- Toasts are also the width of what they say now. The container's width was being inherited as every notice's width, so a short one sat in a wide box with a long empty right side.
## More than one hand writes to base
- A third level for people: `read+write` makes an **editor**, whose Update commits straight to git the way the owner's does, on a mount, a folder, or one page. Viewers read, contributors suggest, editors write, the owner decides. A token never holds it: agents keep proposing.
- A contributor's conflict folds like the owner's. When the page moved under a contributor, Hanji merges the two edits before proposing, so the proposal carries both, and only a true overlap stops to ask. And **Keep my version** no longer overwrites the other side: it keeps your wording where you both changed the same lines and folds in the rest of their edit, on the commit path and the propose path alike. ~~Keep mine replaced theirs entirely~~ - that was the data-loss door, and the contributor side was about to inherit it.
- The open page checks itself every seven seconds while its tab is visible, editing or reading: an editor gets the quiet chip, a reader gets the page repainted in place, and this holds for saves made in the app, not just edits arriving from the files. The chip names the other hand where the name is honest (the last committer on a git-backed mount) and says "This page changed" on a plain folder.
- Presence, the first primitive of the room to come: a page says "Remy is editing" beside its byline, and an editor sees "Remy is also editing this page. Edits merge when you save." In-process, twenty seconds of memory, no lock, no take-over door.
- Two serializer fixes. An ordered list running past nine (`1.` to `10.`) saved with a leading space and refused to reconcile; it now writes its numbers unpadded. And a re-serialized block escaped `[[wikilinks]]` into `\[\[...\]\]`, which the link index no longer saw; wikilinks now survive an edit verbatim. The handbook's own guide index, ten wikilinked items, could not be saved before.
## A replaced image reaches the page
- An agent swapping a picture used to be invisible. The index only ever walked `.md`, so the scan that watches for change never looked at an image, reported that nothing had moved, and no open page refreshed. And even when something else did force a render, the image's URL had not changed, so the browser kept serving the copy it already held. Quitting the app was the only way through.
- Every mount now carries a fingerprint of its assets: names, sizes and modification times, never contents, so it costs the same stat scan the page check already pays for. When it moves, the mount counts as changed and the page refreshes exactly as it does for a prose edit.
- That same fingerprint is stamped into every image URL, so a changed asset arrives at an address the browser has never held. Versioned URLs can then be cached properly: images are immutable for a year instead of being re-fetched every five minutes.
- The trade, said plainly: changing one image moves the URL of every image in that mount, so they all fetch once more. The value is already computed by a scan that was happening anyway, and the alternative is hashing every image on every render.
## Highlights, in four colours
- Highlight a phrase while editing: `⌘⇧H`, the swatches in the selection bubble and the bottom toolbar, or just type `==like this==`. Four curated colours - a default yellow, then blue, green, and pink.
- The default writes portable `==x==` on disk, the spelling every other Markdown tool already understands. A chosen colour writes ``, because standard Markdown has no way to name a colour. The common case stays clean Markdown; colour costs portability only when you ask for one. Both read back as the same highlight, so the editor never shows you raw angle brackets.
- Colours ride a fixed class through the sanitize allowlist, never a `style` attribute - the same rule that governs image widths and embeds. A colour the workspace does not know stays literal text rather than being quietly recoloured.
- The tint is mixed into the page's own paper, so the same four swatches stay legible when the workspace theme flips dark.
## The page keeps its chrome, and a selection keeps its text
- The header bar - back and forward, the breadcrumb, the page's actions - sticks to the top of the view now, so a long page never scrolls away from its own controls. The hairline under it belongs to the stuck state alone: at rest the bar is simply the page's first row, and the rule arrives only once there is prose running underneath for it to separate.
- Reading and editing draw the *same* bar. Entering edit mode swaps its buttons instead of rebuilding the top of the page.
- Edit is no longer a navigation. The editor mounts in place of the prose, on the page you are already on, so `⌘E` from halfway down a page leaves you exactly halfway down it. Building the editor over a long page takes a moment, and for that moment the page you were reading is what stays on screen - the two are laid over each other and trade places in one frame, so there is no blank, no placeholder, and nothing to jump back to the top.
- Selecting prose no longer opens a comment box on top of your selection. A small bar appears instead, offering **Copy** and **Comment**, and the text stays selected the whole time - so `⌘C` still works, and commenting is a deliberate press rather than the only thing a selection is allowed to mean.
- **Copy** puts the selection on the clipboard twice over: as rich text for anywhere that takes it, and as real Markdown for anywhere that does not. Headings, lists, tables, and code fences all survive the trip. A selection that crosses blocks can still be copied; only commenting needs a single block to anchor to.
## The queue triages, the strip counts hands, the digest writes itself
- The Activity page grew a strip: eight weeks of days, human work rising above a baseline in ink, agent work hanging below in the accent, outside writes worn a shade fainter on the human side - the workspace's balance of authorship at a glance. Under it, the feed takes filters (humans / agents / outside, edits / comments / reviews), by URL, bookmarkable.
- The proposals queue reads like triage now: every row carries the agent's self-review note, the diff's weight (+added/−removed), and its age; the proposal page shows the note beside the diff. The cheapest trust signal an agent can send, finally shown where the decision happens.
- `hanji digest ` writes "what changed, what awaits you" as a Markdown page into the workspace - web-readable, agent-readable, carried by git like everything else, committed by Hanji under its own name. Each digest covers since the last one; a quiet window with a clear queue writes nothing at all, because the system must never notify about its own notifications. Cron it and the artifact is your morning briefing - no mail server, no push service, no channel to own.
## Agents can read comments, if you let them
- A new capability joins the token grammar: `read+comments` (stackable, so `read+propose+comments` works too). Grant it, and `hanji_list_comments` and the `GET /comments` alias hand back a page's threads - quote, author, body, resolved state, one level of replies - shaped for an agent to read, not to write. Nothing about a comment's DOM anchor internals leaks out.
- Comments stay opt-in and page-scoped: a token still needs ordinary read access to the page itself, and the capability only reaches as far as the grant that carries it. Without `+comments`, a token gets back the same "Not found" a missing page would - the opacity people already get from a locked page holds for agents and comments too. See [[Comments]] and [[Agents]].
## The version catches up
- Hanji sat at 0.1 through forty-odd arcs of work; it reads 0.5 now, closer to honest. The number keeps one home - the repository's own version - and the reading surface and the agent surface both take it from there, so what the sidebar shows can never drift from what shipped.
## The comment surface settles
- The way into a page's annotations is a comment icon in the page header now, in one row with history, share, and edit, rather than a pill floating over the prose.
- A thread reads as one unit: each on its own soft card, the author on a small accent label, the quiet actions - edit, resolve, delete - held back until you hover, and a reply tucked under its parent behind a hairline.
- The quote at the head of an anchored thread wears the very highlight its phrase wears in the page, so the panel and the prose show one mark, not two treatments of it.
## The comment box becomes a small editor
- The comment composer is a real WYSIWYG surface now, the same editing engine as the page itself in miniature: **⌘B** and **⌘I** toggle bold and italic, a line starting with `-` or `1.` still becomes a list as you type, and `:name:` completes to an emoji through the same menu the page editor uses. Enter still sends and Shift+Enter still starts a new line - the composer keeps a visible **Comment**/**Reply**/**Save** button too, so a mouse or a touch screen works exactly as well as the keyboard.
- You can edit your own comment: an Edit link swaps it back into the composer, pre-filled with its current text, and Save or Cancel take Comment's place. An edited comment carries a quiet "edited" mark next to its time, so a reader can tell a note changed after it was first written. The workspace owner can edit any comment, the same reach they already had to resolve or delete one.
## Comments read like comments
- Comment bodies support light formatting: **bold**, *italic*, and `-`/`1.` lists render for real now, instead of every note flattening to one plain paragraph. It is a small, safe renderer built as React elements, not raw HTML - there is no way a comment's body becomes markup another reader's browser executes.
- Enter sends a comment, Shift+Enter starts a new line - reaching for the mouse to click **Comment** is optional now, not the only way.
- The composer that opens on a fresh text selection focuses itself: selecting a phrase is already the intent to write about it.
- A comment's time reads like the rest of the app: "3 hours ago" rather than a bare `2026-08-25`, with the exact moment on hover.
## A place to talk about a page
- A sidebar for the marks in a page: click a highlighted phrase and the note it belongs to opens in a panel beside the prose, scrolled to and lit. Browse every annotation on the page there, filter by open or resolved, reply or resolve without leaving your place in the text.
- Select a word or sentence to comment on exactly that, not just the page: the quote gets a shared highlight in the prose for every reader. Edit the underlying text and the note detaches to "On an earlier version" at the foot of the page, quote intact, rather than pointing at words that no longer exist.
- Comments: a thread at the foot of every page, open to anyone who can read it. Reply one level deep, resolve a thread or delete a comment as its author or the owner. Comments live in Hanji's own store, never in git and never in an export - move a page and its comments follow, delete one and they go with it. See [[Comments]].
## Navigation, quick
- Moving between pages is near-instant now. The page byline was quietly walking git history on every navigation to find who touched the page and when - hundreds of milliseconds on a deep-history mount, regardless of how little history a page had. That reading is now indexed at sync time and read from the index, so a navigation costs only what the page takes to render.
- Back and forward controls sit at the top of every page, with `⌘[` and `⌘]` - a standard way through your recent trail that does not lean on the browser's own chrome.
- Clicking a page shows its shape immediately - a light skeleton where the content will land - instead of holding the previous page still until the next is ready.
## Finding things: sort a section by date or name
- A folder or mount can now sort its own children four ways: **Manual** (your curated `order:`, the default and unchanged), **Newest first**, **Oldest first**, or **A–Z** by title. The control lives on the section's row and appears once it is open; a re-sorted section stays lit so it reads at a glance.
- The date it sorts by is the one that actually means something: a date in the filename if the page carries one (notes are often `YYYY-MM-DD`-named), the page's last-commit date otherwise. So a bulk rename or re-commit that flattens git's own dates does not flatten the order - the filename's date survives it.
- The choice is per section and per person, kept in the browser: it never touches the repository or what anyone else sees. Drag-to-reorder pauses while a section is sorted, because the visible order is no longer the stored one - switch back to Manual to curate.
- Under it: every page's last-commit date is now indexed (one git pass per sync), so date order is instant to read and doesn't walk history on every render. The same date powers the page byline.
## The editor sharpens
- Code blocks take the workspace's own accent hue instead of a fixed slate, so a warm workspace gets a warm block and a green one a green block - a deep panel that reads as part of the theme, in both modes.
- Selecting an image no longer pops the text formatting bubble over it: bold/italic have nothing to act on there, and the image's own controls own that moment.
- The image controls give their chips more room, and left/right alignment show real alignment icons rather than the empty rectangles the old glyphs left where a font had no character.
- The slash menu gained **Code** - a code block was insertable by typing ``` but had no entry in the menu until now.
- The page's YAML frontmatter no longer shows as a raw "source" card while editing: it is metadata, not body content, so it stays in the file (round-tripped untouched) but out of the editor's way. Raw HTML and other unsupported blocks still show their source card, since there is nowhere else for them to live. Frontmatter is guarded, too - clear the whole page and it survives, because losing a title or an order to a stray select-all is not an edit anyone means to make.
## Small breaths
- A page's header opens up: more air between the breadcrumb and the title, and between the title and its byline, so the top of every page reads calmer.
- Images sit a touch inside the measure now instead of edge to edge, and take a wider gap above them than a paragraph does - so a figure reads as placed, not pasted.
## The page you are looking at is the page that exists
- The screen notices now. Coming back to the tab syncs your local mounts and refreshes the open page in place when something changed - client state survives, one quiet line says it happened. A sync that finds nothing costs milliseconds: Hanji stats the tree and stops.
- The byline wears a freshness line: "checked just now / 4m ago / syncing… / sync failed - retry" - when the index last agreed with the files, deliberately a different word than the page's own "updated". The mechanisms deliver freshness; the line delivers confidence in it. The toast names the mounts that moved and wears inverted paper: ink surface on light, paper on dark - ephemeral UI earns its six seconds by contrast.
- An open editor is never refreshed out from under you. It warns early - "This page changed while you were editing" appears before Update is pressed - and the save itself re-reads the disk at the last moment, so an outside edit cannot be overwritten unseen. Plain folders get the same guard, where no git history could bring an overwritten change back.
- Agents stopped reading stale: every MCP and curl read freshens local mounts first (throttled, cheap by construction).
- The Activity page: what moved across your mounts, straight from the history - edits, comments, and reviews, grouped by day, actors labeled honestly (an outside commit is never dressed as an in-app one). A "seen up to here" line rides a per-reader cursor; nothing anywhere is marked read by being seen.
- `hanji_changes` and `GET /changes?since=…`: an agent's first question at session start - what moved since I last read? - answered from the same authz-filtered feed. `GET /feed.atom` is the Atom projection; a token rides the URL, and no token means the everyone regime, exactly like a published site.
- Proposals carry the agent's self-review note (the commit body, shown with the diff), and Reject finally leaves a trace: who refused what and when, with the proposal's tip kept under a ref instead of drifting toward garbage collection.
## Export speaks Open Knowledge Format
- `hanji export --okf` shapes the Markdown mirror into an [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) bundle: every concept doc gets a `type` in its frontmatter (yours wins if you set one), the reserved `index.md` and `log.md` stay frontmatter-free, and the bundle root declares `okf_version`. The static site and `llms.txt` ride along unchanged, and the source in git is never touched, the shaping lives only in the copy on disk. Mounting an OKF bundle already worked, because a bundle is just Markdown on git.
## The lockup opens, the footer retires
- The workspace name is a door now: Settings and Log out lead, Documentation and Report an issue follow as real rows, and the project meta fades below - the version beside a What's new link to this page, then Contribute and Support, quietest last. The mark alone stays the one-click way home. Members see "Signed in as" where owners see Settings.
- The sidebar's bottom chrome is gone entirely - no action row, no links row, no version line, no closing hairline. The page tree gets the whole rail.
- Page history moved up beside Delete and Edit as a quiet bordered chip - and it shows for every reader now, not just principals who could edit. The byline keeps saying who and when; ~~Browse page history~~ as a trailing text link made history look like metadata instead of an action.
- The sidebar's search trigger grew into a proper field - the rail's search matters more than its old whisper suggested.
- With the footer gone and the header calm, the rail's last rule retired too: the list slides beneath the search bar edge to edge, and the gradient alone says there is more.
- The byline learned the whole paper trail: **Created by** whom and when, **updated** by whom and when - the Confluence answer, one quiet line. The creation is the page's actual first commit, followed through renames and past the history list's cap; a repeated name is not repeated.
- Appearance's reading controls merged into one room, **Reading experience**: font, width, and text size sit over the same live specimen, because that is what they always previewed on.
## The search sheds its lines
- The palette arrives instead of appearing: the scrim leads, the card settles with a quiet rise on the same curves the sheets ride. Dismissal stays instant - a keyboard surface leaves at keyboard speed.
- The palette's strokes retired - no frame, no rule under the box, no line above the footer. One calm card held by shadow and tone; the query reads in serif at reading size, and the active row is a soft inset tint instead of an edge-to-edge bar.
- The sidebar's search trigger: ~~a tonal pill with no stroke~~ - stroke-less read as unfinished at field size, so it keeps its discreet hairline; the ⌘K chip is a tint rather than a box.
- The full results page's field became the app's one emphasized field - taller, serif, full-round - because search is that page's whole job. One height everywhere else still stands; this is the deliberate exception.
- Settings tabs turned into pilled labels, the active room a quiet accent tint; the underline bar and the divider above Save both retired - space does those jobs.
- The rule between a page's header and its prose retired too, on the reading view and the history list alike: the byline's small sans already marks the boundary, and the prose opens after honest air.
## Settings, in four calm rooms
- Appearance itself found its order: **Mode**, **Colors** (light and dark palettes as quiet labeled rows), **Reading font**, and **Reading surface** - width and text size together at last, sharing one live specimen instead of each section carrying its own.
- The dense Customization tab split in two: **General** (workspace, working mode, sidebar) and **Appearance** (mode, colors, reading surface), beside People and Mount access. Each room saves itself; the divider between the page header and the tabs retired - space does that job.
- The color controls went on a diet: circular swatches, and one quiet line naming only what the contrast pass actually adjusted. ~~A per-mode card restating every color that was "kept as given"~~ - the page itself is the live preview, and a card full of nothing-happened rows was noise.
## The sidebar finds its depth
- The sidebar sits on its own surface now: a breath darker than the page in both modes, derived from the same background seed - panels told apart by tone, with the hairline kept only at the scroll boundary.
- The workspace lockup calmed down: one steady size at medium weight beside the mark, both on the same 24px line, so the pair centers optically at any name length. ~~Two sizes stepped by name length~~ - the step was why long names never quite aligned.
- The page's delete button joined its neighbors: the same bordered chip family as Share, with a danger tint drawn from one token that reads in both modes. ~~hover:bg-red-50~~ was a light-mode assumption glowing in the dark.
## Night gets a personality, fields get a hand
- Every workspace's dark background was the same cool slate, no matter what its light background said - the derivation used a constant. Auto-dark now keeps the light background's own hue at a quiet fraction of its chroma, anchored deep: a warm workspace has a warm night, a green one a green night. Detached dark seeds are untouched.
- Fields and dropdowns wear the full-round pill now, one silhouette and one height across the app - a small Korean hand in the chrome. Selects drop the native arrow for a quiet chevron with honest room on the right.
- Fewer hairlines, more air: the settings sections separate by space and their serif headings, table rows drop their per-row rules for a soft ink-tint hover, and dividers remain only where they mark a real boundary - the sidebar's scroll limits, ~~the tab bar, the action row~~.
## The details pass
- Search snippets read as prose now: frontmatter fences, heading marks, wikilink brackets, table pipes, and backticks are quieted before display - in the palette, on the results page, and in what agents receive.
- Closing a sheet no longer leaves an invisible wall behind: the scrim stops catching clicks the moment the exit animation starts, so the next click lands where you aimed it.
- Redirects after sign-in and settings saves stay on the exact host you are on. Rebuilding them from the request could respell 127.0.0.1 as localhost, and the session cookie does not follow a respelled host.
- Smaller courtesies: people rows wear their initial on a small paper seal, the logo uploader lost the browser's "No file chosen" caption, the editor's source card holds its label inside itself in both modes, the emoji button became an outline glyph in the toolbar's own ink, a page with no history yet stops holding an empty seat where the byline would sit, and keyboard focus shows one quiet accent ring everywhere.
## curl counts as an agent
- The MCP port grew plain-HTTP aliases for all four tools: `GET /pages`, `GET /page`, `GET /search`, `POST /propose` - same bearer token, same principal, same permission check, no JSON-RPC envelope and no stream framing to parse. A cron job with curl and a token is now a first-class agent; an unknown path answers with a map of the surface.
## Ask tailscaled itself
- Tailscale mode's trust rested on a deployment shape: the port is only reachable through `tailscale serve`, so the identity header is honest. `HANJI_TAILSCALE_WHOIS=1` makes the instance verify instead of assume - every header sign-in is checked against the local tailscaled's own answer for the connecting address, one cached lookup per address per minute, mismatch means the login page. Off by default; the shape contract stands unchanged without it.
## The reading surface, your size
- Settings grew a content text size slider: 17 to 25 pixels, previewed live on the page as you drag, saved with the rest of appearance. Every size inside the prose - headings, tables, code, captions - now derives from the one base, so the whole surface scales together instead of the body drifting under fixed headings.
- The slider previews on a sample paragraph right under it - the same sentence the reading-font picker sets, so the two previews read as one voice - in your chosen font, at the size under your cursor.
## The 35,000-page sync, in minutes not half-hours
- Big first syncs were superlinear: every page's full-text row was addressed by column equality, which on an FTS5 table is a scan of the whole growing index - by page 35,000, most of the work was rereading what was already written. FTS rows now share their page row's rowid and are addressed by it, index writes run in batched transactions instead of a commit per statement, and a page's frontmatter is parsed once instead of twice. Measured on the same 35,000-page corpus: ~35 minutes before, ~2.5 minutes after. Existing databases rebuild their FTS pairing once on next open, losing nothing.
## One lock, all processes
- The per-mount write lock now holds across processes, not just within one. The web front, the MCP server, and the CLI routinely share a data directory, and a save racing a sync from a different process could wedge the clone. ~~An in-process promise chain~~ The chain remains for queueing within a process, and beneath it SQLite - already in the stack - takes a real file lock per mount that the kernel releases the moment its holder exits, so a crashed process cannot leave a stale lock behind.
## Every mark, handled
- Adapt-to-mode is an alpha mask, and a mark that fills its whole canvas masks into a solid slab - which is exactly what one looked like. The mark's **coverage** is now measured alongside its luminance at upload, adapt is refused for full-bleed marks everywhere they render (with the settings page saying why), and marks stored before measuring get measured on their next settings visit.
- The sidebar and login now follow the same matrix: full-bleed marks and photos render edge to edge instead of floating inside a contrast plate (a photo in a dark chip read as a broken border). The adapt toggle stays visible for a full-bleed SVG - disabled, with the sentence saying why - instead of vanishing. And the settings page's live sidebar preview stopped inheriting its size from whatever element it replaced, which could blow the mark up to the file's intrinsic size and push the workspace name out of the lockup entirely.
## A long sync tells you where it is
- Someone pointed the welcome at thirty-five thousand pages, and the button said "Setting up…" for a quarter of an hour. Syncs now report what they are doing - fetching, then indexing with a live count over a real progress bar, then tidying - to the welcome and to the empty workspace's content doors alike. `syncMount` grew an optional progress callback; a progress endpoint serves it, public exactly while the welcome is, owner-only after.
- The same run found something worse than silence: the indexer held the server's event loop for its whole pass, so a big sync froze every request the instance should have answered - the waiting page eventually crashed on a starved fetch. The indexer now yields every hundred files: the server stays responsive, the progress endpoint actually answers, and the bar above is honest in real time.
## The review we owed ourselves
A fast-shipped arc earned itself a deep review - every finding independently verified, then fixed.
- A security hole closed: while a freshly created instance sat unconfigured, any website in any browser could have configured it with a drive-by request - and pointed it at a git URL of its choosing. Cross-site requests to the welcome are now refused.
- Small honesties: forms no longer strand their buttons when the connection drops mid-request, and an empty front page now tells "no mounts yet" apart from "mounts, but no pages yet".
## Content without a terminal
- An empty workspace now offers its content doors right on the front page: mount a folder of Markdown, or clone a repository - owner only, same validation as the welcome, through a new mounts API. ~~"Start empty" used to dead-end into a CLI snippet~~; the wire remains for those who like it.
## Sessions, untangled
- Two instances on one host were signing each other out: cookies scope by host and ignore the port, so every login overwrote the other instance's session under the same name - two dev servers side by side did it in any browser. The session cookie now folds the port into its name, and each instance keeps its own. Existing sessions get one last "please sign in"; after that it holds.
- The login form finally answers when pressed: the button reads "Signing in…" and refuses double submits.
## The secret color, corrected
- The default accent is now the celadon-ink the landing page wears: hue 196, the blue-green 비색 has always named - `#1e8485` on paper, deeper `#007374` for text. The old default leaned green; published sites pick the change up on their next export. Workspaces with their own palette keep it - this only moves the default.
## Solo and team, told apart
- A working-mode switch in Settings: **Solo** keeps the chrome away - no Share buttons, no People or Mount access tabs - because an owner alone has nobody to share with. **Team** brings it all back. A fresh instance starts solo; one that already has people or rules counts as team on its own, so nothing existing loses its buttons.
## The door for contributors
- Published pages can end with a quiet **suggest an edit** link into your repository's web editor (`export_edit_base_url`) - a typo becomes a pull request in two clicks. This site turns it on the day the code is public.
- A `set` command joined the CLI, so the export settings are one line each instead of a database visit.
- [[Contributing]] now says the quiet part plainly: agent-assisted contributions are welcome - a human signs and answers, the gates judge the code.
## The first run lost its terminal
- A brand-new instance now greets you with a welcome screen: name the place, choose the owner's password, point it at a folder of Markdown or a git URL - and land signed in, on your pages. No environment variables, no CLI, nothing to wire by hand.
- One shot, by design: the moment an owner exists, the welcome endpoint is gone. A configured instance cannot be taken over through it, and a failed attempt never leaves you configured-but-broken - the password is stored last, after everything fallible succeeded.
- Env still wins everywhere, so server deployments keep their exact shape. The session secret heals itself too: absent from the env, one is generated once and kept.
## The front door serves you first
- The handbook's home now leads with what every reader actually came for: a five-minute [[Quick start]], then a door for each shape Hanji is lived in - solo, tailnet, agents, publishing. The manifesto still matters; it just stops standing in front of the person mid-task.
- Two guides joined: [[Quick start]] and [[Publish to the web]] - the second one describing exactly how the site you are reading came to exist.
- The published site grew a masthead cover built from the workspace's own landing page, and an optional way home to the site that sent you (`export_home_url`).
## The handbook publishes itself
- `hanji export` grew its browsable half: a static site in the hanji look - warm paper, the serif, the sidebar tree - with not one line of JavaScript. Folders collapse with ``, dark mode rides the system preference, and every link is relative, so the site serves from any path.
- The rendering pipeline moved into its own package, shared verbatim between the web front and the exporter. There is exactly one way Hanji turns Markdown into HTML, and the sanitize rules travel with it.
- Only assets referenced by exported pages ride along: an image on a restricted page stays as dark as the page.
- A `rule` command joined the CLI, so a fresh instance can open content to everyone without touching the front - the piece a publish pipeline needs.
- And this is not hypothetical: the page you are reading is served by that exporter.
## Changes arrive on their own
- Push-to-sync: set one secret, add one webhook to the repository, and a push syncs the matching mount seconds later. Deliveries are signature-verified and answered immediately - the git work happens behind the response, serialized by the same mount lock as every other write. ~~A GitHub App~~ A plain signed webhook: the App, with its registration and installation machinery, was the roadmap's word and would have bought auto-configuration at the price of tying v1 to GitHub. The endpoint speaks to anything that can sign a POST.
- The poll loop: `HANJI_POLL_SECONDS` syncs everything on an interval, because a tailnet has no inbound path for webhooks and polling stays first-class. It also covers local mounts, so edits made outside Hanji show up without asking.
## The export honors the lock, because it is the lock
- `hanji export ` mirrors the workspace as `llms.txt`, `llms-full.txt`, and a per-page `.md` tree - the shape agents on the open web actually read. What lands on disk is exactly what a person with an account and no grants could see: the everyone regime, computed by the same authz predicate as every read in the product. No flag widens an export; mount names only narrow one.
- The exporter refuses a directory it didn't write, refuses an empty everyone-set, and re-exports clean over its own output.
## Agents get the same lock people have
- Token scopes grew a path: `hanji token intern notes/reviews=read` grants one folder, `notes/reviews/draft.md=read+propose` exactly one page. Tokens and people now share a single allow-region mechanism in the one authz predicate; the only asymmetry left is deliberate - "everyone" rules speak to people, never to tokens.
- Old mount-level scopes migrate themselves on the next start; nothing to run.
## The tailnet signs you in
- Tailscale mode: run the instance behind `tailscale serve`, set one variable, and the tailnet's identity signs people in - the owner by env match, everyone else through a login linked on their person sheet. No password typed, no session ceremony. See [[On your tailnet]].
- The mapping answers who, never what: grants stay exactly what the owner assigned, agents stay bearer-token only, and an unlinked tailnet visitor gets a login page that says who it saw and why they are not in yet.
- Trust is opt-in and explicit: the headers are only honored when `HANJI_TAILSCALE_OWNER` is set, and the docs state the deployment contract that makes them unforgeable.
## The lock reaches a single page
- Visibility became content-first, the way every knowledge tool taught people to think: general access (everyone reads, everyone suggests, or restricted) set on a mount, folder, or page, inherited downward, deepest rule wins. People are the exceptions that punch through. Agents never inherit "everyone" - tokens see exactly their scopes.
- Grants grew a path: a person can hold a mount, a folder, or exactly one page. The 1:1 note two people share is invisible to everyone else - tree, search, links, all of it - and a locked page still looks identical to one that does not exist.
- Sharing moved onto the page: a Share button, a name, read or suggest, done. Revoking lands on the person's next request.
- People can now be added bare, with no access at all, and given pages one Share at a time.
## The sidebar becomes a place you organize
- A second pair of hands: the owner adds people in Settings with a name, a password, and per-mount scopes. Viewers read; contributors propose through the same review card as agents. Owner powers stay the owner's.
- Suggest-only mounts opened to people: Update becomes Propose in the editor, and the proposal waits in the same review card an agent's would. One ceremony for every writer.
- A + at the end of every mount, folder, and page row creates a new page in that context. The + on a page nests: the page becomes its section's landing and the new page starts inside it.
- Pages and folders drag: onto a folder or a mount to move there, onto a row's edge to reorder among siblings, onto a page's middle to nest into it. A folder brings its whole subtree, and mounts drag to reorder the rail itself.
- Drops land instantly. The tree moves first, the git work follows, and a failure snaps back with a toast that says why. Reordering is one commit writing `order:` frontmatter; a move is a real `git mv`, so history follows the page.
- Pages can be deleted: a quiet trash next to Edit, armed by a second click, backed by `git rm`. Nothing is truly lost; the repository keeps every sheet.
## Search grew into a real feature
- One backend upgrade: bm25 ranking with a heavy title boost, snippets that mark their hits, prefix matching on the word you are still typing, and multi-word queries ranked by how many of your words a page shares, adjacent phrases first.
- Two surfaces on it: a `⌘K` palette (ranked results, recent pages when empty, keyboard all the way) and a bookmarkable `/kb/search` page with highlighted passages and per-mount filters.
- Agents search through the same ranking, via `hanji_search`.
## The workspace becomes yours
- A workspace name and logo. An SVG adapts its color to light and dark; any other mark gets a mode-safe plate when contrast demands one.
- Colors from three seeds, reinterpreted per mode in OKLCH with readability secured for you, plus curated reading fonts. Your seeds are never altered, only reinterpreted, and the settings page shows which is which.
- Emoji in the editor, three ways: a toolbar picker, `:query` suggestions at the caret, and GitHub-style `:tada:` conversion on the closing colon.
- Proposals left the content list. A card appears under the search box only while something waits, and disappears when the queue is empty.
- Versioning starts at 0.1.0, printed quietly at the bottom of the sidebar.
- A real phone experience: a top bar, a drawer, the same sidebar.
## The agent git flow
- The handbook itself now takes proposals: its mount points at the real repository, so agent contributions to these pages arrive as branches and get reviewed right here.
- Proposals now land in the front: a pending badge in the sidebar, a line diff, and Merge or Reject. Merging is git all the way down: a merge commit on base, the branch deleted, the page and index updated. See [[Proposals]].
- A proposal that no longer merges cleanly opens with a warning and a disabled Merge. No conflict screen, by design: re-propose or reject.
- Along the way the handbook itself found and drove product fixes: sidebar ordering by frontmatter `order`, SVG assets served safely, image width, alignment and captions, local path mounts, a boot smoke test, and an editor that loads only when you edit.
## The editor, and everything around it
- Editable GFM tables, with Notion-style controls: hover handles, boundary insert, drag to move.
- GFM parity in the editor: interactive task-list checkboxes, strikethrough, bare-URL autolinks.
- A collapsible nested sidebar built from page paths, plus a middle-collapsing breadcrumb.
- The editor grew from ~~a block-scoped textarea~~ into a full WYSIWYG surface, with dirty-block byte-fidelity intact.
- A slash menu to insert, image upload into the repo's own `assets/`, and domain-allowlisted embeds.
## v0.1, the foundation
- The byte-fidelity serializer, its corpus gate green on 600-plus real Markdown files.
- The git-backed core: mounts, sync, a SQLite and FTS5 index, permissions on a single read path.
- The MCP agent surface: scoped read tools and propose-as-PR, one bearer token per agent.
- The first web front. Reading only at first, then editing once the craft was locked.
## Decisions we walked back
- Non-Markdown blocks: ~~custom directives~~ plain HTML blocks, so the files stay portable and you can always walk away.
- The editor engine: ~~Tiptap~~ ProseMirror with prosemirror-markdown. We chose byte-fidelity over the faster start.
- The name: ~~kb-rail~~ Hanji. A codename is a placeholder. This one is a promise.
See [[Where we are]] for what is shipping now.
---
---
title: Conventions
order: 20
description: How the code and the commits are kept.
---
# Conventions
Nothing exotic here. The conventions exist so the codebase stays boring, and boring is the goal.
## Code
- **Strict TypeScript**, everywhere, with no exceptions carved out to dodge a type.
- **Small modules with one job.** The parts of Hanji have clear seams on purpose. A file that grows into two responsibilities gets split.
- **The security-critical paths are single-sourced.** Permissions live in one module. Byte-fidelity lives in one engine. You do not add a second way to do either, because a second way is a second thing to get wrong.
- **No new dependency for what a few lines can do**, and never a dependency that pulls in a model or a meter.
## Commits
- **DCO sign-off** on every commit, `git commit -s`.
- **Clean, human history.** A commit message says what changed and why, in a person's words. No machine trailers, no noise. The point of byte-fidelity is a readable history, and the commits are held to the same bar.
- **One change per commit.** A one-word fix is a one-line diff, and the commit that carries it should be just as legible.
## Documentation
- **Docs ship with the feature.** A behavior change and its handbook pages land in the same series: the changelog says what changed, [[Where we are]] stays true, and the affected guides get swept for claims the change made false. This handbook is the product documenting itself, so a stale page is a product bug.
- **A mechanical floor is enforced by the test suite**: every environment variable the product reads is documented, every CLI command is in the [[Command reference]], every guide page is in the reading order. When that test fails, the docs are wrong, not the test.
## Reviews
Changes get reviewed, and the reviews are adversarial where they need to be, on the permission and byte-fidelity paths especially. A finding on those paths is not a nit. It is the product. Read [[Open core]] for the license this all ships under.
---
---
title: Developing
order: 10
description: Running the project, and the tests that guard it.
---
# Developing
The project is a pnpm workspace: four packages and one web app, strict TypeScript throughout. Here is how to run it and how to keep it honest.
## Get set up
- [x] Clone the repo
- [ ] `pnpm install` (this builds the native SQLite driver, so you need a C toolchain)
- [ ] Set the environment from [[Install Hanji]]
- [ ] `pnpm dev:web` and `pnpm dev:mcp` to run the two surfaces
- [ ] `pnpm typecheck` and `pnpm test` before you commit
## The tests that matter
Most of the suite is ordinary unit tests, a little over 200 of them, run with `pnpm test`. Two guards are worth calling out.
**The corpus gate.** The serializer's promise is guarded by round-tripping real Markdown files and asserting byte-identity. It is off by default and turns on with `KB_CORPUS_DIR`. It must stay green, and it never gets weakened to make a change pass.
```bash
KB_CORPUS_DIR=/path/to/markdown pnpm vitest run packages/serializer/test/corpus.test.ts
```
**The permission invariant.** Every read flows through one module, and the tests probe it with hostile inputs to make sure a scope can never leak. If you touch reads, you keep them on that one path. Read [[The core]].
## A known gap
The type checker and the unit suite never open the database through the Next app, so a class of runtime binding bug can ship green. A boot smoke test is on the [[Where we are]]. Until it lands, run the front and load a page before you trust a change to the web build.
Read [[Conventions]] for how the code is kept.
---
---
title: Contributing
order: 40
description: How to get involved, and the state of contributions today.
---
# Contributing
The honest state first. Hanji is pre-release, and it is not accepting external contributions yet. A lightweight contributor agreement will be in place before that opens. So this section is a standing invitation and a map, not an open door quite yet.
## When it opens
Here is what contributing will look like, and what already holds today for anyone building on it.
- Every commit needs a DCO sign-off. Use `git commit -s`. It is the Developer Certificate of Origin, a one-line statement that you wrote the code and can license it.
- The history stays clean and human. Commits read like a person wrote them, because a person did.
- Read [[Developing]] for how to run the project and the tests, [[Conventions]] for how the code is kept, and [[Open core]] for the license and what stays free.
## Bringing your agent
You will likely write your contribution with a coding agent at your side - we do, and this product exists because of that way of working. So the policy is explicit rather than awkward: **agent-assisted contributions are welcome.** A human signs the DCO and answers for the change; the gates - the tests, the corpus round-trip, the leak suite - judge the code without asking who typed it. What we ask is the same thing we ask of any contributor: understand the change you are proposing, keep the history human, and never let an agent negotiate a review for you.
## The spirit
Hanji has a small number of beliefs, listed in [[Principles]], and the code is meant to hold them. A contribution that makes the product faster, clearer, or more honest about its limits is welcome. One that bundles a model, meters a feature, or quietly rewrites the user's files is not, however clever it is. The tie-breakers are the principles, for the code as much as the product.
---
---
title: Open core
order: 30
description: The license, the open-core model, and the promise about what stays free.
---
# Open core
Hanji is open core, and the model is chosen deliberately, because the license is a promise you cannot take back later without burning the people who trusted it.
## The license today
The whole project is **AGPL-3.0-only**. That is a strong copyleft license. You can run it, read it, change it, and self-host it freely, and if you offer it as a service, your changes stay open too. It is the terminal license, picked on purpose. There is no plan to relicense, because relicensing is a permanent tax on the trust of everyone who showed up early.
## The seam
Over time, a set of features for administrators will live behind a license key, the way the open-core projects of this era do it. The rule that decides which side of the seam a feature lands on is fixed.
| Free, forever | Paid |
|---|---|
| The complete single-team knowledge base | Single sign-on |
| The editor, history, search | Per-folder granular permissions |
| Bring-your-own-agent, the MCP surface | Audit and enforcement |
| Import and export, the API | Priority support |
The line is simple: never gate what an individual needs to write. A student, a solo maintainer, a small team gets the whole writing experience for nothing, forever. The paid tier is for the things an administrator wants when the team gets big, and only those.
## Where it is today
The paid seam is a plan, not yet code. What ships now is the AGPL core. When the license key and the administrator features arrive, they arrive behind the seam described above, and this page will stop describing a plan and start describing a product. Read [[Mission and vision]] for the why, and the [[Where we are]] for the when.
---
---
title: Agents
order: 40
description: Connecting a coding agent through the MCP server.
---
# Agents
Agents get their own surface. Two of them, really: raw git, which they already know, and a small MCP server for the times a scoped, hosted read helps. Both go through the same permission check a person does.
## Mint a token
An agent authenticates with a bearer token, scoped to what it may touch - a
whole mount, a folder, or exactly one page, the same path grants people have.
```bash
pnpm hanji token ...
```
```bash
# a read-only agent for the handbook
pnpm hanji token docs-reader handbook=read
# an agent that can also propose changes to two mounts
pnpm hanji token my-claude handbook=read+propose notes=read+propose
# a narrow one: read one folder, propose to a single page
pnpm hanji token intern notes/reviews=read notes/reviews/draft.md=read+propose
# an agent that may also read a page's comments
pnpm hanji token reviewer notes=read+comments
```
The command prints the token once. A `read` scope lets the agent search and read within its grant. A `read+propose` scope adds the right to propose changes there. A `read+comments` scope adds the right to read that page's comments too - stack all three as `read+propose+comments` if the agent needs the full run. There is no `write` scope for a token: editing to base is for people, and an agent always proposes. Anything outside its grants the agent cannot see, and cannot even tell exists - a locked page and a missing page answer identically. Content visibility rules never widen a token: an "everyone" rule speaks to people, and an agent holds exactly what it was granted. See [[Permissions]].
Where a propose lands: a title, a diff, and your decision.
## Connect it
The MCP server speaks Streamable HTTP on `127.0.0.1:4101`. Point your agent at it with the token.
```bash
claude mcp add hanji --transport http http://localhost:4101/mcp \
--header "Authorization: Bearer "
```
## The tools
| Tool | What it does | Needs |
|---|---|---|
| `hanji_list_pages` | List every page the token can read, with mount, path, and title | `read` |
| `hanji_get_page` | Fetch one page's Markdown by mount and path | `read` |
| `hanji_search` | Full-text search across readable mounts | `read` |
| `hanji_propose` | Propose a page change as a branch, and a PR where GitHub is connected | `read+propose` |
| `hanji_list_comments` | Read one page's comments (threads, quotes, replies) by mount and path | `read+comments` |
| `hanji_changes` | What changed since a moment: commits with authors and pages, edits not yet committed, and comments | `read` |
## No MCP client? curl works
Every tool is also a plain HTTP endpoint on the same port, same token, no
JSON-RPC envelope and no streaming to parse. For scripts, cron jobs, and
agents that can run a shell but not an MCP client:
```bash
curl -H "Authorization: Bearer " "http://localhost:4101/pages"
curl -H "Authorization: Bearer " "http://localhost:4101/page?mount=handbook&path=install.md"
curl -H "Authorization: Bearer " "http://localhost:4101/search?q=permissions&limit=5"
curl -H "Authorization: Bearer " "http://localhost:4101/comments?mount=notes&path=idea.md"
curl -H "Authorization: Bearer " "http://localhost:4101/changes?since=2026-08-27T00:00:00Z"
curl -H "Authorization: Bearer " "http://localhost:4101/propose" \
-d '{"mount":"notes","path":"ideas/from-cron.md","title":"An idea","content":"# It\n","note":"Confident on wording; check the anchor link."}'
```
`/pages`, `/search`, and `/comments` answer JSON, `/page` answers raw
Markdown, `/propose` answers the branch (and PR URL where GitHub is
connected). The permission check is the same one the MCP tools run; an
unknown path answers with a map of the surface.
## Propose, do not overwrite
## Start the session with what changed
`hanji_changes` (and `GET /changes?since=…`) answers the question a returning agent should ask first: what moved since I last read? Commits with their authors and pages, a local mount's edits not yet committed (one row per mount, timed by the newest file), and new comments, across exactly the mounts the token can read. Items authored by Hanji itself (the digest's own commits) carry the actor class `hanji` - housekeeping labeled as itself, so an agent can skip it when briefing. The intended habit: ask at session start, then brief your human in two lines instead of making them scroll. Reads freshen local mounts first (throttled to about once per ten seconds), so the answer is at most a few seconds behind the files - and there is an Atom projection of the same feed at `/feed.atom?token=` for feed readers and bots (no token serves only what is open to everyone, the same rule a published site follows).
## Propose, and say what you are unsure about
A proposal accepts an optional `note`: the agent's own self-review, riding the commit body under the title. Say what you are confident about and what you are not - it shows on the review card and makes the human's yes faster. An honest "unsure whether the old anchor still resolves" is worth more than a confident silence.
`hanji_propose` never edits a page in place. It creates a branch named for the change, commits the new content there, and pushes it. If `HANJI_GITHUB_TOKEN` is set, it also opens a pull request and hands back the URL. A person reviews it and merges. That is the whole point. An agent can suggest all day, and nothing lands in your knowledge base until someone says yes.
Where does the suggestion go? Straight into the front: see [[Proposals]] for the review-and-merge side of this loop.
Read [[Command reference]] for the rest of the CLI.
---
---
title: Command reference
order: 50
description: Every Hanji CLI command, in one table.
---
# Command reference
The whole CLI, run as `pnpm hanji `. It reads `HANJI_DATA_DIR` for where to work, defaulting to `~/.hanji`.
| Command | What it does |
|---|---|
| `add-mount [subpath] [--suggest]` | Add a repository, or a folder of one, as a mount. `--suggest` makes it proposal-only. |
| `remove-mount ` | Remove a mount and its indexed pages. The clone directory stays on disk. |
| `sync` | Fetch every mount from its remote and reindex what changed. |
| `rebuild` | Discard the index and rebuild it from the local clones. Always safe. |
| `token ...` | Mint a bearer token for an agent, scoped to a mount, a folder, or one page - add `+comments` to also grant that scope's page comments. Prints it once. |
| `list` | List every mount and how many pages it holds. |
| `export [mount...] [--okf]` | Publish everything open to everyone: a browsable static site, `llms.txt`, `llms-full.txt`, and every page as plain Markdown. Mount names narrow the set; nothing widens it. `--okf` also shapes the Markdown mirror into an [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) bundle, described in [[Publish to the web]]. |
| `rule [path] ` | Set or clear a general-access rule from the command line - what Share's select does in the front. |
| `set ` | Write an instance setting - the workspace name, or the export settings [[Publish to the web]] describes. |
| `assets [mount]` | List uploaded images no page references anymore, per mount or everywhere. |
| `digest [path] [--since ]` | Write "what changed, what awaits you" as a Markdown page into the mount (default `digest.md`), committed by Hanji itself. Covers since the previous digest; a quiet window with an empty review queue writes nothing. |
One caution on `digest`: the page is written with the owner's eyes and summarizes the whole workspace, so it belongs in a mount whose readers may see all of that. In a mixed-audience workspace, point it at a restricted mount.
## Examples
```bash
pnpm hanji add-mount handbook git@github.com:you/product.git handbook docs
pnpm hanji sync
pnpm hanji token my-claude handbook=read+propose
pnpm hanji list
# the digest as a morning ritual: cron writes the page, you and your
# agents read it like any other page - no mail server involved
# 0 7 * * * cd /path/to/hanji && pnpm hanji digest notes
pnpm hanji digest notes
```
## The environment it reads
| Variable | Used by | For |
|---|---|---|
| `HANJI_DATA_DIR` | everything | Where clones and the index live. Defaults to `~/.hanji`. |
| `HANJI_SESSION_SECRET` | the front | Signs login sessions. Optional - first run generates and stores one; env overrides. |
| `HANJI_OWNER_PASSWORD` | the front | The owner's password. Optional - the welcome screen stores one on first run; env overrides. |
| `HANJI_GITHUB_TOKEN` | propose | Lets `hanji_propose` open real pull requests. |
| `HANJI_GIT_AUTHOR_NAME`, `HANJI_GIT_AUTHOR_EMAIL` | writes | The name and email on the commits Hanji makes. |
| `PORT` | the MCP server | The port it listens on. Defaults to 4101. |
| `HANJI_TAILSCALE_OWNER` | the front | Turns on [[On your tailnet]] and names the owner's tailnet login. |
| `HANJI_WEBHOOK_SECRET` | the front | Turns on push-to-sync at `/api/webhook/git`; deliveries must carry its signature. |
| `HANJI_POLL_SECONDS` | the front | Sync every mount on this interval. The mechanism for tailnets and local mounts. |
For the agent side of this, read [[Agents]]. For what is coming, read [[Where we are]].
---
---
title: Comments
order: 35
description: A place to talk about a page, right on the page.
---
# Comments
Every page ends in a quiet thread. Anyone who can read the page can read what is said there, and add to it too - comments are discussion, not page content, so reading is the only right that gates them.
## What a comment is
A comment sits at the bottom of the page, under its own heading, separate from the prose above it. Write a note - **⌘B** and **⌘I** format as you go, a line starting with `-` or `1.` becomes a list, and `:` opens the same emoji menu the page editor uses - then press **Comment**, or just press Enter (Shift+Enter starts a new line). It appears with your name and when you wrote it. Reply to any comment once - replies are one level deep, so a thread stays a conversation, not a maze. The comment's author, or the workspace owner, can edit it afterward (an edited comment carries a quiet "edited" mark), resolve a thread (it fades but stays readable), or delete a comment outright; everyone else reads.
This is Hanji's version of the sticky note on a shared page: for the aside that is not worth its own edit, the question for a reader before you make a change, the "looks good" that closes a loop. It is not a change to the page and never a substitute for one.
## What a comment is not
Comments never enter git. They live in Hanji's own metadata store, next to mounts, users, and permissions, and they never become a commit, a file, or a line anyone `git blame`s. Export a mount to the static site or `llms.txt`, and comments do not ride along - the export, and the open web it feeds, carry the page alone. (An agent on the live MCP can read them, but only with an explicit grant - see below.)
That also means a comment follows the page's own life in git: move a page and its comments move with it, delete a page and its comments go too. Nothing is ever left pointing at a path that no longer exists.
## Commenting on a word or a sentence
A comment does not have to be about the whole page. Select a run of prose - a word, a sentence, a paragraph - and a small bar appears over it offering **Copy** and **Comment**. Press **Comment** and the composer opens right there, already focused, ready to type. (The bar is deliberate: a selection used to open the composer outright, which took the selection with it, so you could not simply copy the words you had just picked out. Now commenting is one press further on, and the text stays yours until you ask for it.) Post the note, and the selection carries a mark from then on: every reader sees the same quiet wash of the workspace accent over that exact phrase.
An annotation's wash is not the same thing as a [[Reading and writing|highlight]] you apply yourself. The wash is Hanji showing you where a note is anchored, it is always the workspace accent, and it lives in Hanji's own store; a highlight is content, it is one of four colours you chose, and it is written into the file.
A comment needs a single block to anchor to, so a selection spanning two paragraphs offers **Copy** alone.
These anchored notes live in a panel beside the page, not in the thread at its foot. Open it from the comment icon in the page header, or just click a highlighted phrase and the panel opens with that note scrolled to and lit. It lists every annotation on the page, filters by open or resolved, and lets you reply or resolve without leaving your place in the text. Each thread leads with the quote it was written against, wearing the same highlight the phrase wears in the prose, so the note reads clearly even with the page scrolled away.
That link is best-effort, not permanent. Edit the block the quote lives in, and if the quote no longer matches what is there, the note detaches: it moves down into "On an earlier version" at the foot of the panel, still showing the exact words it was written against, but no longer painted into the prose. The note is not lost, only its anchor is - the words it once pointed at have moved on.
Inline highlights use the browser's CSS Custom Highlight API, which needs a fairly current engine (recent Chrome, Edge, or Safari). On an older browser without it, the panel and its quoted text still read fine - only the painted highlight in the prose is missing.
## Agents and comments
Comments are a human side-channel by default. An agent reads them only through an explicit `read+comments` scope on its token (or `read+propose+comments`), and only on pages it can already read - a token without the capability sees nothing, the same as a page that does not exist. See [[Agents]] for the token grammar and `hanji_list_comments`.
## What's here now
Both forms of comment ship: a thread at the foot of the page for the page as a whole, and anchored threads for a specific word or sentence - highlighted right where they were written, and gathered in a panel beside the prose, reached from the page header or by clicking any highlight.
---
---
title: Using Hanji
order: 10
description: Getting started, and the order to read these in.
---
# Using Hanji
This section is the how-to. It is written to be read in order the first time, and dipped into after.
1. [[Quick start]] - five minutes to a running Hanji, and the map of where to go next.
2. [[Install Hanji]] gets it running on your machine in a handful of commands.
3. [[Mounts]] points it at the repositories you want to read and write.
4. [[Reading and writing]] covers the human front: reading, editing, history, and making new pages.
5. [[Comments]] is where you and your readers talk about a page, right on it.
6. [[Agents]] connects a coding agent through the MCP server, with scoped tokens and propose-as-PR.
7. [[Proposals]] is where those agent suggestions land: review the diff, merge or reject.
8. [[Publish to the web]] turns everything you've opened into a website, llms.txt included.
9. [[On your tailnet]] moves the instance onto your tailnet, where the network signs people in and nobody types a password.
10. [[Command reference]] is the full list of CLI commands, for when you already know what you want.
A note before you start. Hanji is early. It runs today on your machine: one owner, the people you add, and the agents you scope - and the recommended way to share it with a team is [[On your tailnet]]. Hosted sync sits in the later column of [[Where we are]], not in your hands yet. What is here works, and this section documents only what works.
---
---
title: Install Hanji
order: 10
description: From clone to a running instance in a handful of commands.
---
# Install Hanji
Hanji runs today on a machine you own - the same way we run it ourselves, daily. The short version: install, `pnpm dev:web`, and the first visit walks you through everything on a welcome screen. This page keeps the explicit path - environment variables and CLI - for servers, scripts, and the people who like to see the wiring. Anything set by env always wins over what the welcome screen stored.
## Before you start
You need a recent Node, `pnpm`, and `git`. One dependency, the SQLite driver, is built from source on install, so you also need a working C toolchain: the Xcode command-line tools on a Mac, or `build-essential` on Linux. That is a known rough edge, and smoothing it out is on the [[Where we are]].
## Get it running
```bash
# 1. install
pnpm install
# 2. point it at a data dir and set the front's secrets
export HANJI_DATA_DIR=~/.hanji
export HANJI_SESSION_SECRET=$(openssl rand -hex 32)
export HANJI_OWNER_PASSWORD=choose-one
# 3. mount a repo you own (name, git url, mount path)
pnpm hanji add-mount notes git@github.com:you/notes.git notes
# 4. clone and index it
pnpm hanji sync
# 5. run the two surfaces
pnpm dev:web # the human front at http://localhost:4100
pnpm dev:mcp # the agent surface at http://localhost:4101/mcp
```
Open `http://localhost:4100`, sign in with the owner password you set, and your notes are there, rendered on warm paper. That is the whole thing running.
The front door. One password for the owner; people you add sign in with their own.
## The three secrets
| Variable | What it is |
|---|---|
| `HANJI_DATA_DIR` | Where Hanji keeps its clones and its index. Everything here is disposable and rebuildable from git. |
| `HANJI_SESSION_SECRET` | The key that signs your login session. Any 32-byte hex string. |
| `HANJI_OWNER_PASSWORD` | The password for the single owner account, the one that reads and writes everything. |
Two more are optional. `HANJI_GITHUB_TOKEN` lets the propose path open real pull requests, and `HANJI_GIT_AUTHOR_NAME` with `HANJI_GIT_AUTHOR_EMAIL` set the name on the commits Hanji makes. See [[Agents]] for the first and [[Command reference]] for the rest.
Next, point Hanji at the repositories you actually want. Read [[Mounts]].
---
---
title: Mounts
order: 20
description: Pointing Hanji at repositories, and keeping them in sync.
---
# Mounts
A **mount** is one git repository, or one folder inside it, brought into Hanji's tree. You can have many. Together they form the single knowledge base that people and agents read.
Two mounts on the rail. Each is a repository; together they read as one collection.
## Adding one
An empty workspace offers this right on its front page: mount a folder or clone a repository, no terminal involved. The welcome screen does the same for your first content. The CLI is the explicit path, and the only one with the full set of knobs:
```bash
pnpm hanji add-mount [subpath] [--suggest]
```
- `name` is how you refer to the mount, and it is the token scope that grants access to it.
- `repoUrl` is any git URL Hanji can clone: an SSH remote, an HTTPS remote, or a local path.
- `mountPath` is where it lives in the tree, the first segment of every page's address.
- `subpath` is optional. Give it to mount only a folder of the repo, so the rest of the repository stays out of the knowledge base.
```bash
# a whole repo
pnpm hanji add-mount notes git@github.com:you/notes.git notes
# only the docs/ folder of a larger repo
pnpm hanji add-mount handbook git@github.com:you/product.git handbook docs
```
An [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) bundle mounts like any other repository, because that is all it is: Markdown concept documents on git, each with a `type` in its frontmatter. Hanji indexes every concept, keeps its frontmatter byte-for-byte, and serves the bundle to people and agents like the rest of your tree. Nothing to configure.
## Keeping in sync
Hanji reads from a local clone and serves from a local index. To pull the latest from a mount's remote and reindex, sync.
```bash
pnpm hanji sync # fetch every mount and reindex what changed
pnpm hanji rebuild # throw the index away and rebuild it from the clones
pnpm hanji list # show every mount and how many pages it holds
```
`rebuild` is safe to run any time. The index is a cache, so nothing you care about lives only there. If it ever looks wrong, rebuild it and it is correct again.
## Changes arrive on their own
Manual sync works, but a knowledge base should not need asking. Two mechanisms, both optional, both feeding the same sync:
**Push-to-sync.** Set `HANJI_WEBHOOK_SECRET` in the web front's environment, then add a webhook to the repository (GitHub: repo → Settings → Webhooks): payload URL `https:///api/webhook/git`, content type JSON, the same secret. Every push to the mount's branch syncs the matching mount seconds later. Hanji answers deliveries immediately and does the git work in the background, so a slow clone never trips the sender's timeout. Deliveries are verified against the secret's signature; without the secret set, the endpoint does not exist.
**The poll loop.** Set `HANJI_POLL_SECONDS=300` and the web front syncs every mount on that interval. This is the mechanism for a tailnet, which a webhook cannot reach from the outside - see [[On your tailnet]] - and it also covers local mounts, picking up edits made outside Hanji entirely. A slow sync skips a beat rather than stacking; a failing one logs and tries again next round.
**The front syncs itself.** The web app freshens local mounts the moment attention arrives: switching back to the tab triggers a sync, and a quiet interval keeps checking while a page is open. A sync that finds nothing costs milliseconds - Hanji stats the tree and stops when nothing moved - so by the time you are looking, the page already shows what your agent just wrote. The byline says so: a small "checked just now" affix, with "sync failed - retry" when a mount cannot be read.
Any mix works: webhooks where the network allows, polling where it doesn't, `pnpm hanji sync` whenever you want it now.
## Direct and suggest mounts
By default a mount is **direct**: editors' edits in the front (the owner's, and anyone granted `read+write`) are written straight to git as commits. Pass `--suggest` and the mount becomes **suggest-only**: direct writes are refused, and every change has to arrive as a proposal instead.
Suggest mounts are for repositories where nothing should change without review, even from the owner. On a suggest mount Update reads Propose for everyone, editors and the owner included, agents alike, and every change waits in the same review card. See [[Proposals]].
Once your mounts are in place, read [[Reading and writing]] to use the front, or [[Agents]] to connect a coding agent.
---
---
title: On your tailnet
order: 55
description: Run Hanji on a tailnet and let the tailnet sign people in.
---
# On your tailnet
The recommended way to share a Hanji instance with a team is not a domain, a
certificate, and a public endpoint. It is a [tailnet](https://tailscale.com):
the instance runs on one box, `tailscale serve` fronts it, and it never
touches the public internet. That sentence is the whole security posture, and
it is a sentence you can say to an administrator with a straight face.
Tailscale mode adds the part that makes it feel finished: the tailnet already
knows who is knocking, so Hanji stops asking.
## Turn it on
One variable, plus the serve command:
```bash
export HANJI_TAILSCALE_OWNER=you@github # your own tailnet login
node_modules/.bin/next start -H 127.0.0.1 -p 4100
tailscale serve --bg 4100
```
Setting `HANJI_TAILSCALE_OWNER` does two things at once. It tells Hanji to
trust the identity headers `tailscale serve` attaches, and it names the
tailnet login that is the owner - you. Open the instance from any device on
your tailnet and you are simply in, as the owner, no password asked.
## Let people in
Everyone else maps through Settings → People. Open a person's sheet and link
their tailnet login in the Tailscale field. From then on, that identity signs
them in with no password - and that is all it does. What they can see and
propose stays exactly the grants you gave them; the tailnet answers who they
are, never what they may read. See [[Permissions]].
One person, one sheet: what they hold, and the tailnet identity that signs them in.
An unlinked visitor is told plainly: the login page names the tailnet
identity it saw and says the owner has not linked it yet. The password form
still works underneath, always - a linked identity is a convenience on top of
sessions, not a replacement for them.
This is also how you invite someone from outside your team: share your node
with them in Tailscale, add them as a person, link their login. Share access,
not code.
## The contract
The headers are trustworthy for exactly one reason: `tailscale serve` strips
any inbound `Tailscale-User-*` headers and sets its own. So the mode is safe
only while the port is reachable exclusively through `tailscale serve` - bind
`127.0.0.1`, as above. If the app is reachable any other way, anyone on that
path can type the header themselves, and `HANJI_TAILSCALE_OWNER` must stay
unset. Hanji will never turn this on by itself.
Two edges worth knowing. Signing out on a tailnet is a shrug: clearing the
session lands you right back in through the header, because the tailnet still
knows you. And agents are untouched by all of this - the MCP surface stays
bearer-token only, and a tailnet identity never grants an agent anything.
## Belt and suspenders: ask tailscaled itself
The contract above is a deployment shape. If you want the instance to verify
it rather than assume it, set one more variable:
```bash
export HANJI_TAILSCALE_WHOIS=1
```
On every header sign-in, Hanji now asks the local tailscaled who owns the
connecting address (`tailscale whois`, one lookup per address per minute) and
refuses the header unless the answers match. A forged header alone no longer
signs anyone in; it would take tailscaled itself vouching for the forger's
address, which the tailnet does not do. The check needs the `tailscale` CLI
on the PATH of the user running Hanji; point `HANJI_TAILSCALE_BIN` at the
binary when it lives elsewhere (on a Mac,
`/Applications/Tailscale.app/Contents/MacOS/Tailscale`). When in doubt, turn
it on: the cost is one process spawn per new visitor address, and the failure
mode is a login page instead of a wrongly trusted header.
## Why this is not SSO
It is better, for the case it covers. On a tailnet you do not integrate an
identity provider; you already have one, and it is the network itself.
Single sign-on for organizations that live behind Okta or Google Workspace
remains where [[Where we are]] puts it: the paid seam, later. This mode is
for the team that wants the writing surface working today, with nobody
typing passwords, on infrastructure they already trust.
---
---
title: Proposals
order: 45
description: Agents propose. You review the diff and merge, right in the front.
---
# Proposals
This is the page where Hanji stops being a wiki and becomes the thing it was built to be. An agent never edits your pages in place. It proposes a change, the proposal lands in the front, and you decide. The whole loop runs over git, and you never have to see that.
## The loop
1. An agent with `read+propose` scope calls `hanji_propose` (see [[Agents]]). Behind the scenes that becomes a `hanji/*` branch on the mount's remote, carrying one commit with the agent's change.
2. A card appears in the sidebar, under the search box: how many proposals are waiting, and **Review**. It exists only while something waits; at zero the sidebar stays pure content. The count is cheap to compute, and it refreshes whenever anything talks to the remote.
3. Open the proposal. You get the title the agent gave it, who proposed it, which page it touches, its weight (+added/−removed), and a line diff: green for what it adds, red for what it removes. If the agent left a self-review note on the proposal, it sits beside the diff - the agent telling you where it is confident and where it is not, before you read a line. The queue itself reads like a triage view: each row carries the note, the diff's weight, and how long it has waited.
4. Press **Merge** or **Reject**. That is the entire ceremony.
An agent's proposal, waiting. The diff is the whole conversation.Merge lands it: the page updates, and the byline says who really wrote it.
## People propose too
Agents are not the only ones held to review. Mount a repository with `--suggest` and the human front follows the same rule: the editor works exactly as everywhere else, but **Update becomes Propose**, and pressing it opens a branch instead of committing. The proposal lands in the same card, gets the same diff, and waits for the same Merge. One review ceremony for every writer, whatever species they are.
## What Merge does
Merge lands the proposal on the page: a merge commit on the base branch, the proposal branch deleted, the page and the search index updated immediately. The agent's own commit survives in history with its name on it, so `git blame` a year from now still tells the truth about who wrote what.
## What Reject does
Reject deletes the branch, and keeps the record. Who refused what, and when, is written down; the proposal's tip commit survives under a kept ref in the clone, so the agent's diff never silently ceases to exist. The page itself never knew it was threatened, and rejections appear on the Activity page for the owner - a refusal is a decision, and decisions deserve a trace.
## When the page moved underneath
If someone edited the page after the agent proposed, the two versions may no longer merge cleanly. Hanji checks before you do anything: the proposal opens with a warning and Merge is disabled. There is no conflict-resolution screen, on purpose. The honest move is to ask the agent to re-propose from the current page, or reject. A merge you have to untangle by hand defeats the point of reviewing calmly.
## The honest notes
- The review surface is owner-only. Agents can propose all day; only you merge.
- A proposal is a branch on the mount's remote, nothing more. There is no separate proposal database to back up or migrate. Walk away and the proposals walk with the repo.
- Local mounts (a working directory on disk) have no proposal surface. Proposals need a remote to hold branches, so they live on clone mounts.
- One page per proposal in this version, which is exactly what `hanji_propose` produces.
- An agent can attach a self-review note to a proposal (what it is confident about, what it is not). The note rides the proposal's commit body and shows with the diff - read it before the diff, it usually tells you where to look.
Read [[Agents]] for minting the token that can propose, and [[Permissions]] for the scopes behind it.
---
---
title: Publish to the web
order: 48
description: One command turns everything open-to-everyone into a website, llms.txt included.
---
# Publish to the web
Some of what a team writes deserves an audience: the docs, the handbook, the
pages you would happily show a customer. Hanji publishes exactly the content
you have opened to everyone - and nothing else. The lock decides what goes;
the exporter never widens it.
The site you are reading right now was made this way.
## Choose, then publish
```bash
# 1. open what should be public (a mount, or just a folder)
pnpm hanji rule handbook everyone-read
# 2. render it
pnpm hanji export ./site
```
`./site` is now a complete static website in Hanji's look: your sidebar, your
reading order, dark mode, zero JavaScript. Beside every HTML page sits the
same page as plain Markdown, plus `llms.txt` and `llms-full.txt` at the root -
so the agents reading the open web get your writing as text, not scraped
pixels.
## Put it on GitHub Pages
```bash
cd site
git init -b main && git add -A && git commit -m "Published with Hanji"
git remote add origin git@github.com:you/docs.git
git push origin main
# then: repository Settings → Pages → deploy from main
```
Publishing again is the same two commands: `hanji export` straight into that
clone (it cleans its own output and leaves `.git` alone), commit, push.
## Make it yours
A few optional settings shape the published site, set once from the CLI:
```bash
pnpm hanji set export_home_url https://your.site/ # the crumb's way home
pnpm hanji set export_home_label YourSite
pnpm hanji set export_site_label Handbook # the crumb's right side
pnpm hanji set export_edit_base_url https://github.com/you/repo/edit/main/docs
```
With an edit base set, every published page ends with a quiet **suggest an
edit** link straight into your repository's web editor - a reader spots a
typo, clicks, and a pull request is two clicks away. The cheapest
contribution funnel there is.
## As an OKF bundle
Add `--okf` and the export is also an [Open Knowledge Format](https://github.com/GoogleCloudPlatform/open-knowledge-format) bundle, the vendor-neutral Markdown-on-git shape other agent tools read:
```bash
pnpm hanji export ./bundle --okf
```
Every page ships as a concept document with a `type` in its frontmatter (`Page` by default; a page that sets its own `type:` keeps it), the reserved `index.md` files carry no frontmatter, and the bundle root declares `okf_version`. The static site and `llms.txt` still ride along. Your source in git is untouched, the OKF shaping happens only in the copy on disk.
## What stays private
Everything without an everyone rule. A restricted folder inside an open mount
stays dark, its pages and even its images never leave the building. If
nothing is open at all, the exporter refuses loudly rather than shipping an
empty site - you will never publish by accident.
---
---
title: Reading and writing
order: 30
description: The human front: reading, editing, history, and new pages.
---
# Reading and writing
Everything here happens at `http://localhost:4100`, after you sign in. It is meant to feel like an editorial tool, not a git client. The owner and anyone granted `read+write` write straight to git; a contributor's Update reads Propose and lands in review (see [[Permissions]]).
## Reading
Pages open on warm paper, set in a serif for long reading. The sidebar on the left is your mounts and their folders, nested and collapsible, and it expands to show where you are. A breadcrumb across the top tells you the path, collapsing its middle on deep pages so it never runs off the screen. Wikilinks, written `[[by title]]`, resolve to the page they name, and only to pages you are allowed to open.
Selecting a run of prose raises a small bar over it, and the text stays selected while the bar is up. **Copy** puts the selection on your clipboard twice over - as rich text for anywhere that takes it, and as real Markdown for anywhere that does not - so a paragraph pasted into another editor arrives with its headings, lists, tables, and code fences intact, and pasted into a plain text field arrives as the Markdown that made it. `⌘C` works exactly as it always did; the button is just the one that also carries the Markdown. **Comment** is the other thing a selection can mean, and it is a deliberate press - see [[Comments]].
Search is a keystroke away. `⌘K` opens the palette: ranked results with the matching passage highlighted, your recently changed pages when the box is still empty, and the keyboard all the way to Enter. When you want to dig, the full results page holds every match with highlighted passages and per-mount filters. Multi-word queries rank pages by how many of your words they share, adjacent phrases first, and the word you are still typing already matches as a prefix.
⌘K, a few letters, Enter. The keyboard the whole way.
## Writing
Editing is deliberate, and it is one gesture.
1. Press **Edit**, or `⌘E`. The page becomes editable in place, on the same measure you were reading, at the same scroll position - the header bar keeps its breadcrumb and simply swaps its buttons for **Discard** and **Update**.
2. Change what you came to change. It is a real editor: Markdown shortcuts, a slash menu to insert, tables with handles, task lists, images, embeds, and emoji (the toolbar's picker, or type `:` and a few letters).
3. Press **Update**. Hanji writes a single commit behind the scenes, with a clean message, and drops you back into reading.
The header bar stays with you: back and forward, the breadcrumb, and the page's actions stick to the top of the view, so a long page never scrolls away from its own controls. A hairline appears under it once the prose is running beneath, and is gone again at the top of the page.
### Line breaks, and tables
`⇧Enter` puts a line break inside the block you are in rather than starting a new one. Inside a table cell, plain **Enter** means the same thing, because a cell is one line and has nothing to split. A break is written to the file as ` ` - the only spelling a table row can carry, and the one Hanji uses everywhere so that a break survives being moved from a paragraph into a cell and back.
That is also as far as a list in a cell goes: broken lines, each with its own dash. Markdown tables hold inline content only, so a real bullet list cannot live in a cell in any tool that writes Markdown - what you get is the lines, correctly broken, and the dashes you typed are left exactly as you typed them.
### Highlights
Highlight a phrase the way you would bold one: select it and press `⌘⇧H`, pick a swatch from the selection bubble or the bottom toolbar, or simply type `==like this==`. There are four colours - a default yellow, then blue, green, and pink - and pressing the swatch a highlight already wears takes it off again.
What lands in the file is worth knowing, because it is your file. The default colour writes `==the phrase==`, the portable spelling every other Markdown tool understands. A chosen colour has to write `the phrase`, because standard Markdown has no way to name a colour. So the common case stays clean Markdown, and colour costs you portability only when you ask for one. Both read back as the same highlight, in the page and in the editor alike.
If you changed nothing, nothing is committed. There is no draft state to babysit, and no save button that lies. And because edits are byte-fidelity, the commit for a one-word fix is a one-line diff. See [[Byte-fidelity]].
The whole gesture: Edit, change, Update. One commit happened behind it.Markdown as you type: hashes make headings, stars make bold, a dash starts a list.The chrome of edit mode: the header bar you were already reading under, and this one floating bar.Type a slash on a fresh line to insert.
## Fresh by the time you look
A page you are reading keeps itself current. While its tab is visible it checks itself every seven seconds, and when anyone changed it meanwhile - another editor pressing Update in the app, or an agent editing the underlying files - the page refreshes in place, quietly; switching back to the tab is enough to catch up. A one-line note says it happened, naming the page that moved, with a link straight to it when the change was somewhere else, or pointing at the Activity page when several moved at once. That includes the pictures: replacing an image counts as a change even though no prose moved, and the new picture arrives rather than the one your browser had already cached; no note for that, since there is no page to name and nothing for one to point at. When someone else has the page open in their editor, "Remy is editing" sits beside the byline, so you know a change is coming before it lands. The byline carries the trust surface: "checked just now" or "checked 4m ago" - when the index last agreed with the files, a different claim than the page's own "updated", which is about content - ticking as you read, with a quiet retry when a sync fails. Silence means nothing changed; nothing here ever interrupts.
## When two edits collide
Hanji notices if the page changed underneath you, and it never makes you throw your edits away to deal with it. While your editor is open, the page checks itself every seven seconds, and a change raises only a quiet line, not an alarm: "Remy changed this page. It will merge when you save." The line names the other hand where the name is honest (the last committer, on a git-backed mount) and says "This page changed" on a plain folder. If someone else is editing at the same moment, a second line says so: "Remy is also editing this page. Edits merge when you save." There is no lock and no take-over; the merge waits until you save. As you type, your draft is kept in the browser too, so a reload or a closed tab loses nothing.
When you press Update and the page has moved, Hanji tries to fold the two edits together. Most of the time they touch different parts (someone fixed a link at the foot of the page while you rewrote the top), and the fold is clean: your work and theirs both land in one commit, and a line tells you it happened. A contributor gets the same fold: the proposal that lands in review already carries both edits. Only when the two genuinely changed the same lines does Hanji stop, and even then it never dead-ends. It shows you what the other side changed and offers doors, none of which discard your work or theirs: **Keep my version** keeps your wording where you both changed the same lines and folds in the rest of their edit; **Take theirs** loads their page and keeps your draft recoverable; **Propose** sends that same merge to review and leaves the page on theirs. A contributor, or anyone on a suggest mount, sees two doors, because for them Keep mine is the propose door: **Propose my version** and **Take theirs**. The save itself still re-checks the file on disk at the last moment, so nothing is ever overwritten unseen, and this holds on plain folders too, where no git history could bring an overwritten change back.
## New pages
Making a page is the same gesture pointed at a path that does not exist yet. The fastest way is the + that appears at the end of every sidebar row: on a mount it creates at the root, on a folder it creates inside it, and on a page it nests, converting that page into its section's landing with the new page beneath. Give it a title, write, and Update. That first Update is the commit that creates the file.
A title, a line, Update. The file now exists, and the sidebar already knows.
## Organizing the sheet
The sidebar is not just a map. It is where you arrange the collection.
- **Move**: drag a page or folder onto another folder, or onto a mount's header for its root. A folder brings everything inside it, and the commit behind the drop is a real `git mv`, so history follows the page.
- **Reorder**: drop on the edge of a sibling row, where the insertion line shows. Hanji writes the new arrangement into the pages' `order:` frontmatter, as one commit. This is the *manual* order, and it is the default the sidebar shows.
- **Nest**: drop onto the middle of a page and it becomes a section landing with the dragged page inside, the same conversion the + performs.
- **The rail too**: drag one mount header onto another to reorder the mounts themselves.
Drops apply instantly; git catches up behind. If something blocks a move, the tree snaps back and a toast tells you why. And if you were reading the page you just moved, the address quietly follows it.
### Sorting a section
The curated `order:` is not the only way to read a folder. Open a folder (or a mount) and a small sort control appears on its row: leave it **Manual** to keep your arrangement, or switch that section to **Newest first**, **Oldest first**, or **A–Z** by title. The date it sorts by is the one in the filename when a page has one (a `2026-04-24-…` note sorts by that), and the page's last-commit date otherwise - so a bulk rename that resets git's dates does not scramble the order. The choice is per folder and per person, kept in your browser, so it never changes what anyone else sees or writes anything to the repository. A re-sorted section keeps its control lit so you can tell at a glance. While a section is sorted, drag-to-reorder pauses there: the order you see is not the stored one, so switch back to Manual to curate again.
## Deleting
Next to Edit sits a quiet trash. The first click arms it into an explicit red question, the second deletes: a `git rm` and one commit. Deleting a section landing takes its children with it, the inverse of nesting. Nothing is ever truly gone, though. The repository remembers every sheet, and history can bring one back.
## History
Every page carries its past. Open its history to see who changed it and when, as a list of revisions with a byline, and open any revision to read the page as it was then. Because the store is git, this is not a feature bolted on top. It is the commits, shown well.
The page's past: every revision, and any of them readable as it was.
Read [[Agents]] next, to let a coding agent read and propose against these same pages.
---
---
title: Start here
order: 0
description: Paper-thin documentation tool. Start here.
---
# Start here
This handbook is written in Hanji, kept in Hanji's repository, and read through the product itself. 🎉
## Quick start
Five minutes from a repo full of Markdown to a place worth reading:
```bash
git clone https://github.com/gethanji/hanji && cd hanji && pnpm install
pnpm dev:web # → http://localhost:4100
```
First visit, a short welcome takes it from there: your name for the place, your password, your first folder of Markdown.
The full walk, with what each line does for you: [[Quick start]].
## Set up yours
Four shapes Hanji is lived in. Start with the one that matches you.
| You are | Start here |
|---|---|
| **Just you, on your machine** | [[Quick start]] - the five minutes above, explained |
| **A team on a tailnet** | [[On your tailnet]] - one shared instance, and nobody types a password |
| **Working with coding agents** | [[Agents]] - scoped reads, and their edits arrive as proposals you review |
| **Publishing docs to the world** | [[Publish to the web]] - one command; this site is the proof |
## The documentation
How to use it, and how it actually works. Written to serve you mid-task.
| Page | What it does for you |
|---|---|
| [[Install Hanji]] | From clone to a running instance, with the rough edges named. |
| [[Mounts]] | Point Hanji at your repositories, and keep them in sync on their own. |
| [[Reading and writing]] | The daily feel: reading, editing, search, history, organizing. |
| [[Proposals]] | Review what agents and teammates propose - diff, Merge, done. |
| [[Command reference]] | Every CLI command in one table, for when you know what you want. |
| [[Permissions]] | The lock, from mount to single page - and exactly what it enforces. |
| [[The two faces]] | The human front and the agent surface, over one git. |
| [[Byte-fidelity]] | Why a one-word edit is a one-line diff, forever. |
| [[Performance]] | Paper-thin: the speed and size numbers. |
| [[Features]] | The whole list, with honest status. |
## The story
The why. What makes this more than a wiki - [[The story]] opens with the manifesto and carries the rest: the name, the difference, the fit, the mission, the principles, the brand.
Deeper still: **how it's built** ([[How it's built]] · [[The serializer]] · [[The core]] · [[The editor]] · [[The MCP server]] · [[The web front]]), **contributing** ([[Contributing]] · [[Developing]] · [[Conventions]] · [[Open core]]), **the brand** ([[The brand]] · [[The visual language]] · [[Voice]]), and **the project** ([[Principles]] · [[Where we are]] · [[Changelog]]).
---
---
title: Byte-fidelity
order: 30
description: Why a one-word edit is a one-line change, and why that matters.
---
# Byte-fidelity
Here is a small test most editors fail. Open a document, change one word, and save. Now look at the diff. If four paragraphs you never touched also changed, quotes flipped, list markers rewritten, whitespace reflowed, then the tool has been rewriting your work behind your back. On a knowledge base kept in git, that is not cosmetic. It is the difference between a history you can read and a history that is noise.
Hanji refuses to do that. A one-word edit produces a one-line change. That is the whole promise, and it is harder to keep than it looks.
## How it holds
When Hanji reads a Markdown file, it does not just parse it into a tree and forget the original. It keeps a map from each block back to the exact bytes it came from. When you edit, only the blocks you actually changed are written back from the tree. Every block you left alone is written back byte-for-byte from the original, untouched. The reconciler works at the block level, so a paragraph you did not open never gets the chance to drift.
The result is quiet. A save you make when you changed nothing is byte-identical to the file you started with. There is no commit, because there is nothing to record.
## Why it is the crown jewel
Two reasons, and both are about trust.
The first is your history. When the diff of a one-word fix is one line, `git blame` still means something, a review is still readable, and a year of edits does not turn into a wall of reformatting churn. The people and the agents reading your history can trust that a change shown is a change made.
The second is the walk-away promise. Byte-fidelity is what lets Hanji claim your files are still just your files. If the tool quietly rewrote them, they would slowly become the tool's files, in a shape only it produced. Because it does not, you can leave at any moment and take exactly what you would have had if Hanji had never existed.
This is guarded, not hoped for. A corpus test round-trips more than 600 real Markdown files through the engine and asserts every one comes back byte-identical. That gate stays green, or the build does not ship. See [[Performance]] for the other numbers, and [[Principles]] for where this belief sits among the rest.
---
---
title: Features
order: 50
description: What Hanji does today, in one table.
---
# Features
The full list, in one place, with the honest status on each. If a row says shipped, it is tested and live in the front you are reading this on. If it says next, it is being built and not yet true.
## Reading and writing
| Feature | Status | Note |
|---|---|---|
| Markdown reading, rendered with care | Shipped | Warm paper, serif, careful spacing |
| In-place WYSIWYG editor | Shipped | Edit, change, Update, one commit - same scroll position, no navigation |
| Sticky page chrome | Shipped | Breadcrumb and actions held at the top; one bar for reading and editing |
| Copy as Markdown | Shipped | A selection lands on the clipboard as Markdown and as rich text |
| Byte-fidelity edits | Shipped | A one-word edit is a one-line diff |
| Tables, with Notion-style controls | Shipped | Hover handles, insert, drag to move; Enter breaks a line inside a cell |
| Line breaks | Shipped | `⇧Enter` anywhere, Enter inside a cell; written as ` ` so one spelling travels |
| Task lists, interactive | Shipped | Check them off in the page |
| Strikethrough and bare-URL autolinks | Shipped | Full GFM parity in the editor |
| Highlights, four colours | Shipped | `⌘⇧H` or a swatch; portable `==x==` for the default, `` for a colour |
| Slash menu to insert | Shipped | Blocks, images, embeds |
| Images uploaded into the repo | Shipped | Into `assets/`, no external link-rot |
| Embeds, video and iframe | Shipped | Domain allowlist: figma, youtube, loom, vimeo |
| Page history, byline and revisions | Shipped | View any past version |
| Page comments | Shipped | A thread at the foot of every page, for anyone who can read it |
| Anchored comments | Shipped | Anchor a note to a word or sentence; a side panel gathers them, filter and resolve in place |
| Comments stay out of git | Shipped | In Hanji's own store, never a commit and never in an export |
| Nested, collapsible sidebar | Shipped | Built from page paths |
| Breadcrumb navigation | Shipped | Middle-collapsing on deep pages |
| Wikilinks, `[[by title]]` | Shipped | Resolved and permission-aware |
| Full-text search | Shipped | bm25, title-boosted, multi-word aware, scoped to what you can read |
| Search palette and results page | Shipped | `⌘K` with ranked snippets; `/kb/search` with filters |
| Emoji, three ways in | Shipped | Toolbar picker, `:query` menu, `:tada:` conversion |
| Sidebar quick-create | Shipped | A + on every mount, folder, and page |
| Drag to move, reorder, and nest | Shipped | Instant, git behind, auto-revert on failure |
| Per-section sort | Shipped | Manual, Newest, Oldest, or A–Z - by filename or commit date, per folder, per person |
| Page delete | Shipped | Two-step trash, `git rm`, recoverable from history |
| Works on a phone | Shipped | Top bar, drawer, the same sidebar |
## The rail and the agents
| Feature | Status | Note |
|---|---|---|
| Git-backed mounts | Shipped | Point at repos or folders, one tree |
| Sync and a rebuildable index | Shipped | The index is a throwaway cache |
| MCP server for agents | Shipped | Scoped search and reading |
| Agents can read comments | Shipped | Opt-in `read+comments` scope, page-scoped by the same lock |
| Plain-HTTP aliases beside MCP | Shipped | `GET /pages`, `/page`, `/search`, `/comments`, `POST /propose` - curl counts as an agent |
| Propose-as-PR writes | Shipped | Agents open branches, people merge |
| Proposal review and merge, in the front | Shipped | A pending badge, a line diff, Merge and Reject |
| Proposals triage queue | Shipped | Self-review note, +added/−removed weight, and age on every row; the note sits beside the diff |
| Activity page | Shipped | Eight weeks in one strip: humans above the line in ink, agents below in celadon; honest attribution from co-author trailers and bot identities; filter by actor and kind; a local mount's uncommitted edits as their own row |
| Per-mount permissions | Shipped | `read`, `read+propose`, and `read+write` for people, one read path |
| Content-first visibility | Shipped | Everyone / restricted, inherited downward, deepest rule wins |
| Path grants, down to one page | Shipped | Mount, folder, or a single file - for people and agent tokens alike |
| Share, on the page itself | Shipped | General access + people, a name, a level, done |
| `hanji export` | Shipped | llms.txt, llms-full.txt, and a per-page `.md` mirror of what is open to everyone |
| `hanji digest` | Shipped | What changed and what awaits you, written into the workspace as a page; a quiet day writes nothing |
| No bundled model, no metering | Shipped | By design, forever |
## The workspace
| Feature | Status | Note |
|---|---|---|
| Name and logo | Shipped | SVG adapts to light and dark; anything else gets a mode-safe plate |
| Colors and fonts | Shipped | Three OKLCH seeds reinterpreted per mode, contrast secured |
| Sidebar collapse | Shipped | Automatic by mount count, or your explicit choice |
| Content width and text size | Shipped | Three measures, and a size slider that scales the whole reading surface |
| Solo and team modes | Shipped | Solo keeps the sharing chrome away; team brings Share, People, and general access |
| Tailscale zero-login | Shipped | On a tailnet, the network signs people in; grants stay owner-assigned |
| A phone-ready shell | Shipped | A slim top bar, and a drawer carrying the full sidebar |
| Version in the workspace menu | Shipped | One source of truth: the repository's own version |
The same sidebar, on a phone: one tap, the whole collection.
For what is coming next, see [[Where we are]]. For what shipped when, see [[Changelog]].
---
---
title: What Hanji is
order: 20
description: How Hanji turns the repos you already have into one permissioned knowledge base.
---
# What Hanji is
Hanji turns the git repositories a team already has into one shared, permissioned knowledge base with two faces. One for people, one for agents. Nothing moves and nothing is copied into a new store. Hanji reads what is already there and serves it well.
## Mounts
You point Hanji at git repositories, or at folders inside them, and each one becomes a **mount** in a single tree. A repo of engineering docs, a folder of product specs, a private repo of meeting notes, all of it lands in one place to read and search, while each repository stays exactly where it is. Hanji never becomes the owner of your content. Git remains the source of truth, and the index Hanji builds on top is a cache it can throw away and rebuild at any time. See [[Mounts]] for how to add one.
## For people
A quiet reading and writing front. Pages are Markdown, rendered with care on a warm sheet meant for long reading. Editing is deliberate. You enter edit mode, make your changes, and press Update, and that Update is a single commit with a full history behind it. Git stays out of sight unless you go looking for it. There is a full editor under that calm surface, with tables, task lists, images, and embeds, and it never rewrites a line you did not touch. Read [[The two faces]] for how the front is built, and [[Byte-fidelity]] for why your history stays clean.
## For agents
Coding agents read the knowledge the way they read code, as files in a repository, at the speed of a local clone. Where a hosted surface helps, a small MCP server offers scoped search and reading, plus a write path that proposes changes as branches rather than editing pages in place. An agent can suggest. A person decides. That one rule fixes the failure mode everyone with a bundled AI has hit, the model that confidently edited the wrong page. Here it opens a branch, and you merge it or you do not.
## For the sensitive parts
Some folders hold decisions and notes that should not be open to everyone. Every read Hanji serves, search and links and the agent tools alike, runs through a single permission check, so a reader only ever sees what they are allowed to see. A search will not leak a title. A wikilink will not resolve to a page you cannot open. And we are honest about where that check is strict today and where it is still getting finer. Read [[Permissions]] for exactly what is enforced now.
## What it will never do
No bundled model. No per-token credits. No format you cannot walk away from. If you leave, you already have everything, because it was always just your files in your git. Read [[Why Hanji is different]] for the longer version of that promise.
---
---
title: Performance
order: 40
description: Paper-thin. The speed and size numbers, measured, and the ones still pending.
---
# Performance
Paper-thin is not only a metaphor about longevity. It is also a claim about weight. A knowledge base you read all day should be quick and light, and most are neither, because every read is a round-trip to someone's cloud and every page ships a small application to your browser.
Hanji is built the other way. Reads come off a local git clone and a small local index, not a network hop. Here are the numbers, measured on the instance we live in daily, with the ones we have not earned yet marked as such. Honesty about limits applies to benchmarks too.
## The agent read path
This is the one that has to be fast, because agents read constantly. Measured against the running instance over 127.0.0.1, five runs each.
| Operation | Time | What it does |
|---|---|---|
| Fetch a page | ~2 ms | Return a page's Markdown by mount and path |
| Full-text search | ~2 to 3 ms | Query the FTS5 index across every mount you can read |
Single-digit milliseconds, because there is no cloud in the loop. The agent reads from a local clone and a local index, the same way it reads your code.
## Indexing at scale
The first sync is the expensive moment: every page is parsed, stored, and
full-text indexed. Measured on a real corpus, on the same machine as above.
| Operation | Time | What it does |
|---|---|---|
| First sync, 5,500 pages | ~13 s | Clone-to-searchable, one mid-size team KB |
| First sync, 35,000 pages | ~2.5 min | The same pipeline at stress-test size |
The cost stays near-linear because index writes run in batched transactions
and the full-text rows are addressed by rowid; an earlier build paid a scan
of the whole growing index per page, which turned the 35,000-page sync into
half an hour. Re-syncs only pull what changed in git first, and the indexing
pass yields to the event loop as it runs, so a big sync never freezes the
instance serving it.
## Weight and dependencies
| Thing | Number |
|---|---|
| Bundled LLM SDKs | 0 |
| AI credits or metering | none, ever |
| Web app runtime dependencies | 24, all ProseMirror, remark-rehype, React, and Next |
| Test cases | 409 |
| Real Markdown files round-tripped byte-identical | 600+ |
The zero on the first row is the point. Hanji ships no model and meters nothing, so nothing about its cost or its weight grows because your team wrote more this month.
## Weight in the browser
Measured on a clean production build, served and loaded through a real request (the `pnpm smoke` boot test).
| Thing | Number |
|---|---|
| Shared JavaScript, first load | ~103 kB |
| Reading a page, first load | ~108 kB |
| A rendered page, over the wire | ~14 kB of HTML |
| Time to first byte | ~55 ms |
Reading sits just above the shared floor. The editor is a separate chunk that does not load until you press Edit, so the common case, reading, stays paper-thin, and the writing tools arrive only when you reach for them.
The index itself is worth one honest note. It is a cache. Whatever it costs in space, you can delete it and rebuild it from git, because the repository is the only thing that has to be trusted. Read [[Byte-fidelity]] for the engine underneath, and [[Why Hanji is different]] for why none of this is bundled.
---
---
title: Permissions
order: 20
description: The two-plane model, and exactly what it enforces today.
---
# Permissions
Permissions are where most knowledge tools get honest too late. Hanji tries to get honest early, which means telling you plainly what the lock does today and what it does not do yet.
## The model
Two planes. One is an open human front, where a team reads and writes in the clear. The other is the set of sensitive areas that must stay closed, the meeting notes and the decisions and anything you would not tack to a shared wall. The whole design rests on one rule: **every surface honors the same check**. Search, wikilinks, page reads, and the agent tools all ask the same question before they show you anything, so there is no back door where a locked title slips out through search, or a link resolves to a page you were never allowed to open.
That is not a claim to make lightly, so here is how it actually works.
## One read path
Every read Hanji serves runs through a single module. The scope check, which mounts may this reader see, is compiled in exactly one place and nowhere else. Search filters to your readable mounts before it runs. A page read returns nothing for a mount you cannot see. Wikilink resolution uses the same filter. This is the boring, load-bearing part. There is one door, and it is the same door for people and for agents.
A locked page and a page that does not exist look identical to a reader who lacks access. Both come back as not found. You cannot probe the shape of what you are not allowed to see.
## Scopes today
Access is granted per mount, in three levels.
| Scope | The reader can | The reader cannot |
|---|---|---|
| none | see nothing of the mount, not even that it exists | read, search, or link into it |
| `read` | read, search, and follow links inside the mount | write, or propose changes |
| `read+propose` | everything `read` can, plus propose changes as a branch or PR | merge, or write directly |
| `read+write` | everything `read+propose` can, plus commit to base from the front and upload images | merge, or restructure the tree |
`read+write` is for people only: a token never holds it.
The owner sees every mount. An agent gets a bearer token whose scopes take the same shape as a person's grants - a whole mount, a folder, or exactly one page - so an agent that helps with the engineering docs never sees the folder of sensitive notes, even when both live in the same repository. See [[Agents]] for how that works in practice.
## Visibility belongs to the content
The model reads the way Notion and Confluence taught everyone to think. Content carries a general access level - **Everyone can read**, **Everyone can suggest**, or **Restricted** - set on a mount, a folder, or a single page, flowing down to everything beneath. Open content is simply open: no per-person bookkeeping. Restricted content is dark by default, and the deepest rule covering a page wins, so one restricted folder inside an open mount goes dark for everyone.
People are the exceptions. A person granted a path - a mount, a folder, or exactly one page - punches through a restriction at or below their grant. That is the 1:1 note: a restricted folder, two names, and nobody else sees it in the tree, in search, or through a wikilink. A locked page still looks identical to a page that does not exist.
Sharing lives where the content lives (in team mode - solo keeps this chrome hidden until you switch, in Settings): a **Share** control on every page, and on every mount and folder in the sidebar, showing the general access (and where it is inherited from), the people with specific access and through which grant, and a one-line way to add someone with read, suggest, or edit. Settings keeps the overview: a Mount access tab for general access per mount, and a People tab for everyone's grants in one place. Revoking takes effect on the person's next request. The rule and grant tables are the implementation; the page is the interface.
Share, where the content lives: the general rule on top, the exceptions by name.
One deliberate exception: **agents never inherit "everyone"**. A token sees exactly the scopes it was minted with, whatever the content rules say - opening a mount to your team never opens it to a bot.
## People, same scopes
The same levels apply to humans. The owner adds a person in Settings with a name, a password, and per-mount scopes: `read` makes a **viewer**, `read+propose` makes a **contributor** whose Update reads Propose and lands in the owner's review card, and `read+write` makes an **editor** whose Update commits to base, the way the owner's does. Nobody but the owner restructures the tree, reviews proposals, or sees settings. In user terms: viewers read, contributors suggest, editors write, the owner decides. One read path, one review ceremony, whatever species the writer is.
## What is honest about it
The unit of permission is now the path, down to one file, for people and agent tokens alike, and it holds on the same single read path as everything else. What remains honest to say: product privacy is not storage privacy - the bytes are plaintext in a git repo, so the instance owner and the git host can always read them. A 1:1 note is hidden from every other principal; hiding it from the owner would take cryptography, not permissions. The two-plane model is real and enforced on one honest read path, and [[Where we are]] tracks the rest without rounding up.
---
---
title: The two faces
order: 10
description: One knowledge base, a human face and an agent face, over the same git.
---
# The two faces
Most tools are built for one reader and then bolt on the other. A wiki built for people adds an AI chat box. An agent tool built for machines adds a rough editor and hopes the humans cope. Hanji starts from the fact that both read the same knowledge, and gives each the surface it actually wants, over the same git underneath.
## The human face
A person should never have to think about git to write down what they know.
So the front hides it completely. You open a page and it reads like a good magazine, warm paper and a serif set for long reading. You click Edit, the page becomes editable in place, you change what you came to change, and you press Update. Behind that one button, Hanji writes a single commit with a clean message and a full history. No staging, no branch names, no merge dialog. And if you change nothing, nothing is committed, because there was nothing to record.
Under the calm surface is a real editor. Tables with the controls you expect, task lists you can check off, a slash menu to insert, images that upload into the repo itself, and embeds for the tools you already use. It stays out of your way, and it never rewrites a line you did not touch. See [[Byte-fidelity]] for why that last part matters more than it sounds.
## The agent face
An agent should not have to pretend to be a browser to read your docs.
Agents already live in git, so the first face for them is git itself. They clone, they read files, they search, all at local speed, with no API throttle and no hosted round-trip. Where a live surface helps, a small MCP server gives them scoped search and reading through the same permission check a person gets. And when an agent wants to contribute, it does not reach into a page and overwrite it. It proposes a change as a branch, and where GitHub is connected, as a pull request. A person reviews it in the front and merges. The agent suggests, you decide, and your history never fills up with edits nobody approved.
## The same git underneath
The point of two faces is that there is one thing behind them. A person edits a page and an agent sees the new bytes on its next read. An agent proposes a branch and a person reviews it on the same warm page they read everything else on. Nobody is working on a copy. It is one knowledge base, and it was your git the whole time.
Read [[What Hanji is]] for the overview, and [[Using Hanji]] when you want to actually drive it.
---
---
title: Quick start
order: 5
description: A running Hanji in five minutes, on the Markdown you already have.
---
# Quick start
You have repositories full of Markdown - notes, docs, decisions that deserve
better than a file tree. In five minutes they become one quiet, readable
place: yours to write, your team's to read, your agents' to work in.
*Pre-release note: the code ships soon as one clean release. This page is
exactly what day one looks like.*
## The five minutes
```bash
git clone https://github.com/gethanji/hanji && cd hanji
pnpm install
pnpm dev:web # → http://localhost:4100
```
The first visit greets you with a short welcome: name the place, choose your
password, and point it at your first pages - a folder of Markdown on your
machine, or a git URL to clone. That is the whole setup. No environment
variables, nothing to configure by hand. (Prefer configuring by env, for a
server? [[Install Hanji]] keeps that path.)
Then your notes are there, rendered on warm paper. Click **Edit**, change
something, press **Update** - you just made a clean git commit without
thinking about git once. That is the whole trick, and it never gets more
complicated than that.
## Where to next
Pick the shape that matches your life:
- **A team on your tailnet** - share one instance where the network signs
people in and nobody types a password. [[On your tailnet]].
- **Your coding agents** - give them scoped reads and let their edits arrive
as proposals you review. [[Agents]].
- **A public site** - one command turns everything you've opened into a
website, `llms.txt` included. [[Publish to the web]].
- **Just curious how it holds together?** [[Mounts]], then
[[Reading and writing]] - the two pages that explain the daily feel.
---
---
title: Where we are
order: 120
description: What is shipped, what is next, and what we are still exploring.
---
# Where we are now
Hanji is early, and built in the open with its own tools. This page is written in Hanji, kept in Hanji's repository, and read through the product you are looking at now. It is honest to a fault, because pretending to be further along than you are is its own kind of lock-in. There are no dates here, because a solo, early project that promises dates is lying. The order is real, though, and it follows the rule in [[Mission and vision]]: earn trust first, charge for what an administrator wants, never gate what one person needs to write.
## Now, shipped and live
- [x] Byte-fidelity Markdown engine, guarded by a corpus gate
- [x] Git-backed mounts, sync, and a rebuildable index
- [x] The human front: reading, in-place WYSIWYG editing, history, nested navigation
- [x] The agent surface: MCP scoped read tools and propose-as-PR
- [x] Per-mount permissions on a single read path
- [x] The agent git flow, end to end: an agent proposes, you review the diff in the front, and merge (see [[Proposals]])
- [x] Search worth the name: a title-boosted, multi-word-aware backend under a `⌘K` palette and a full results page
- [x] The sidebar as an organizer: quick-create, drag to move, reorder, nest, and delete, all real git behind an instant UI
- [x] Per-section sorting: a folder or mount sorts its own children by date (newest or oldest, from the filename or the commit) or A-Z - a personal, per-section choice kept in the browser
- [x] Every page's paper trail: a byline of who began it and who last changed it, read from git history, shown on the page and powering the recency sort
- [x] A workspace made yours: name, logo, OKLCH theming with secured contrast, fonts, and a phone-ready shell
- [x] Proposal from the human front: on a suggest-only mount, Update becomes Propose, landing in the same review card
- [x] Several human principals: viewers, contributors, and editors with per-mount scopes, added by the owner in Settings; editors commit to base, contributors propose
- [x] The lock, down to a single page: path grants (mount, folder, or one file) and a Share button on the page itself
- [x] Content-first visibility: general access inherited downward, restrictions that go dark, people as the exceptions
- [x] Tailscale mode: run on a tailnet and the network signs people in, no password, grants untouched (see [[On your tailnet]])
- [x] Path grants for agent tokens: a token holds a mount, a folder, or one page - the same mechanism people have
- [x] `hanji export`: the everyone-visible content mirrored as `llms.txt` plus per-page Markdown, for the agents that read the open web
- [x] Changes arrive on their own: a signed push-to-sync webhook, and an opt-in poll loop for tailnets and local mounts
- [x] A first run without a terminal: the welcome walks three soft steps - source, details, name and owner - suggesting the name from what you picked, and signs you in
- [x] The publishing plane: `hanji export` renders a browsable static site - the hanji look, no JavaScript - beside the Markdown mirror. This handbook is published with it.
- [x] Fresh by the time you look: sync-on-focus, a byline freshness line, early conflict warning, a content-compare save guard, and agents that sync local mounts before every read
- [x] The freshness note carries a destination: it names the page that moved and links to it, or points at Activity when several did; a change that moved no page refreshes without a note
- [x] Images count as freshness too: a per-mount asset fingerprint makes a replaced picture a change the screen hears about, and stamps the image URLs so the browser cannot answer from its cache
- [x] The Activity page: what moved across your mounts - edits, comments, reviews - straight from the history, plus a local mount's edits not yet committed, with honest actor labels and a "seen up to here" line
- [x] The activity strip: eight weeks of days above the feed, human hands above the line, agents below, with filters over the feed by actor and kind
- [x] `hanji digest`: the day's changes and the waiting queue written into the workspace as a page - the artifact is the notification, cron is the only scheduler
- [x] The agent briefing channel: `hanji_changes` / `GET /changes` for "what moved since I last read", plus an Atom feed of the same, token-scoped like every read
- [x] Open Knowledge Format interop: any OKF bundle mounts as-is, and `hanji export --okf` emits a conformant one - typed concept docs, reserved index files, an `okf_version` root
- [x] One sticky chrome bar for reading and editing, and edit mode that swaps in place - same scroll position, same header, only the buttons and the toolbar change
- [x] A selection that stays selected: Copy (as Markdown *and* as rich text) and Comment offered on a bar, rather than the comment box taking the selection
- [x] Highlights in four curated colours, portable `==x==` for the default and `` for a chosen one (see [[Reading and writing]])
- [x] Page comments: a thread at the foot of every page, and anchored comments on an exact word or sentence, highlighted in the prose for every reader and preserved under "On an earlier version" when the text beneath them changes (see [[Comments]])
- [x] Presence on the page: who else is editing, named beside the byline and inside the editor, and a page that checks itself every few seconds while you look, so a conflict folds at save instead of surprising you
Reading and writing both feel like paper. That was the first thing that had to be true. See [[Features]] for the full list, and [[Changelog]] for what shipped when.
## Next, in build
The agent git flow shipped; what remains of the real test is a team living on it daily.
- [ ] **A proper team on a shared instance** - people and agents writing side by side, the proof no more code can produce
## Later, the paid seam
Everything an administrator needs to trust Hanji with a whole organization. This is where the revenue is, and it is deliberately not where the writing is.
| Area | What it adds |
|---|---|
| Hosted sync | A managed instance, for teams who would rather not run their own |
| Single sign-on | Log in with your identity provider |
| Audit | Who read and wrote what, and when, read straight from the git history |
| Hardening | The operational harnesses that keep an organization-size instance honest at scale |
## Exploring, not promised
Ideas we like and have not committed to.
- Real-time collaboration, reconciled with git as the source of truth
- Serving a folder as an installable agent skill
- A desktop shell around the web plane, once it can be distributed properly
Read [[Mission and vision]] for the why behind the order.
---
---
title: The brand
order: 70
description: The whole of Hanji's brand in one page: the name, the look, the voice.
---
# The brand
Hanji's brand is one idea, held consistently: this is paper made to be written on and meant to last, and everything you can see or read should feel that way. Three pieces carry it.
- [[The name]] is where it starts. 한지 is Korean paper that has survived a thousand years, and the name is a promise about longevity.
- [[The visual language]] is how it looks: warm paper, an editorial serif, one secret color, and the ㅎ mark cut back to its bones.
- [[Voice]] is how it speaks: plainly, with an edge, honest about its limits.
None of the four is loud. That is the brand. The craft lives in restraint, in what was left out, in one considered color instead of ten. A knowledge base is a quiet place you go to read and to think, and the brand's only job is to keep it quiet and make it feel like it will still be here in ten years.
---
---
title: The visual language
order: 10
description: Warm paper, an editorial serif, celadon, and a mark cut back to its bones.
---
# The visual language
Every visual choice in Hanji answers to one idea: this is paper made to be written on and meant to last. Here is what that produced, and why.
## Warm paper white
The background is a warm off-white, close to a fresh sheet, with the faintest grain laid over everything. Pure white is a screen. Warm white is paper. The difference is small, and you feel it on every page, the way you feel the difference between a glossy flyer and a good book.
## An editorial serif
The reading type is **Newsreader**, a serif drawn for long-form reading, set large with a generous line-height and a comfortable measure. A knowledge base is a place you read, sometimes for a long time, and it should carry the calm of a good magazine rather than the density of a dashboard. The interface itself steps back into a quiet sans, so the writing stays the loudest thing on the page.
## 비색, the secret color
The accent is **celadon (비색, bisaek)**, the blue-green glaze of Goryeo celadon that Korean potters a thousand years ago called *the secret color*. It is calm, it is unmistakably Korean, and it is almost never seen in software. It ties the product to a tradition of objects made by hand and meant to endure, which is the same claim hanji makes about paper.
## The mark
ㅎ or hieut from the Korean alphabet (한글)
The logo is **ㅎ (hieut)**, the first consonant of 한 in 한지 and Hanji, cut back to its bones: a short top stroke, a horizontal bar, and a ring. The letter was already almost a mark, and we only had to clear away what it did not need. The three parts sit exactly where the letter puts them, so it reads as ㅎ to anyone who knows the alphabet and as a calm, balanced glyph to anyone who does not.
The mark keeps its own deep charcoal rather than taking the celadon of the text around it. Celadon is the accent of the writing surface, and the mark is the object that sits on the surface, so it holds its own weight, the way a seal pressed into paper is darker than the ink of the page. It is the single, deliberate exception to the color rule.
## Restraint as the style
There is no chrome for its own sake, no gradient, no theatre of shadows. The craft lives in the spacing, the type, and one considered color. Elegance here is a matter of what we left out. Read [[Voice]] for how the same restraint shapes the words.
---
---
title: Voice
order: 20
description: "How Hanji speaks: plainly, with an edge, and honest about its limits."
---
# Voice
Hanji's visual language is restraint, and its voice is the same restraint carried into words. Here is how it speaks, and why.
## Plain
We say the thing. No jargon standing in for a plain word, no sentence that could have come from any product on any landing page. If a claim can be made in a shorter word, it is. The reader is a peer, a working engineer or designer who can smell marketing from a mile off, and they get talked to like one.
## With an edge
Plain does not mean bland. Hanji takes positions. Git is the store, and a backup target is not good enough. An agent is a reader, not a chat box. The day you charge someone for the right to write down what they know is the day you have become the thing this was built against. The voice is willing to be a little sharp, because a product with no point of view is not worth reading about.
## Honest about limits
This is the load-bearing one. When a feature is not built, we say so. When product privacy is real but storage privacy is not - the bytes stay plaintext in your git, readable by whoever holds the repository - the page says exactly that. A benchmark we have not run honestly is marked as not run, not rounded up. It is the [[Principles]] page applied to the writing: a promise we cannot keep is worse than no promise, so we do not make it.
## In practice
A page reads like a person wrote it, because one did. Sentences vary. Some run long and accumulating, some are three words. There is a wink now and then, and a number wherever a claim can carry one. What there never is: a summary that restates what you just read, or a conclusion that adds nothing. When the point is made, the page stops.
Read [[The visual language]] for the same restraint in pixels.
---
---
title: The story
order: 60
description: The manifesto, and the why behind every decision.
---
# The story
*Paper-thin documentation tool.*
Most knowledge tools are built for the moment you write in them. Hanji is built for every moment after.
We went looking for a place to keep what a team knows, and we kept finding the same shape. A store you cannot read without the app that made it. A model bundled in and metered by the token. A wall between your writing and the agents that, more and more, do the reading. In every case the knowledge was the hostage, and the software was holding it.
So we built the reverse.
Hanji keeps what a team knows as plain Markdown, in a git repository you already own. People read and write through a quiet, well-set web front. Coding agents read and contribute through the same git and a small protocol, at the speed of a local clone. Nothing is bundled. Nothing is metered. Nothing is locked away where only one program can reach it.
That is the whole product in one move. Git is the store, and everything else is a face on top of it.
## Two faces
One face is for people. Pages are Markdown, rendered with care, on a warm sheet meant for long reading. You enter edit mode, make a change, and press Update. That update is a single commit with a full history behind it, and git never once shows itself unless you go looking for it.
The other face is for agents. They already live in git, so Hanji meets them there, plus a small MCP server for scoped search and reading. When an agent wants to write, it does not edit your page. It proposes a change as a branch, and a person decides. An agent can suggest. You merge.
## A lock on the sensitive folders
Some folders hold meeting notes and decisions that are not for everyone. Every read Hanji serves, search and links and the agent tools alike, runs through one permission check, so a reader only ever sees what they are allowed to see. We are honest about what that lock protects today and what it does not. A promise we cannot keep is worse than no promise at all.
## Paper-thin
The name is also a claim about weight. Reads come off a local clone and a small local index, not a round-trip to someone's cloud, so the front stays quick and ships almost nothing to the browser. It is meant to get out of the way. See [[Performance]] for the numbers.
## Why "Hanji"
한지 is Korean paper that has carried writing for a thousand years. The name is a promise about longevity: software comes and goes, and the paper should outlast the app. Read [[The name]] for where it comes from, [[What Hanji is]] for how it works, [[Why Hanji is different]] for what it refuses to do, and [[Mission and vision]] for where it is going.
> Your agents already live in git. Hanji gives the humans a front and the sensitive folders a lock.
---
The rest of the story, in reading order:
1. [[The name]] - 한지, the paper this is named for.
2. [[Why Hanji is different]], how it lands differently from other tools.
3. [[Is Hanji for you?]], an honest look at fit, with better options where they exist.
4. [[Mission and vision]], why it exists and where it is going.
5. [[Principles]], the beliefs that break the ties.
6. [[The brand]] - the visual language and the voice, for anyone writing as Hanji.
---
---
title: Is Hanji for you?
order: 40
description: An honest look at who Hanji fits, and where something else fits better.
---
# Is Hanji for you?
A tool that claims to be right for everyone is lying to someone. Here is the honest version.
## Hanji fits you well if
- Your team already keeps things in git, and your agents already read code from it.
- You want people to read and write without ever learning git.
- You want to bring your own agent, and you refuse to pay by the token for the privilege.
- You have folders that must stay private, and you need that lock honored everywhere, including by the agents.
- You want to be able to walk away with everything, because it was always just your files.
That is the center of the target. A 10-to-200-person team, engineering-led, agent-heavy, already paying for a knowledge base its agents cannot read. If that is you, the rest of this handbook is written for you.
## You will be happier somewhere else if
Honesty about limits is one of our [[Principles]], so here it is turned on ourselves. If your need is in the left column, the tool in the middle will serve you better today, and we would rather point you to it.
| If you want | A better fit today | Why it wins there |
|---|---|---|
| One app for docs, tasks, databases, and a bundled AI | Notion | It plays the all-in-one game well. You trade portability for it, and often that trade is worth making. |
| A polished hosted wiki, and git or agents do not matter to you | Docmost, Outline | Excellent open-source wikis with a real team layer today. Hanji only pays off if the git-native part matters. |
| Personal notes on your own machine, just you | Obsidian | Hard to beat, and free for exactly that. Hanji is built for a team with a shared repo and a permission line down the middle. |
| To publish external product documentation | GitBook, Mintlify | Shaped for published docs, with the site polish and the SEO Hanji does not chase. |
| Enterprise SSO, audit, and a filled-in procurement checklist today | Confluence, or Hanji's paid tier later | Hanji is early. Those parts live on the paid tier, and some are still ahead of us. See [[Where we are]]. |
| Git and agents, but no human front and no permissions | The agent-first newcomers | They move fast in that space. Hanji exists because we wanted the human front and the lock too. |
## The short version
Hanji is the tool built to hold four things at once: git-backed storage, agent-native access, a polished human front, and real permissions. Need all four, and as of today nothing else does the whole set. Need only one or two, and one of the tools above will serve you better, and we would rather tell you that now than sell you the wrong thing.
No bundled model. No credits, ever. See [[Why Hanji is different]] for the longer argument.
---
---
title: Mission and vision
order: 50
description: Why Hanji exists, and where it is going.
---
# Mission and vision
The whole of it in one line: keep what a team knows on a foundation they own, readable by people and agents alike, free to walk away from, and good enough that they stay.
## The mission
Keep what a team knows in a form that outlasts the software holding it, and never hold it hostage to get there.
That is the entire job. A team's knowledge is one of the few things it builds that should still be readable in ten years, and it is usually trapped in the tool least likely to last that long. Hanji's mission is to break that trap. Put the knowledge somewhere durable and yours. Give people a surface calm enough to actually write on. Give agents a native way in. Lock the parts that need locking. If Hanji disappeared tomorrow, you would lose a nice reading experience and nothing else, because the knowledge was never inside it.
## The vision
Most tools grow by pulling more of your work inside their walls, so that leaving costs a little more every year. We want to grow the opposite way, by being worth keeping even though leaving is free.
The order matters, and it is the same order for every decision. Earn trust first. Charge for the parts an administrator wants, the hosted sync and the single sign-on and the per-folder controls a real organization needs. Never gate what one person needs to write. A student, a solo maintainer, a two-person team should get the whole writing experience for nothing, because the day you charge someone for the right to write down what they know is the day you have become the thing Hanji was built against.
Further out, the bet is bigger than a nicer wiki. Documentation is turning into something machines read as much as people do, and the team that owns its knowledge as clean, permissioned, git-native files hands its agents a sharper tool than any bundled chat box can be. The knowledge base stops being a place you visit and becomes infrastructure you own. That is the world Hanji is building toward.
See [[Where we are]] for what is shipped and what is next, and [[Why Hanji is different]] for the refusals that keep this honest.
---
---
title: Principles
order: 60
---
# Principles
A short list of the beliefs Hanji is built on. When a decision is hard, these break the tie.
- **Git is the store.** The repository is the source of truth. The index is a cache we can throw away and rebuild at any time. Anything we cannot rebuild from git, we do not trust.
- **The human never meets git.** Reading and writing should feel like a good editorial tool. Commits, branches, and conflicts are plumbing, and plumbing stays behind the wall until someone goes looking for it.
- **Agents are first-class readers.** More than half of documentation is now read by machines. Hanji treats an agent as a reader with an identity and a scope, the same as a person, and lets it propose rather than overwrite.
- **No bundled intelligence.** We do not sell you a model or meter your tokens. Bring the agent you already trust. Your bill does not grow because your team wrote more this month.
- **Permissions are honest about their limits.** We say plainly what a lock protects and what it leaves open. A promise we cannot keep is worse than no promise.
- **Byte-fidelity.** A one-word edit produces a one-line change. Your history stays readable because the tool refuses to rewrite what you did not touch.
None of these is novel on its own. Holding all of them at once, and refusing to trade one away for a feature, is the point.
---
---
title: The name
order: 20
---
# The name
한지 (hanji) is traditional Korean handmade paper. It is made from the inner bark of the mulberry tree, laid and pressed by hand, and it is known above all for one thing: it lasts.
Documents written on hanji have survived a thousand years. The oldest surviving woodblock print in the world sits on Korean paper from the eighth century, and it is still legible today. The paper was made to be written on, and made to keep what was written on it, which is a rarer pairing than it sounds, because most surfaces you can write on freely will not hold the writing for long, and most surfaces that endure a millennium were never meant to take ink in the first place. Hanji does both.
That is also the whole thesis of the product in a single word.
A knowledge base should be a surface that holds your writing for as long as you need it, in a form you can always read, on a foundation you control. Software comes and goes. Formats fall out of fashion, companies get acquired, apps stop shipping updates and quietly rot. The writing should not go down with them. The paper should outlast the app.
We kept the metaphor close in the craft too. The reading surface is warm, like a fresh sheet. The color is drawn from Korean tradition. The mark is a Korean letter, cut back to its bones. None of that is decoration. It is the same claim the paper makes, carried into the pixels: made by hand, meant to endure. See [[The visual language]].
---
---
title: Why Hanji is different
order: 30
description: What Hanji refuses to do, and why that is the point.
---
# Why Hanji is different
Open five knowledge tools and four of them share the same shape. The knowledge lives in a database only their app can read, intelligence comes bundled and metered so the more your team writes the more you pay, permissions are an afterthought bolted to the side, and leaving is a migration project because your writing was never really yours to carry.
Hanji is built against that shape, on purpose. Here is where it lands differently.
| | The common shape | Hanji |
|---|---|---|
| Where knowledge lives | A proprietary store, read through the app | Plain Markdown in a git repo you already own |
| Intelligence | A bundled model, metered by the token | Bring the agent you already trust. None bundled, nothing metered |
| Who reads it | People, with agents bolted on as a chat box | People and agents, both first-class, both scoped |
| Permissions | One flat space, or an afterthought | Two planes: an open human front, locked sensitive folders |
| Edit history | Opaque blocks, a diff you cannot read | Byte-fidelity: a one-word edit is a one-line change |
| Leaving | An export, a migration, a loss | Nothing to do. It was always your files in your git |
None of these axes is novel on its own. Plenty of tools keep files in git. A few take permissions seriously. The point is holding four things at once, git-backed storage, agent-native access, a polished human front, and real permissions, and refusing to trade any of them away for a feature. As of today, nothing else holds the whole set. That is the gap Hanji was built to sit in.
## Git is the store, not a backup
Most tools that "support git" treat it as an export target. The truth lives in their database, and git gets a copy when you remember to sync. Hanji reverses that. The repository is the source of truth, and the index is a cache we can throw away and rebuild at any time. That single choice is what makes leaving free. There is no export, because there was never anything to export from.
## Agents are readers, not a chat box
More than half of documentation today is read by a machine, not a person. Most tools answer that by adding a chat box that reads everything and replies in a blur. Hanji treats an agent the way it treats a person: a reader with an identity and a scope. It reads through git at the speed of a local clone, and when it wants to write, it proposes a branch. A person still decides. Your knowledge base does not quietly get rewritten by a model at 3am.
## The lock is honest about its limits
A two-plane model is simple to say and hard to do. The human front is open and inviting, the sensitive folders are locked, and every surface honors the lock. Search will not leak a title. A wikilink will not resolve to a page you cannot open. The agent tools see exactly what you see, nothing more. And where the lock has a limit, we say so plainly, because a promise we cannot keep is worse than none.
No bundled model. No credits, ever. That line is a promise, and every choice above is what keeps it.
Read [[Principles]] for the beliefs underneath these choices, [[Is Hanji for you?]] for an honest look at where something else fits better, and [[What Hanji is]] for how they fit together.