Skip to main content

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 package is zttl, because zettel was taken on every registry. On npm it is @cjohnhanson/zttl, because the registry refuses zttl as too close to names it already holds. The command is zettel, and both names install together.

cargo install --locked zttl
brew install cjohnhanson/tap/zettel
uv tool install zttl
npm install -g @cjohnhanson/zttl

cargo install builds from source. It needs Rust 1.88 and a C compiler. The other three carry a prebuilt binary for macOS and Linux, x86-64 and arm64, published by a tagged release.

To build the unreleased main branch:

cargo install --locked --git https://github.com/cjohnhanson/zettel

Or run it without installing:

uvx zttl read
npx @cjohnhanson/zttl read

A release also carries prebuilt archives and a .deb, on the releases page. Each archive holds the binary and the man page. Install a .deb with dpkg -i: it is a file, not a repository, so apt-get install does not reach it.

Check the install with 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: a summary of sources, an index, or an inference (a new claim not present in the sources).
  • citation[:source] — quoted verbatim; the source is a note ID or a src=<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

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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

zttl-0.2.5.tar.gz (98.0 kB view details)

Uploaded Source

Built Distributions

If you're not sure about the file name format, learn more about wheel file names.

zttl-0.2.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl (8.5 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ x86-64musllinux: musl 1.1+ x86-64

zttl-0.2.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl (7.9 MB view details)

Uploaded Python 3manylinux: glibc 2.17+ ARM64musllinux: musl 1.1+ ARM64

zttl-0.2.5-py3-none-macosx_11_0_arm64.whl (7.8 MB view details)

Uploaded Python 3macOS 11.0+ ARM64

zttl-0.2.5-py3-none-macosx_10_12_x86_64.whl (8.0 MB view details)

Uploaded Python 3macOS 10.12+ x86-64

File details

Details for the file zttl-0.2.5.tar.gz.

File metadata

  • Download URL: zttl-0.2.5.tar.gz
  • Upload date:
  • Size: 98.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

File hashes

Hashes for zttl-0.2.5.tar.gz
Algorithm Hash digest
SHA256 b132bc7597167066e1b52ba8b2a7dde35551e88ca48c69709aa3c28d80dc0229
MD5 8f39c34121e02d46d3eb37b025858d72
BLAKE2b-256 9f1fb6192e611e63f933837a67bb0cf86bf5be820d73f2270f17544e41200df5

See more details on using hashes here.

File details

Details for the file zttl-0.2.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl.

File metadata

  • Download URL: zttl-0.2.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
  • Upload date:
  • Size: 8.5 MB
  • Tags: Python 3, manylinux: glibc 2.17+ x86-64, musllinux: musl 1.1+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

File hashes

Hashes for zttl-0.2.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.musllinux_1_1_x86_64.whl
Algorithm Hash digest
SHA256 78de3a56416dcabd8a60e1e7f15bdb67b0717b577b454464b41e2265b2d09898
MD5 441cbb7a47294483d3395eb6fcbbb239
BLAKE2b-256 044d2a7f9d128ad23726faf9ea6dbda8d330c19928964b2faf63296f22c4511f

See more details on using hashes here.

File details

Details for the file zttl-0.2.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl.

File metadata

  • Download URL: zttl-0.2.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
  • Upload date:
  • Size: 7.9 MB
  • Tags: Python 3, manylinux: glibc 2.17+ ARM64, musllinux: musl 1.1+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

File hashes

Hashes for zttl-0.2.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl
Algorithm Hash digest
SHA256 3e897a737b545cab487027fdf2f9f16dae42707d157b64a0a842b01649f72e66
MD5 0e9e47a0b9c1a29f97d1695bd9bf34e1
BLAKE2b-256 3354ee96b72e6d190b498e914f89524d02ff1ebd4af9f1b2ca05c1e9a4c6a3cf

See more details on using hashes here.

File details

Details for the file zttl-0.2.5-py3-none-macosx_11_0_arm64.whl.

File metadata

  • Download URL: zttl-0.2.5-py3-none-macosx_11_0_arm64.whl
  • Upload date:
  • Size: 7.8 MB
  • Tags: Python 3, macOS 11.0+ ARM64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

File hashes

Hashes for zttl-0.2.5-py3-none-macosx_11_0_arm64.whl
Algorithm Hash digest
SHA256 5754c1bc1fe2e54863c1757526aa52ce72e9e1abdfb5da88054f0e296d773105
MD5 221809c450b3a64d6f7ab504b7c1a899
BLAKE2b-256 aa46992151acbba0252b51d51e709fb419fd999ff5e1629314dd577c8c6cd609

See more details on using hashes here.

File details

Details for the file zttl-0.2.5-py3-none-macosx_10_12_x86_64.whl.

File metadata

  • Download URL: zttl-0.2.5-py3-none-macosx_10_12_x86_64.whl
  • Upload date:
  • Size: 8.0 MB
  • Tags: Python 3, macOS 10.12+ x86-64
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.12.13 {"installer":{"name":"uv","version":"0.12.13","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}

File hashes

Hashes for zttl-0.2.5-py3-none-macosx_10_12_x86_64.whl
Algorithm Hash digest
SHA256 e767ebd6c3b4f9a65f12d4b4d890635b922c5294419708a1707ac7e8de976b69
MD5 79d0293bd3725c3c041fe6f6156451b2
BLAKE2b-256 0771bc106ceedbce1f52fa50cd9d22b06c24b152eb375c5a26f939dd4b9103e7

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.5 This release

5 files

0.2.4

5 files

0.2.3

5 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page