cd /news/artificial-intelligence/building-an-llm-wiki-for-your-projec… · home topics artificial-intelligence article
[ARTICLE · art-106797] src=gist.github.com ↗ pub= topic=artificial-intelligence verified=true sentiment=· neutral

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)

A developer has published a step-by-step guide for building an 'LLM Wiki'—a persistent, interlinked markdown knowledge base that an AI agent (such as Claude Code or Codex) maintains incrementally. The approach, based on Andrej Karpathy's pattern, compiles knowledge once and keeps it current, contrasting with typical RAG systems that re-derive answers on each query. The guide details a three-layer structure (raw sources, wiki pages, and a schema file) and includes optional search and linting workflows.

read13 min views2 publishedAug 22, 2026

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.

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 withqmdnpm 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:


> 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`

:

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

Overview #

Domains #

(grows as you ingest)

Entities #

(grows as you ingest)

Concepts #

(grows as you ingest)


`log.md`

:

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 #

[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
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"
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
[ -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

— theIndex:

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):

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 companionscripts/lint.test.mjs

using Node's built-innode:test

. Run tests withnode --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. SetQMD_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 whatWIKI.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 filesfirst, 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 markdownprettier

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 setembeddedLanguageFormatting: "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.

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.

── more in #artificial-intelligence 4 stories · sorted by recency
── more on @andrej karpathy 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/building-an-llm-wiki…] indexed:0 read:13min 2026-08-22 ·