# Building an LLM Wiki for Your Project — a step-by-step guide (agent-maintained knowledge base: schema, ingest/query/lint workflows, qmd search, lint script)

> Source: <https://gist.github.com/quangyendn/5683375a47d9e5cc1c90c43b97d3849a>
> Published: 2026-08-22 04:26:46+00:00

This guide shows you how to set up an **LLM Wiki** in your own project: a persistent, interlinked markdown knowledge base that an LLM agent (Claude Code, Codex, etc.) builds and maintains for you. It is based on [Andrej Karpathy's "LLM Wiki" pattern](https://gist.github.com/karpathy/442a6bf555914893e9891c11519de94f).

You can go from nothing to a working wiki in ~15 minutes (Parts 1). Search and linting (Parts 2–3) are optional add-ons. Part 4 and the Use Cases show how to actually drive it day to day.

Most people use LLMs with documents via **RAG**: upload files, the model retrieves chunks at query time, answers, and forgets. Nothing accumulates — every question rediscovers knowledge from scratch.

An **LLM Wiki** is different. Instead of re-deriving on every query, the LLM **incrementally builds and maintains a wiki** — a folder of cross-linked markdown pages that sits between you and your raw sources. Add a source, and the agent reads it, updates the relevant pages, flags contradictions, and files it. The knowledge is *compiled once and kept current*.

Three layers:

| Layer | What it is | Who owns it |
|---|---|---|
Raw sources (`raw/` ) |
Immutable inputs — articles, transcripts, screenshots, page dumps, exported docs. Read-only. | You (curate) |
The wiki (`wiki/` ) |
LLM-generated, cross-linked markdown pages. | The agent (writes & maintains) |
The schema (`WIKI.md` ) |
Conventions + the ingest/query/lint workflows the agent follows. | You + agent (co-evolve) |

Plus two navigation files: `index.md`

(a catalog) and `log.md`

(an append-only history).

**Division of labor:** you do sourcing, exploration, and asking good questions. The agent does the grunt work — summarizing, cross-referencing, filing, and bookkeeping. You (almost) never write the wiki by hand.

**git**— the wiki is just a folder of markdown; version it.** A coding agent**— Claude Code, Codex CLI, OpenCode, etc. This is the "engine."*(Optional)***Node.js ≥ 22**— only if you want the lint script (Part 3).*(Optional)*— a local, on-device search engine for markdown (Part 2). Install with[qmd](https://github.com/tobi/qmd)`npm install -g @tobilu/qmd`

. Not needed until your wiki grows past a few hundred pages —`index.md`

alone works well at small/medium scale.

Where to put the wiki.A dedicated top-level folder (e.g.`llm-wiki/`

) with itsown git repois the cleanest choice, especially if your project root is not itself a repo, or if you don't want wiki churn mixed into your product's history.

```
mkdir -p llm-wiki/raw \
         llm-wiki/wiki/domains \
         llm-wiki/wiki/screens \
         llm-wiki/wiki/entities \
         llm-wiki/wiki/concepts \
         llm-wiki/scripts
cd llm-wiki
```

Adjust the `wiki/`

subfolders to your domain. Common choices:

`domains/`

— top-level areas of your subject (the main axis).`entities/`

— the "nouns" (people, objects, records, components).`concepts/`

— cross-cutting ideas.`screens/`

(for apps) /`modules/`

(for codebases) /`sources/`

(for research).

This is the most important file. It turns a generic chatbot into a disciplined wiki maintainer. Every agent session reads it first. Copy this template and adapt the bracketed parts:

```
# WIKI.md — Schema & Workflows

> Read this file FIRST before ingesting, querying, or linting. It defines how
> this wiki is structured and the workflows to follow.

## 1. Three layers
- `raw/` — immutable sources, read-only. Never edit after saving.
- `wiki/` — LLM-generated pages. The agent owns these.
- `WIKI.md` + `index.md` + `log.md` — schema, catalog, history.

**Roles:** the human curates sources and asks questions; the LLM does all
summarizing, cross-referencing, filing, and bookkeeping.

## 2. Navigation: index-first
When answering, read `index.md` FIRST to find relevant pages, then read them.
`index.md` is enough at moderate scale. (Optional: use `qmd` for semantic search
when the wiki grows large — it augments, never replaces, `index.md`.)

## 3. Page organization
- `wiki/overview.md` — the map of the whole wiki.
- `wiki/domains/<slug>.md` — [your main axis].
- `wiki/entities/<slug>.md` — [your nouns].
- `wiki/concepts/<slug>.md` — [cross-cutting ideas].

## 4. Page conventions
- Slugs are kebab-case ASCII. Filename = slug + `.md`.
- Every page starts with YAML frontmatter:
  ``` yaml
  ---
  title: <human title>
  type: overview | domain | entity | concept
  tags: [<tag>, ...]
  sources: [raw/<path>/, ...]   # provenance
  updated: YYYY-MM-DD
  ---
  ```
- Link liberally with `[[slug]]` (or `[[slug|label]]`). Linking to a page that
  doesn't exist yet is fine — it becomes a to-do (the linter flags it).
- **Never hard-wrap prose. One paragraph, list item, or table cell = one line**,
  however long. Never break mid-sentence to shorten a line; the editor
  soft-wraps. Only exception: inside a code fence. Enforce with
  `npx prettier --write <file>` and a `.prettierrc` holding
  `{ "proseWrap": "never", "embeddedLanguageFormatting": "off" }`.

## 5. The three workflows

### Ingest (add one source)
1. Read the source in `raw/`.
2. Discuss the key takeaways with the human; ask what to emphasize.
3. Create/update the primary page for this source.
4. Propagate to related domain/entity/concept pages; add `[[cross-links]]`.
5. Update `index.md` (add/adjust the line for each new page).
6. Append to `log.md`: `## [YYYY-MM-DD] ingest | <title>`.
7. Ingest one source at a time, supervised. One source may touch 10–15 pages.

### Query (ask the wiki)
1. Read `index.md` first (use `qmd query` for deep search if configured).
2. Read the relevant pages; answer WITH citations.
3. File valuable answers back as new wiki pages so explorations compound.

### Lint (health check)
1. Mechanical: run the lint script (broken links, orphans, missing-in-index).
2. Semantic (the agent): contradictions, stale claims, concepts mentioned but
   lacking a page, missing cross-references, data gaps.
3. Suggest new questions to investigate and sources to add.
```

`index.md`

:

```
# Index

> Read this first. Each page: a link + a one-line summary. Updated on every ingest.

## Overview
- [overview](wiki/overview.md) — the map of the wiki.

## Domains
_(grows as you ingest)_

## Entities
_(grows as you ingest)_

## Concepts
_(grows as you ingest)_
```

`log.md`

:

```
# Log

> Append-only. Each entry starts `## [YYYY-MM-DD] <op> | <title>` so it greps:
> `grep "^## \[" log.md | tail -5`

## [2025-01-01] init | Scaffolded the wiki
```

`wiki/overview.md`

— a seed page describing your subject (even a rough one; the agent will enrich it):

```
---
title: Overview
type: overview
tags: [overview]
sources: []
updated: 2025-01-01
---

# Overview

[2–3 sentences on what this wiki covers.]

## Map
_(domains grow here as you ingest)_
git init
printf '.qmd-home/\n.DS_Store\n' > .gitignore   # ignore qmd's local index (Part 2)
git add -A
git commit -m "chore: scaffold llm-wiki"
```

**You now have a working LLM Wiki.** You can stop here and start ingesting (Part 4). Parts 2–3 add search and linting.

`index.md`

is enough to start. Add [qmd](https://github.com/tobi/qmd) when you want semantic/hybrid search over a larger wiki. qmd runs fully **on-device** (local GGUF models, no API key).

```
npm install -g @tobilu/qmd      # or: bun install -g @tobilu/qmd
qmd --help
```

qmd stores its index in a global cache by default (`~/.cache/qmd/index.sqlite`

). To keep **this** wiki's index separate from any other qmd use, point qmd's `XDG`

dirs at a local folder. Save this as `scripts/qmd-wiki.sh`

:

``` bash
#!/usr/bin/env bash
# Run qmd with an index ISOLATED to this wiki (won't touch your global qmd index).
set -euo pipefail
WIKI_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
export XDG_CACHE_HOME="$WIKI_ROOT/.qmd-home/cache"
export XDG_CONFIG_HOME="$WIKI_ROOT/.qmd-home/config"
# Optional: pick an embedding model. For non-English / CJK content, Qwen3 is far better:
export QMD_EMBED_MODEL="hf:Qwen/Qwen3-Embedding-0.6B-GGUF/Qwen3-Embedding-0.6B-Q8_0.gguf"
exec qmd "$@"
chmod +x scripts/qmd-wiki.sh
mkdir -p .qmd-home/cache/qmd
# Share the (large) model cache with your global qmd, if present, to avoid re-downloading:
[ -d "$HOME/.cache/qmd/models" ] && ln -s "$HOME/.cache/qmd/models" .qmd-home/cache/qmd/models
```

Why a wrapper?It bundles the isolation env + model choice into one command you (and the MCP server) always use. Verify isolation with`./scripts/qmd-wiki.sh status`

— the`Index:`

line must point at`.qmd-home/`

, not`~/.cache`

.

```
./scripts/qmd-wiki.sh collection add ./wiki --name wiki
./scripts/qmd-wiki.sh context add qmd://wiki "What this wiki covers (one sentence)."
./scripts/qmd-wiki.sh embed        # first run downloads the model (~600MB); on-device
./scripts/qmd-wiki.sh query "your first question"
```

Re-run `embed`

after each batch of ingests. Search commands:

`qmd-wiki.sh search "term"`

— fast BM25 keyword.`qmd-wiki.sh vsearch "meaning"`

— semantic.`qmd-wiki.sh query "question"`

— hybrid + rerank (best).

So the agent can search natively instead of shelling out. For Claude Code, add to `.mcp.json`

:

```
{
  "mcpServers": {
    "qmd-wiki": {
      "command": "/absolute/path/to/llm-wiki/scripts/qmd-wiki.sh",
      "args": ["mcp"]
    }
  }
}
```

Pointing the MCP command at the wrapper ensures the server uses the isolated index. **Restart your agent** for MCP changes to take effect.

Mechanical checks are cheap and deterministic — let a script do them so the agent can focus on semantic linting. Save this as `scripts/lint.mjs`

(Node ≥ 22, zero dependencies):

``` js
import { readdirSync, readFileSync, existsSync } from 'node:fs';
import { join, relative, basename } from 'node:path';
import { pathToFileURL } from 'node:url';

const WIKILINK = /\[\[([^\]|]+)(?:\|[^\]]*)?\]\]/g;

export function lintWiki(root) {
  const wikiDir = join(root, 'wiki');
  const indexPath = join(root, 'index.md');
  const files = existsSync(wikiDir)
    ? readdirSync(wikiDir, { recursive: true }).map(String)
        .filter((f) => f.endsWith('.md')).map((f) => join(wikiDir, f))
    : [];

  const slugOf = (p) => basename(p, '.md');
  const pages = new Map();
  for (const f of files) pages.set(slugOf(f), f);

  const brokenLinks = [];
  const inbound = new Map();
  for (const slug of pages.keys()) inbound.set(slug, 0);

  for (const f of files) {
    readFileSync(f, 'utf8').split('\n').forEach((line, i) => {
      const re = new RegExp(WIKILINK.source, 'g');
      let m;
      while ((m = re.exec(line))) {
        const target = m[1].trim();
        if (pages.has(target)) inbound.set(target, inbound.get(target) + 1);
        else brokenLinks.push({ file: relative(root, f), line: i + 1, target });
      }
    });
  }

  const orphans = [];
  for (const [slug, count] of inbound)
    if (count === 0 && slug !== 'overview')
      orphans.push({ slug, file: relative(root, pages.get(slug)) });

  const indexText = existsSync(indexPath) ? readFileSync(indexPath, 'utf8') : '';
  const missingInIndex = [];
  for (const [slug, f] of pages)
    if (!indexText.includes(slug)) missingInIndex.push({ slug, file: relative(root, f) });

  return { brokenLinks, orphans, missingInIndex };
}

// CLI — skipped when loaded by the test runner
if (!process.env.NODE_TEST_CONTEXT && process.argv[1] &&
    import.meta.url === pathToFileURL(process.argv[1]).href) {
  const root = process.argv[2] || '.';
  const r = lintWiki(root);
  let problems = 0;
  const section = (title, arr, fmt) => {
    if (arr.length) { problems += arr.length; console.log(`\n${title} (${arr.length}):`);
      for (const x of arr) console.log('  - ' + fmt(x)); }
  };
  section('Broken [[links]]', r.brokenLinks, (x) => `${x.file}:${x.line} → [[${x.target}]]`);
  section('Orphan pages', r.orphans, (x) => x.file);
  section('Missing in index.md', r.missingInIndex, (x) => `${x.file} (slug: ${x.slug})`);
  if (problems === 0) { console.log('✓ Lint clean.'); process.exit(0); }
  console.log(`\n✗ ${problems} problem(s).`); process.exit(1);
}
```

Run it with `node scripts/lint.mjs .`

. It exits non-zero when there are problems, so you can wire it into CI or a pre-commit hook.

Tip:keep a companion`scripts/lint.test.mjs`

using Node's built-in`node:test`

. Run tests with`node --test scripts/*.test.mjs`

(pass the glob —`node --test scripts/`

tries to load the folder as a module and fails).

The wiki grows through three operations you trigger by talking to your agent.

Drop a source into `raw/`

(a saved article, a transcript, a screen dump, an exported doc), then tell the agent:

"Ingest

`raw/<path>`

into the wiki following WIKI.md. Summarize the key takeaways first and ask me what to emphasize before writing pages."

The agent reads the source, discusses it with you, writes/updates pages, adds cross-links, updates `index.md`

, and appends to `log.md`

. Review its summary, then let it file. Commit.

Ask questions against the accumulated knowledge:

"Using the wiki, how does the reservation approval flow work? Cite the pages."

The agent reads `index.md`

, opens the relevant pages, and answers with citations. When an answer is valuable (a comparison, an analysis, a newly discovered connection), tell it to **file it back** as a new page — so your explorations compound just like ingested sources.

Periodically:

"Run

`node scripts/lint.mjs .`

, fix any broken links or missing index entries, then do a semantic lint per WIKI.md §5 and suggest what to ingest next."

You have an internal app or system with tribal knowledge and no docs. Point your agent's browser tools at each screen/module, dump the page text into `raw/<screen>/`

, and ingest one screen at a time. The agent builds `screens/`

pages plus `domains/`

(business areas) and `entities/`

(the records the system manages), fully cross-linked. Later: *"How does staff attendance get recorded?"* → a cited answer synthesized across pages. This is exactly the pattern this repo was built for.

Treat source directories as raw sources. Ingest module by module; the agent writes `modules/`

pages (responsibility, key files, dependencies) and `concepts/`

pages (auth, caching, the data model), linking them. Ask *"how does auth work end to end?"* and get a synthesized, cited walkthrough instead of grepping. Re-ingest a module after a big refactor; the linter flags stale cross-references.

Going deep on a topic over weeks. Clip articles/papers into `raw/`

(the Obsidian Web Clipper is handy). Ingest each; the agent maintains an evolving thesis in `overview.md`

, entity pages for key people/works, and concept pages, flagging where a new paper contradicts an earlier claim. The wiki becomes your compounding literature review.

Feed meeting transcripts, decision records, and thread exports into `raw/`

. The agent maintains pages per project, per decision, per person — the maintenance no one on the team wants to do. Query *"why did we choose Postgres over DynamoDB?"* months later and get the reasoning with sources.

**Keep** It's your source of truth. The agent reads it, never edits it.`raw/`

immutable.**Don't hand-write the wiki.** Let the agent write it; you curate and ask. Hand edits drift from the conventions and the agent's model of the wiki.in your installed version — don't rely on it. Use the XDG wrapper (Step 6) for isolation.`qmd init`

may not existtreats the path as a module and fails. Pass a glob:`node --test scripts/`

`node --test scripts/*.test.mjs`

.**Non-English content?** The default embedding model is English-centric. Set`QMD_EMBED_MODEL`

to a multilingual model (e.g. Qwen3-Embedding) for CJK/Vietnamese/etc.**MCP changes need an agent restart** to load.**The agent copies your corpus, not your rules.** If your existing pages hard-wrap prose at ~90 columns, the agent will keep hard-wrapping no matter what`WIKI.md`

says — in-context examples beat instructions. Worse,`WIKI.md`

itself is usually the most hard-wrapped file you own, so it teaches the opposite of what it states. Reflow your rule files*first*, then write the rule. The same trap applies to any convention you state but don't demonstrate.**Don't reach for** It re-serializes what your markdown`prettier`

before reading what it changes.*means*, which is not always what you see. A frontmatter template indented under a list item — with no code fence — parses as prose, and its`---`

lines become setext underlines: prettier collapses the whole block into one line, because that block was already rendering as broken headings on GitHub and you never noticed. Fence it as```` yaml`

, and set`embeddedLanguageFormatting: "off"`

so prettier does not reformat the YAML inside. Same story for steps packed onto one line (`4. Do X. 5. Do Y.`

): markdown sees one item, prettier renumbers to match. Both are bugs in the source, revealed rather than caused.**Browse with** Open the folder — its graph view is the fastest way to see hubs, orphans, and the shape of your knowledge. Obsidian is the "IDE"; the agent is the programmer; the wiki is the codebase.[Obsidian](https://obsidian.md).

```
llm-wiki/
├── WIKI.md              # schema + ingest/query/lint workflows (agent reads first)
├── index.md             # catalog — read first when answering
├── log.md               # append-only history
├── README.md            # human-facing readme
├── .prettierrc          # { proseWrap: never, embeddedLanguageFormatting: off }
├── raw/                 # immutable sources (read-only)
├── wiki/                # LLM-generated pages
│   ├── overview.md
│   ├── domains/  entities/  concepts/   (+ screens/ or modules/ …)
├── scripts/
│   ├── lint.mjs         # mechanical lint (Part 3)
│   └── qmd-wiki.sh      # isolated qmd wrapper (Part 2)
└── .qmd-home/           # qmd's local index + model cache (git-ignored)
```

That's it. Scaffold in 15 minutes, then let the agent do the bookkeeping while you curate and ask good questions. The wiki compounds with every source you add and every question you ask.
