✦ Documented by Claude · 2026-08-14
Write-up
- Kind
- Claude Code plugin
- Author
- Christopher Whitelam
- Status
- Shipped
- Surface
/write-up- Documented
- 2026-08-18
Write-up is a skill for Claude Code that writes the story of a finished piece of work as a self-contained HTML article, and accumulates those articles into a small local wiki. It exists because a git history records what changed and never records why, and because the reasoning that produced a change is at its most complete in the minutes after the work is done and gone a week later. This article is itself a write-up, generated by the tool it describes.
Why
A commit preserves a diff and a one-line message. A pull request adds a title and whatever the author had the patience to type into the description box. Neither preserves the three approaches that were tried and abandoned, the constraint that made the obvious design impossible, or the one function that turned out to be the whole trick. That context lives in a chat transcript nobody will ever reopen.
The gap is worst exactly where it hurts most: six months later, when someone (often the original author) asks why a thing was built this way and the honest answer is that nobody remembers. The goal here was to capture that reasoning at the moment it is still cheap to capture, in a format someone would actually read.
What
- A Claude Code skill that writes
<outputDir>/<branch>.htmlfrom what the model already holds in session context, in six fixed sections: Why, What, How, Decisions, Day by day, Highlights. - Decisions is an Architecture Decision Record log, so the choices that shaped the work stay scannable without rereading the whole article.
- A generated front page with full-text search, status filter chips, and hover previews of every article.
- Automatic "Referenced by" backlinks derived from a manifest, so the wiki cross-links itself without hand maintenance.
- Glossary hovercards for non-obvious terms, from a curated dictionary rather than runtime guessing.
- A gated Stop hook that asks for the article exactly once, at genuine wrap-up, and a day-journal entry once per working day.
- A POSIX
shStop hook with no dependencies at all, and an optional Python CLI (init,build,digest,suggest,shot,sweep,doctor) for the mechanical jobs. Python is never required. - Installed as a plugin in two commands, which wires the hook without anyone editing
settings.json. - A home wiki at
~/.claude/write-ups/for work that is not a branch and may not be code: research that reached an answer, a decision argued out over one session.
How
- Started from the observation that the model already knows the story at the end of a session. No mining, no summarisation pass: chose "write from live context" over "reconstruct from the transcript" because reconstruction is expensive and produces a worse story than the one already in working memory.[1]
- Fixed the article structure instead of letting each one find its own shape. Six sections, always in the same order, so a reader who has seen one article can skim any other. Chose a rigid skeleton over freeform prose because consistency is what makes a wiki scannable.
- Made the page a single self-contained HTML file: styles inline, no build step, no framework, no network. Chose static files over a documentation site generator because a wiki nobody has to install is a wiki that still opens in three years.
- Wrote the Highlights section as verbatim code rather than prose descriptions of code. A snippet you can copy is worth a paragraph explaining it, and it cannot drift into being wrong the way a summary can.
- Generated the index rather than hand-maintaining it. An early version had the model append an entry per article; entries went missing for days because a hand-maintained list is a hand-forgotten list. Chose "scan the folder and rebuild" over "append on write".[2]
- Gated the Stop hook hard. It fires only when the tree is clean, the branch is ahead of the default branch, and the work is pushed, and then only if the article is missing or older than the newest commit. Chose a narrow gate over a reminder-on-every-turn because a hook that cries wolf gets disabled within a day.[3]
- Extracted the forge-specific URLs into
.write-up.json, detected from the git remote atinittime. GitHub, GitLab, Bitbucket, and Azure DevOps each get their own link patterns; an unrecognised host resolves to no links at all rather than broken ones.
Decisions
build restores any that go missing.sh -c on macOS and Linux and Git Bash on Windows, so a shell script needs nothing installed. Python is not in that position: macOS has shipped no bundled Python 3 since 12.3 and Windows ships none. The CLI survives for the mechanical jobs, with a documented fallback for every command.settings.json. Four manual steps, one of them editing a config people care about, which is where they quit. A plugin carries the skill and the hook together, so it is two commands and no config editing.init refused outside a repo and the hook was permanently silent there. But a lot of real work is research, a decision, an investigation. The wiki now falls back to ~/.claude/write-ups/. The hook stays repo-only on purpose, because branch state is the only honest "this is finished" signal and a hook that guessed would nag.body.new qualifier and two states to reason about, to serve a choice nobody was making. Removed: 48 lines out of the runtime.Day by day
Audit
- Inventoried what the private version actually depended on: three files in the skill folder, four more outside it, all of the latter gitignored.
- Found the real blocker: the article template loads
write-up-ui.jsandglossary.jsby relative path, and neither shipped with the skill. - Counted five hardcoded forge URLs across the template and the hook.
Extraction
- Collapsed two PowerShell scripts and two Python scripts into one cross-platform CLI.
- Added forge detection so
initreads the git remote and writes the right link patterns without being told. - Ported the Stop hook from PowerShell to Python, keeping the gate logic identical.
- Made
buildself-healing: it restores any runtime asset missing from the wiki folder.
Named it, then proved it
- Renamed from Blueprint to Write-up. Codex found the killer fact:
backstory-clialready ships on PyPI mining Claude Code sessions for exactly this, which killed the leading candidate. - Built a 42-check acceptance harness that extracts the repo with
git checkout-index, installs underdashwith no Python on PATH, and runs a real branch through every hook gate. - It found two shipped bugs: the template never wrote
data-day, so the hook could never see a journal entry and would have asked for one every day forever; and the empty-state search message never appeared, because the code cleared an inline style instead of settingdisplay:block.
Packaged, generalised, stress-tested
- Repackaged as a plugin so install is two commands rather than four manual steps.
- Stress-tested the six-section format against five work archetypes, including a spike that shipped nothing and a debugging session that never found the bug. All five held; the exercise added Concluded and Unresolved to the status vocabulary and a rule that a reversing decision must say so in its first clause.
- Fixed a
UnicodeEncodeErrorreported from real use: a right arrow in a transcript crasheddigeston any default Windows console. Nothing in the source is non-ASCII, so it was user data all along. - Added the home wiki after noticing the tool assumed every piece of work was a git branch.
Highlights
build treats their absence as a repairable state rather than an error, and copies them back every time it runs.[1]def ensure_runtime(wdir):
"""Copy any missing runtime asset into the wiki folder. Returns what it wrote."""
written = []
if not os.path.isdir(wdir):
os.makedirs(wdir)
for dest, src in RUNTIME_ASSETS.items():
dpath = os.path.join(wdir, dest)
spath = os.path.join(ASSET_DIR, src)
if os.path.exists(dpath) or not os.path.exists(spath):
continue
shutil.copyfile(spath, dpath)
written.append(dest)
return written
tpl_path = os.path.join(ASSET_DIR, "index-template.html")
if not os.path.exists(tpl_path):
# Fail loudly and non-zero. A quiet aside here lets the front page rot
# unnoticed while new articles pile up behind it.
print("wrote manifest.js (%d articles)" % len(items))
die("index-template.html missing at %s; index.html NOT regenerated "
"and the front page is now stale" % tpl_path, 2)
if os.path.exists(article):
# Fire once more only if the article went STALE: commits landed after it was
# written. A refresh patches deltas, it never regenerates (see SKILL.md).
last = git("log", "-1", "--format=%ct")
if not last or not last.isdigit():
return 0
if int(last) <= int(os.path.getmtime(article)):
return 0
kind = (
"github" if "github" in host
else "gitlab" if "gitlab" in host
else "bitbucket" if "bitbucket" in host
else None
)
if not kind:
return forge
repo_url = "https://%s/%s" % (host, path)
t = FORGE_TEMPLATES[kind]
forge.update(
type=kind,
repoUrl=repo_url,
branchUrl=t["branchUrl"].format(repo=repo_url, branch="{branch}"),
fileUrl=t["fileUrl"].format(repo=repo_url, branch="{branch}", path="{path}"),
)
last_doc=$(git log -1 --format=%H -- "$article" 2>/dev/null)
if [ -n "$last_doc" ]; then
since=$(git rev-list --count "$last_doc..HEAD" -- . ":(exclude)$article" 2>/dev/null)
if [ "${since:-0}" -gt 0 ] 2>/dev/null; then
block "$since commit(s) landed on $branch after $article was last updated. ..."
fi
fi
Open
Window-targeted screenshots work on Windows only; every other platform gets full-screen capture, and web pages are better captured with the browser tools anyway. Closing an article out after its pull request merges is a separate opt-in command, writeup.py sweep, supporting GitHub through the gh CLI and Azure DevOps through a personal access token. It is the only part of the tool that makes a network call, it never runs on its own, and the Stop hook never touches the network. The macOS and Linux capture fallbacks are written against the standard tools but have not been exercised on those platforms. Two people adding articles to the same repo wiki on different branches still conflict in manifest.js, because both insert at the top; the entries are one per line now, so the conflict is readable and resolved by keeping both, but it is not gone. And the format has only been proven by articles this author wrote: whether a different model on a different codebase reaches for the right section is still untested.
References
plugin/scripts/writeup.py— the whole toolchain: init, build, digest, suggest, shot, sweep, doctor.plugin/assets/index-template.html— the front page shell the build fills with entries.plugin/SKILL.md— the authoring rules the model follows, including the refresh and final-sweep modes.plugin/hooks/write-up-hook.sh— the Stop-hook gate, POSIX sh