zettel
Zettel is a zettelkasten knowledge base for code repositories. It keeps atomic
notes as markdown files in .zettel/. Notes link to each other through YAML
frontmatter and inline [[id]] references. Git tracks the notes next to the
code, and no external service holds them.
During research an agent reads code, traces bugs, and compares options. That work is usually lost at the end of the session. Zettel writes it down as notes labeled with their provenance: who produced each piece of text, and what kind of claim it makes. A human reviews the agent content later and approves what they stand behind.
Install
The command is zettel, whichever package you install.
Archives of precompiled binaries are available for macOS and Linux. The Linux binaries are static executables. Each archive holds the binary, its man pages, the README and the licence. There is no Windows build.
| Package manager | Package | Command |
|---|---|---|
| Homebrew | cjohnhanson/tap/zettel | brew install cjohnhanson/tap/zettel |
| Cargo | zttl | cargo install --locked zttl |
| uv | zttl | uv tool install zttl |
| npm | @cjohnhanson/zttl | npm install -g @cjohnhanson/zttl |
On Debian or Ubuntu, download the .deb from the releases
page and install it:
wget https://github.com/cjohnhanson/zettel/releases/download/v0.3.0/zttl_0.3.0-1_amd64.deb
sudo dpkg -i zttl_0.3.0-1_amd64.deb
To run it once without installing anything:
uvx zttl read
npx @cjohnhanson/zttl read
Building
zettel is written in Rust, so you need a Rust
installation to compile it. zettel compiles
with Rust 1.88 or newer. A C compiler is needed as well, which
aws-lc-sys uses for its cryptography.
To build zettel:
git clone https://github.com/cjohnhanson/zettel
cd zettel
cargo build --release
./target/release/zettel --version
Usage
zettel init # run inside a git repo
zettel note create "Connection pooling causes stale reads" \
--tag bug,postgres --body "Pool reuses sockets after failover..."
zettel note list --tag postgres
zettel search "stale read"
zettel context a3f2 --depth 2
Every command:
zettel init # make .zettel/ in a git repo
zettel note create "Title" -t a,b -p agent:summary # make a note; set tags and provenance
zettel note list [--tag t] [--provenance p] # list the notes
zettel note list --unreviewed # list the notes with unreviewed agent content
zettel note show <id> # show the full content of one note
zettel note edit <id> --add-link b7c1 # link one note to another
zettel note review <id> [--approve all] # list provenance spans; approve agent content
zettel note delete <id> # remove a note
zettel read [--tag t] [--provenance p] # show the content of the matching notes/spans
zettel search <pattern> # search all notes with a regex
zettel backlinks <id> # show the notes that link to this note
zettel context <id> --depth N # show the notes within N hops
zettel orphans # show the notes with no links
zettel store list # show this store and the stores it declares
zettel store sync # fetch the declared remote stores into the cache
zettel store root [<path>] # show or set the store that reads fall back to
zettel check # check for broken links and invalid provenance
zettel migrate # convert pre-provenance notes (status keys)
zettel stats # show counts, tag distribution, and connectivity
zettel serve [--bind ADDR] [--access MODE] # serve this knowledge base over MCP
zettel docs [topic] # show the bundled documentation
zettel prime # print what zettel is, for an agent's context
How it works
Each note is one markdown file with YAML frontmatter:
---
title: Connection pooling causes stale reads
tags: [bug, postgres]
links: [b7c1]
provenance: agent:summary
---
After failover, the pool reuses sockets bound to the old primary.
See [[b7c1]] for the workaround.
<!-- prov agent:inference -->
The 2026-08-02 retry storm probably started here.
<!-- /prov -->
<!-- prov human:cody -->
Confirmed with the infra team.
<!-- /prov -->
The note ID is the filename stem, so the frontmatter does not repeat
it. Zettel keeps the notes in one flat .zettel/ directory:
.zettel/
a3f2-connection-pooling-causes-stale-reads.md
b7c1-workaround-force-new-connection.md
Notes connect through the frontmatter links field and inline [[id]]
references. Zettel walks the note graph. It computes backlinks, finds the
orphan notes, shows the neighborhood of a note, and checks for broken links.
Every piece of text has a provenance. The frontmatter provenance: key sets
the note default; a <!-- prov ... --> marker overrides it for one section.
The origins:
human[:name]— a person wrote it.agent[:kind]— an agent wrote it: asummaryof sources, anindex, or aninference(a new claim not present in the sources).citation[:source]— quoted verbatim; the source is a note ID or asrc=<url>attribute. A citation of a note is a link-graph edge.- No provenance means unknown. Unknown is never upgraded to human, so an agent that forgets the label cannot mint human-authored text.
A human approves agent content with zettel note review <id> --approve,
which writes a reviewed= stamp. A later reader filters by all of this:
zettel read --provenance human,citation,reviewed returns only the text a
human wrote, quoted, or vouched for. Nothing verifies a label against what
actually wrote the span. Reading the files directly gives the same labels
and the same caveat.
Composed stores
A knowledge base can link into others. Declare them in stores.yml:
format: 2
stores:
- alias: project
path: ../project # this machine
- alias: handbook
git: https://example.com/org/handbook # a git repository
- alias: archive
blob: https://example.com/notes # an https prefix with an index.txt
zettel store sync fetches the remote stores into a local cache. It is
the only command that reaches the network.
A reference then names the store. Write [[project:a3f2]] in a body,
project:a3f2 in links:, or citation:project:a3f2 in a provenance
marker. A reference with no alias stays local.
The declarations set the direction. A personal knowledge base declares the repositories that it annotates. A repository does not declare the personal knowledge base. It cannot: the target does not exist for the other users who clone the repository. Two repositories can declare each other, because each one is equally reachable.
Each command runs from one store. It reads that store and the stores that the store declares. Thus one note has different backlinks in different stores. Dependency stores are read-only.
Serving over MCP
zettel serve offers the same knowledge base to a Model Context
Protocol client. The library answers both interfaces, so the server
returns what the CLI returns.
zettel serve # speak MCP on stdin and stdout
zettel serve --bind 127.0.0.1:7431 # serve over HTTP for a client that connects
zettel serve --access read-write # also allow note creation
The server is read-only by default. With --access read-write it
stamps every note it writes as agent-written. It refuses any other
provenance, and it never exposes approval. A served store has no
authentication: bind it to 127.0.0.1, or put a proxy that
authenticates in front of it.
Documentation
- What is Zettel? — the zettelkasten model, the note format, the workflow
- Getting started — a walkthrough of the first notes
- CLI reference — the complete command documentation
Run zettel docs to read the same documentation from the binary.
Related
- tisket — issue tracker. Markdown issues with YAML frontmatter, in the repository
- almanac — agent skill index, over pluggable sources
- gaff — context-lifecycle handler for coding agents
- missouri — end-to-end tests as directed graphs of filesystem states
- mdstore — the frontmattered markdown library zettel stores notes with
License
MIT.
Metadata
Release files for zttl 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| zttl-0.3.0.tar.gz | 112.2 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| zttl-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl | Python 3 | none | Linux musl 1.1+ x86-64, Linux glibc 2.17+ x86-64 | Details |
| zttl-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64, Linux musl 1.1+ ARM64 | Details |
| zttl-0.3.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| zttl-0.3.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 32.3 MB
Release files / zttl-0.3.0.tar.gz
| Download URL | zttl-0.3.0.tar.gz |
|---|---|
| Size | 112.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a9bd5ede81a6eda0a466c5f59e6600b5ce2b67148b58223639dbeff6c5521bc7
|
|
BLAKE2b-256 checksum How to use checksums |
bf79c00a7ab344c3fb03abc5ff4de7501ace9a3e610986af0861f36fe50f2537
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / zttl-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
| Download URL | zttl-0.3.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl |
|---|---|
| Size | 8.5 MB |
| Tags | Linux glibc 2.17+ x86-64 Linux musl 1.1+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
d67113ce33f049bdbc39ac0947948557eeb88180ea07407949f9b7f7e2bd5d56
|
|
BLAKE2b-256 checksum How to use checksums |
5baf64615828e53456dc68c77488b8189b599fd0d8be2e6f83469927087c8aa4
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / zttl-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
| Download URL | zttl-0.3.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl |
|---|---|
| Size | 7.9 MB |
| Tags | Linux glibc 2.17+ ARM64 Linux musl 1.1+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
e59e4efc0f9399f009d36170a27eaeaf4548f816cb2d74a4955bd9380973a56d
|
|
BLAKE2b-256 checksum How to use checksums |
611de8acdb2a5bbe96f28be00d14d720bdc3171bd8c51c0d0a6c827669f88d42
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / zttl-0.3.0-py3-none-macosx_11_0_arm64.whl
| Download URL | zttl-0.3.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 7.8 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
b9ec924bd25e4f78bf6d3fbb35e4a45281f62ef989626e9649cd324a498e1905
|
|
BLAKE2b-256 checksum How to use checksums |
ad3fbb87038037af6c31c924b891edffd9cff6f549ce4ff83b2356024feb95b5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / zttl-0.3.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | zttl-0.3.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 8.0 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
598ca09b6e93eb66f10e33a4d50573c8971ef7a3b71354e2e922fdc85538c657
|
|
BLAKE2b-256 checksum How to use checksums |
043c86d2d05577f71d08bd3c548e372f277e81cafe0d9ecce0c014d4158326df
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.13.0 {"installer":{"name":"uv","version":"0.13.0","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|