The Problem Nobody Wants to Own
Every team I've worked with has the same wiki: lovingly written in month one, quietly wrong by month six. Nobody decides to let it rot. The code just changes faster than anyone has time to write about it.
At one of my clients, I decided to stop relying on willpower. About once a week I run a script that reads what actually happened in each repo (commits, merged merge requests, issues opened and closed) and asks Claude to update the wiki to match. So far it works better than I expected.
The Shadow Wiki Trick
GitLab gives every project a wiki, and that wiki is itself a git repository. If your project lives at gitlab.example.com/group/infrastructure-terraform, the wiki is cloned from gitlab.example.com/group/infrastructure-terraform.wiki.git.
That detail does most of the work for us:
- The wiki is just markdown files in a git repo, so there's no special API to learn.
- Pushing to it is a normal
git push. - Git history gives us an audit trail of every documentation change for free.
- Git commit trailers give us somewhere to keep state (more on that below).
What Comes Out
The output is what sold me. Claude doesn't just paraphrase commit messages. It writes pages that look like someone cared:
- Markdown tables for inventories: environments, modules, variables, services.
- Mermaid diagrams for architecture and flow, which GitLab renders natively in wikis.
- A change history that records when something changed, who changed it, and a link to the MR that did it.
That last one is the part I value most. The "why did this change?" question now has a clickable answer sitting next to the documentation itself.
How the Script Works
The design principle is that the script does everything deterministic, and Claude does only the writing. Claude never touches the network, never runs git, and never sees a source repo checkout.
1. State lives in the wiki itself
I didn't want a database, a state file, or a cron-host dotfile to get out of sync. Instead, each sync commit to the wiki carries git trailers:
wiki-sync: 23 commits, 4 merged MRs, 2 issues since 2026-10-08T14:02:11Z
Synced-Branch: main 3f9c1e0a...
Synced-Branch: release 9b77d412...
Synced-Through: 2026-10-15T13:58:40Z
Synced-Branch records the last commit digested on each tracked branch. Synced-Through is a timestamp cursor for issues and MRs. The next run finds the newest commit carrying these trailers with git log --format='%(trailers:...)' and picks up from there.
The important property: the cursor only advances in the same push as the pages it describes. If anything fails halfway, nothing moves, and the next run re-covers the same ground.
2. Build a digest
For each repo, the script assembles one markdown digest:
- Commits since the cursor on each tracked branch, oldest first, each with its message, a
--stat, and a capped diff excerpt (150 lines per commit, with lockfiles,vendor/, minified assets and source maps excluded). There's a hard cap on commits per run and a total byte budget for diffs so a monster week doesn't blow up the context window. - Merged MRs from the GitLab API, with author, source and target branch, and a trimmed description.
- Issues created or closed in the window, with labels and state.
Two details earned their keep:
- Cherry-pick de-duplication. If the same fix lands on
mainand a release branch, it would otherwise be described twice. The script computesgit patch-id --stablefor each commit and drops repeats. - Cursor fallback. If someone force-pushes and the stored SHA is no longer an ancestor of the branch tip, the script falls back to a time-based
--sinceinstead of failing.
3. Run Claude headless and locked down
The digest is appended to a prompt template and piped into claude -p, run from inside the wiki checkout:
claude -p --permission-mode dontAsk \
--allowedTools Read Edit Write Glob Grep
That's the entire tool allowlist: read, edit, write, and search, with no Bash and no network. Claude can only touch files in the wiki clone.
I use a dedicated CLAUDE_CONFIG_DIR so the client work runs under its own login and settings, separate from my personal setup.
4. Don't trust, sanitize
After Claude finishes, a sanitize_wiki step reverts anything the script doesn't accept: any changed file that isn't .md, and any deleted page. Belt and suspenders on top of the tool allowlist.
If Claude exits non-zero, the script resets the wiki checkout to clean and reports a failure. Half-written documentation never gets committed.
5. Commit and push
The commit message is a one-line summary of counts, then Claude's own summary of what it changed (trimmed), then the trailers, plus a Co-Authored-By: Claude line so it's honest about who wrote what. The script pushes, and if the push is rejected it rebases once and retries.
Small Safety Features That Matter
None of these are clever, but together they make a weekly manual run something I trust:
| Feature | Why |
|---|---|
flock on a lock file |
One run at a time per machine |
.updating marker in the wiki |
A human or another session editing that wiki blocks the run; stale after 2 hours |
| Refuses to run with a dirty wiki checkout | Never mixes my hand edits with generated ones |
--ff-only merge before starting |
Fails loudly if the wiki diverged from origin |
Once-per-day rule (--force to override) |
Prevents accidental double runs |
--dry-run |
Builds every digest and prints sizes; no Claude, no commit |
--keep |
Preserves digests, prompts and Claude's raw output for debugging |
| Per-repo exit status summary | updated, skipped, nothing new, or FAILED at a glance |
The summary at the end looks like:
api-gateway updated
infrastructure-terraform updated
legacy-batch-jobs nothing new
shared-modules skipped
What I'd Tell You Before You Try This
- Pass in facts, not hopes. Claude is good at turning a diff and an MR description into prose and tables. It's much worse at guessing what happened. Every claim in the wiki traces back to something in the digest, which is why the digest includes links.
- Cap everything. Commits, diff lines, description lengths, total bytes. Context is finite, and bounded input makes the cost and runtime predictable.
- Keep the prompt in a file under version control. Mine is a template with placeholders for the repo, date range and branches. Tuning documentation style is just editing markdown.
- Seed the wiki first. The script refuses to run without a baseline sync commit. The first generation is a different, larger job; weekly updates are incremental.
- Review it, at least at first. Because the wiki is a git repo,
git showon the sync commit is the review. I skim the diff after each run.
Why It Works
It isn't magic. It works because the pieces are boring: git as the database, trailers as the cursor, the GitLab API for the facts, and a model confined to editing markdown files. The model does the one thing it's good at, which is turning a pile of change records into readable documentation, and everything that could go wrong is handled by plain shell around it.
The wiki goes from "stale by default" to "current as of last Tuesday," and for about thirty minutes of babysitting a week, I'll take that trade.