Skip to content
vadboshPublic

About

A Markdown knowledge base whose index is generated, not maintained by hand — every note typed by how it goes out of date. Works with or without an AI assistant.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Latest commit

 

History

158 Commits

Folders and files

Repository files navigation

kb

A knowledge base in plain Markdown. The agent updates the index itself as the session goes. Every note says how it goes out of date.

Point it at anything you would otherwise keep as a growing pile of .md files — a migration, an incident, a research thread, a renovation, a legal case. Nothing about it is specific to code.

Русская версия · Full manual: English · Русский · Changelog Using it from a terminal, without an assistant: CLI guide · по-русски

Just want to start? → Install, then Usage. Three steps.


The idea

Almost everyone ends up keeping a wiki for their own work. It is usually built the same way: a list of files, and beside it the files that the list points at. While there are few files, the list and the files agree.

Then files get added, renamed, split. Each of those changes means editing the list by hand. One day the list stops being edited. From that point the list and the files disagree, and it looks like this:

  • the "current status" section keeps a date nobody refreshed;
  • links lead to files that moved long ago;
  • a stack of 00-overview.md.bak.* grows beside the directory — one copy per attempt to put the list back in order.

We built a skill that solves this. It has three mechanisms. The first two keep the list from falling behind the files. The third fixes nothing: it shows you where the list has already fallen behind the files.

1. The index is generated, not maintained

Every note opens with three front-matter fields. This is the top of kb/02-pipeline.md:

---
title: how the pipeline works
kind: reference
updated: 2026-08-05
---
# the note itself follows

The index is kb/00-overview.md. The first /kb save creates it: headings, a rules section, a paragraph about what the work is. From then on that text is ordinary text — the assistant extends it as the work goes, or you edit it by hand.

Inside the index sits a region between two markers. That region, and only that region, kb rebuilds from the front matter of every note:

## Where to start reading          <- your text, kb never touches it

<!-- kb:begin -->                  <- everything down to kb:end is generated
Current snapshot: `21-state-2026-08-07.md` (2026-08-07).

| what you need            | file             | kind      | updated    |
|--------------------------|------------------|-----------|------------|
| how the pipeline works   | `02-pipeline.md` | reference | 2026-08-05 |
| traps and check commands | `23-traps.md`    | recipe    | 2026-08-07 |
<!-- kb:end -->

Prose again: what the work is, what does NOT go here, where else to look.

Nobody edits the snapshot line or the table — not you, not the assistant. Both are assembled from the files on every kb sync, and an edit inside the markers is lost. Such a table cannot disagree with the files: it came from them. Anything outside the markers kb never touches.

2. Knowledge is typed by how it expires

In an ordinary wiki every page is equal: nothing on the page tells you whether it describes how things are or how they were. Yet they expire differently — two files from the same directory:

  • 02-pipeline.md, "how the log pipeline works". True for months. When the pipeline changes the file is edited in place; the previous text is of no use to anyone.
  • 15-state-2026-08-05.md, "what is done and what is open as of 5 August". True for exactly that date. A week later it is not wrong — it is a historical fact. Rewriting it destroys evidence; a newer snapshot appears next to it while this one stays.

The first is maintained, the second is frozen and superseded. Put both in one undifferentiated list and the reader cannot tell which is which — and starts doubting both. kb makes you say which it is, in the kind field.

kind holds how it ages
charter why this exists, its boundaries, what is deliberately not built edited in place — one per kb
state a snapshot on a date superseded by a later one
plan what is planned, in what order superseded once executed
decision a choice and why; rejected alternatives never expires — a historical fact
reference how something works edited in place
recipe traps and ready-to-run checks edited in place

What the tool then gets for free:

  • Which snapshot is current is computed, not declared — the state with the latest date in its filename. Earlier ones are marked, never deleted: each still describes its own date truthfully.
  • A decision is never rewritten. Reversing one means writing a new decision that supersedes it, so the reasoning behind the old choice stays readable — usually the thing you actually needed six months later.

3. Two independent checks watch for staleness

kb check     mechanical: index vs files, dead links, missing front matter, size
kb verify    advisory:   paths that no longer exist, notes nobody revisited

Deliberately not merged. check is binary — exit 3 means go fix it. verify is probabilistic — exit 3 means go look. Merging them would drown an exact signal in guesses.

Neither claims to know whether a statement about the world is still true. A recipe can hold the command that would prove it; running that command against production on a schedule is not a note-taking tool's job.


Install

Requires Python 3.10+ and nothing else. No pip, no packages, no sudo.

git clone https://github.com/vadbosh/kb && cd kb

./install.sh              # Linux / macOS
./install.sh --dry-run    # print what would happen, change nothing
./install.sh --with-path  # also put kb on PATH, for running it by hand
.\install.ps1             # Windows
.\install.ps1 -DryRun
.\install.ps1 -WithPath

# if execution policy blocks the file:
powershell -ExecutionPolicy Bypass -File .\install.ps1

Both check Python before touching anything. If Python is missing or not on PATH, the installer stops and prints in colour what to do. On Windows that happens often: "Add python.exe to PATH" was left unticked during setup.

Everything lands in one directory per assistant:

<skills-dir>/kb/SKILL.md
<skills-dir>/kb/references/*.md
<skills-dir>/kb/scripts/kb        + kb.cmd launcher on Windows

Nothing depends on PATH or on ~/.local/bin existing — that directory is not a thing on Windows, which is also why the Windows installer writes a .cmd launcher (Windows does not act on a shebang line).

Only assistant directories that already exist are written to; --skills-dir <path> / -SkillsDir <path> overrides the detection. Manual install is just cp -r skills/kb <skills-dir>/ — the skill is self-contained.

Re-running an installer is a genuine no-op for unchanged files. A file it does replace is copied to ~/.local/state/kb-backups (on Windows %LOCALAPPDATA%\kb-backups) — but only when that content is not already in the repository. A hand edit is the one thing git checkout cannot give back; a copy of anything else would save what is already recoverable. The copy is never left beside the file: in a skills directory an assistant would load it as part of the skill. The three newest copies of each file are kept.


Usage

What you MUST do, in order:

  1. Install — ./install.sh (Linux/macOS) or .\install.ps1 (Windows). See Install.
  2. cd into your project and start your assistant there.
  3. Type /kb save. This is the step that creates the notes directory and writes the first note. Before it, the project has none — launching the assistant does not create one, and that is normal.

The directory is kb/, unless the project already has one of its own — kb is an ordinary name, and a directory of source, a mount or a placeholder may already be sitting there. Then kb refuses to write into a directory it did not make — empty or not — and hands you the choice:

  • ./.kb — the second name kb searches, so nothing has to be passed afterwards;
  • this directory after all — re-run with KB_ALLOW_EXISTING=1;
  • somewhere else — --dir <path>, which every later command then needs too.

From the second session on it makes no difference: the notes are found from anywhere inside the project, by either name.

After that, two commands are the whole workflow:

/kb restore    first thing when you sit down — loads the notes, says what changed
/kb save       any time something is worth keeping

That is all. You never run kb in a shell; the assistant does it for you.

A command name on its own works too — /kb check, /kb status, /kb list. The assistant runs that one command and reports what it said, without loading the notes or writing anything.

There is no /kbsave or /kbrestore — the skill is named kb, so the command is /kb and the half is chosen by the first word. Saying "kbsave" or "kbrestore" inside a sentence usually works as well, since both are listed in the skill's description, but that is recognition rather than a command.

What those two actually run
Command Underneath
/kb restore kb status + kb check + kb verify, then the index and the current snapshot, then only the files the task needs
/kb save kb status → decides append-or-new → kb add → writes the body → kb sync → kb check → kb verify

Adopting notes you already have

If a directory already holds a hand-written index and numbered files:

kb adopt              # preview: what moves, which titles it harvests
kb adopt --apply

It moves the notes into kb/, adds front matter, inserts the managed markers and builds the table. Titles are harvested from your existing table, not invented from each # H1. The phrasing a human wrote for a reader answers "what do I need"; a document title does not. Lose it and the point of the move is gone. Every touched file is backed up first.

Making the notes findable without kb

The notes get read by whoever runs /kb restore. Someone who does not know the notes exist will never run that command. A file at the project root, though, is what an agent reads every time — it starts there.

kb route

kb route writes two files into the project root. The first is AGENTS.md, holding the pointer to the notes, the charter and the current snapshot. The second is CLAUDE.md, holding one line: @AGENTS.md.

There are two files because Claude Code reads CLAUDE.md and does not read AGENTS.md, while Codex and Opencode read AGENTS.md. The text itself lives only in AGENTS.md; CLAUDE.md imports it.

/kb save calls kb route too — when it creates the kb/ directory in a project that has no AGENTS.md yet. Where AGENTS.md already exists, /kb save only offers to run kb route and touches the file itself not at all.

Between the kb:begin and kb:end markers is what kb derives from the notes. kb route and kb sync keep that block current. Outside the markers is your text: the check commands and the definition of done. An AGENTS.md without markers kb does not touch and refuses to write to. A CLAUDE.md without the import kb does not write to either — it prints the line for you to add by hand.

Both files reach the context window at every session start, so their combined length is capped at 200 lines — imports and CLAUDE.local.md included, since the host expands those at launch too. The cap is per launch directory: a session started in a subdirectory loads that directory's CLAUDE.md on top of every ancestor's, and kb route and kb verify report those chains as well. kb will not shorten them: outside the markers the text is yours.

Versioned, or only on this machine

Notes inside a git repository are committed by default: git add -A sweeps them in. kb says so once — when it creates the kb/ directory.

kb local          # .git/info/exclude — the notes stay here, and the
                  # entry point kb wrote goes with them
                  # or commit them, and they ship with the project

.git/info/exclude applies to one checkout, .gitignore to everyone who clones the repository. kb records its own choice nowhere: kb status asks git every time.


What kb deliberately does not do

  • No auto-splitting of long files. Size is reported, never enforced. A long coherent reference is a good file; where to cut is a question of meaning. kb outline prints a section map so the decision costs a dozen lines to read instead of fifteen hundred.
  • No database, no server, no lock-in. The notes are ordinary Markdown in your own repository. Delete the tool and they stay readable; only the table stops updating itself.
  • No opinion about your writing. Only the managed block is generated.

Two layers

Layer Owns Lives in
CLI kb everything mechanical — numbering, the table, drift detection, which snapshot is current <skill-dir>/scripts/kb
Skill kb everything needing judgement — new file or existing one, which kind, when a file has grown two questions your assistant's skills directory

The split is the design. Neither side does the other's job: the CLI never guesses what a note means, the assistant never hand-edits the table.

The skill has two halves, each loaded only when needed:

kb/SKILL.md               routing + the rules both halves share
kb/references/save.md     writing: append or new file, which kind, titles
kb/references/restore.md  reading: orient, be selective, brief
kb/references/entry-point.md  only when a command prints a ⚠ about AGENTS.md
kb/scripts/kb             the CLI — one copy, no PATH dependency

Written for assistants that read SKILL.md — Claude Code, Opencode, Codex. The CLI also works on its own if you prefer driving it by hand.


Commands

Command What
kb status where the kb is, file count, current snapshot, largest files
kb brief the overview and the current snapshot printed verbatim — the same bytes every run
kb check mechanical drift; exit 3 = go fix it
kb verify suspicions: vanished paths, notes past their re-check age, work that outran the notes
kb sync rebuild the index table from front matter
kb add <slug> --kind <k> --title "…" next numbered note + front matter + rebuild
kb outline [file] section map with weights — where the seams for a split are
kb list [--scan DIR] [--prune] every kb known on this machine
kb streams [--sessions N] where this session stands, then which directories it touched — read from the transcript
kb adopt [--apply] [--in-place] migrate a hand-made notes directory
kb hook --install git pre-commit that refuses a commit on exit 4
kb route AGENTS.md at the project root so agents find the notes without kb; CLAUDE.md imports it
kb local [--dry-run] keep the notes on this machine (.git/info/exclude)
--dir X operate on X instead of ./kb or ./.kb

Configuration

Variable Default Controls
KB_REGISTRY $HOME/.local/state/kb/registry.txt where the list of known kb directories lives
KB_DOC_DIR $HOME/.kb-docs install target for the manual
KB_BIN_DIR $HOME/.local/bin · %LOCALAPPDATA%\kb\bin install target for the optional PATH copy

The frame is English, always; the writing is yours. Everything kb writes into a note — the table header, the "current snapshot" line, the skeletons, the marker notes — is English, and nothing configures it. Everything a person writes — title:, note bodies, the prose outside the markers, the filled sections of the entry point — is in the language the work is discussed in, and kb never translates or inspects it. A Russian note under an English frame is the designed outcome.

A kb made before 5.0.0 may carry another frame (kb:begin lang=ru). kb check reports its index as stale, and the next kb sync rewrites what sits between the markers in English — in place, once, with a frame: ru → en line. Headings a scaffold wrote outside the markers stay as they are: from then on they are the human's text.

No path anywhere is hardcoded to a particular machine.


Credentials

kb check scans the notes for credentials on every run — which means on every /kb save, since the skill calls it there. Two layers, and the report always says which of them ran:

secrets: none found — checked with built-in patterns + trufflehog 3.95.3
secrets: none found — checked with built-in patterns only
         install gitleaks or trufflehog for the full ruleset

Layer 1, always — nineteen patterns, no install, works everywhere. Fifteen match a shape with an unmistakable prefix or header: AWS access keys, GitHub, GitLab, Slack, Google, Stripe, Tavily, OpenAI-family and Atlassian tokens, PEM private keys, JWTs, passwords inside connection URLs, IDENTIFIED BY '…' in SQL.

Four more require the variable name and the value shape together, because the secret itself has no prefix — an AWS secret access key is forty characters of base64 and nothing else, and a Huawei or OpenStack one is the same problem. aws_secret_access_key = <40 base64> has no innocent reading; a bare forty-character token has many. Each of these was measured against both live note directories before being added: zero matches, so they do not collide with ordinary text.

Every pattern here arrives with a test naming the shape, including the shapes that must not fire. A list like this is maintained by comparing it against other people's, and an import is worth nothing without the negative case. For example, the model name zai-sk-glm-4-6-turbo-preview was read as an OpenAI key. A test caught it — one that asserts a model name is not a finding. The pattern itself had let it through.

Layer 2, when present — gitleaks, trufflehog or detect-secrets, whichever is found on PATH first, run over the same directory. Roughly 150 rules instead of nineteen. Missing is not an error; it narrows coverage, and the report says so. Only kb check runs it. kb brief runs at every session start, and gitleaks spends about two seconds starting up, so brief uses layer 1 only.

A finding exits 4, distinct from 3 for ordinary drift, so a hook or a pipeline can tell "the table is stale" from "there is a key in a note".

What the scan does not catch

An entropy pass was written for this and then removed. On two real note directories it produced four findings, all four false: a file path, a config value, a Kubernetes pod name, another path. Identifiers and paths are long and look random; no threshold separated them from a token. A report at that ratio stops being read within days.

So a short generic password sitting in a sentence is caught by nothing here — not by the built-in patterns, not by gitleaks, not by trufflehog. The author of gitleaks says as much about MyServiceToken=EXAMPLE_VALUE. This lowers the risk; it does not replace not writing credentials down.

Testing the scan

A credential with a correct format is indistinguishable from a live one, both to this scan and to everyone else's. Do not commit sample credentials as test fixtures, even fake ones: GitHub's own secret scanning will flag them, and so will the scanners of everyone who clones the repository. Generate them inside the test from a template instead, so no valid-looking value is ever stored in a file.

The same applies to the notes themselves. A fake key pasted into a note "to see what happens" is a real finding as far as every tool is concerned.

Blocking a commit

kb hook --install     # git pre-commit; refuses a commit on exit 4

Only meaningful when the notes are inside a git repository. Plenty are not — in that case the control that applies is kb check at the end of every save, which runs regardless.


Design notes

Worth knowing before extending it:

  • Numbering gaps are never reused. A number that once pointed at a file may still be cited from a commit message or another note.
  • Row order is by number, so editing one file does not reshuffle the table and diffs stay small.
  • verify checks a path only when both conditions hold: it is written in backticks, and its root exists on disk. Measured on a real 19-file kb: without them you get 271 flags of which about two are real — URL paths, API versions, fragments of longer paths. With them: 6 flags, 2 real. At the first ratio the report stops being read within days. The cost is that a path inside a fenced block is not checked. ~/… counts as absolute; the notes, the overview and the entry point are all scanned.
  • Size thresholds are 400 and 1200 lines. The familiar "keep it under 200 lines" rule is about instruction files, which load into context on every turn. kb files are read on demand and one at a time, so that rule does not apply to them.
  • Front matter is parsed without a YAML dependency — flat key: value pairs between --- fences, split on the first colon so that a title may contain a colon of its own.

Tests

tests/run.sh                      # every group in parallel, about 30 s on 4 cores
python3 tests/test_kb.py          # the same tests one after another, about 2 min
python3 tests/test_kb.py -v       # one line per test
python3 tests/test_kb.py Guards   # one group

The tests need the Python standard library and nothing else. Each run gets a temporary directory with its own HOME and its own KB_REGISTRY, so real notes and the real registry are never touched.

A test covers one of two things:

  • a guard the tool exists to enforce — writing outside the current directory, into $HOME, into a directory on PATH;
  • a bug that shipped once — its version is named in the test.

What is deliberately not covered is whether a note is useful: that was attempted, measured against nineteen real notes, and abandoned, because no mechanical signal separated the adequate ones from the inadequate.


Uninstall

rm -rf <skills-dir>/kb
rm -f  ~/.local/bin/kb        # only if --with-path was used
rm -rf ~/.local/state/kb      # the registry
rm -rf ~/.kb-docs

Your notes are untouched — they were never inside the tool.


Sending a change

Commit messages are written in English, body included. The history is the only place the reason for a change survives, and whoever can read the code can read the message. A message that quotes Russian — a heading being renamed, a word being replaced — keeps the quotation: there the Russian is the subject.

The documentation itself is bilingual and stays that way: README.md and README.RU.md are edited as a pair.

License

MIT.

About

A Markdown knowledge base whose index is generated, not maintained by hand — every note typed by how it goes out of date. Works with or without an AI assistant.

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages