docsync-setup — track and fix doc drift
Caution
Docs go stale the moment code moves on. Nobody remembers to re-read SKILL.md after a refactor, and a forgotten README.md means the next session starts from wrong assumptions. Manual checks don’t scale across dozens of files.
Tip
Frontmatter is the source of truth — no separate ledger. Docsync installs three project-local hooks that quietly record which .md files you read or edit. At the end of a turn, if any touched doc is stale by its own last_updated date, it nags once and asks whether to sync. The skill is user-invoked only (disable-model-invocation: true) — the model never fires it on its own; the installed hooks do the running afterwards.
Note
docsync-setup is the reference implementation. Its last_updated-based staleness model, and its version / generated_by / last_updated metadata trio at the top of config.json, is the pattern the other nine -setup skills across brewcode, brewtools and brewdoc were unified onto in v5.1.0.
Quick reference
| Field | Value |
|---|---|
| Command | /brewdoc:docsync-setup [mode] |
| Canonical modes | status · install · upgrade · enable · disable · uninstall · purge |
| Extras | sync [--all] · reread · frontmatter · free text (RU/EN) |
| No argument | status when the hooks are installed, install when they are not |
| Invocation | user-invoked only — disable-model-invocation: true |
| Model | sonnet |
| Tools | Read, Write, Edit, Bash, Glob, Grep, AskUserQuestion |
When to use
Fits repos with hand-maintained prose docs — README, SKILL.md, design notes — that
quietly drift behind the code they describe. It is not for generated docs (API
reference, changelogs) that already regenerate on build. Purely user-driven:
disable-model-invocation: true in the skill’s frontmatter means the model never
runs it on its own — you always invoke it by hand.
| Scenario | Command |
|---|---|
| First time in a project — set up staleness tracking | /brewdoc:docsync-setup install |
| Check what’s tracked and what’s stale, no changes | /brewdoc:docsync-setup status |
| Refresh an existing install to the current plugin version | /brewdoc:docsync-setup upgrade |
| Pause tracking without unwiring hooks or losing config | /brewdoc:docsync-setup disable |
| Resume a paused tracker with zero re-analysis | /brewdoc:docsync-setup enable |
| Refresh stale docs after confirming which ones | /brewdoc:docsync-setup sync |
| Force a full resync of every in-scope doc | /brewdoc:docsync-setup sync --all |
| Reload tracked docs into context without editing | /brewdoc:docsync-setup reread |
| Retro-add tracking frontmatter to existing docs | /brewdoc:docsync-setup frontmatter |
| Remove the tracker without touching foreign hooks | /brewdoc:docsync-setup uninstall |
Remove the tracker and delete .claude/docsync/ too | /brewdoc:docsync-setup purge |
Examples
# First run in a project — asks threshold + excludes, installs 3 hooks
/brewdoc:docsync-setup install
# Natural language also works, RU or EN
"что устарело в документации"
"track doc staleness in this project"
"docsync status"
status prints an age table per tracked doc:
## Status
| Doc | doc_type | last_updated | age | state |
|-----|----------|--------------|-----|-------|
| README.md | user | 2026-07-01 | 18d | stale |
| SKILL.md | llm | 2026-07-17 | 2d | fresh |
| CHANGELOG.md | skip | — | — | excluded |
Flow
- Resolve mode
Free text in RU or EN maps to a canonical verb (
status,install,upgrade,enable,disable,uninstall,purge) or one of the extras (sync,reread,frontmatter). Empty input picksinstallwhen the hooks are not there yet, otherwisestatus. The oldinitalias parses as free text and is echoed back asinstall. - Install (once)
installasks for a staleness threshold (default 7 days) and exclude globs, copies the 3 hooks into.claude/hooks/, writes.claude/docsync/config.json, and idempotently merges.claude/settings.json— always backed up to.bakfirst. It never adds frontmatter to your docs.upgradereplays the same copy + idempotent merge against the current plugin version while preservingconfig.json— threshold and excludes survive. - Track silently
From the next session on,
docsync-track.mjs(PostToolUse onWrite|Edit|MultiEdit) anddocsync-watch.mjs(PostToolUse onRead) record every.mdfile touched during a session — no output, no interruption. - Nag once
At
Stop,docsync-gate.mjschecks touched docs against their ownlast_updatedfrontmatter. If any is older thanthreshold_days— or carries no date at all — it blocks the turn once per session, listing every offender, and tells Claude to ask about syncing. - Sync on confirmation
/brewdoc:docsync-setup synclists the stale set, asks viaAskUserQuestion, then reads each confirmed doc, follows itssync_procedure(prose Claude acts on — no hook parses it) and bumpslast_updatedto today. Compression depth followsdoc_type:llm= deep,user= light, absent =user.
Internals
Frontmatter schema — every tracked .md carries its own state, no separate registry:
---
doc_type: "llm" # optional; absent or unrecognized => user. values: llm | user | skip
last_updated: "2026-07-19" # sole staleness input (YYYY-MM-DD, LOCAL time)
sync_procedure: "what to check / where to look when syncing" # optional, prose
---Staleness is DATE ONLY: today - last_updated > threshold_days. No hashing, no depends_on graph.
- Values are quoted. The hooks’ frontmatter parser strips surrounding quotes either way, so unquoted docs already in your repo keep working — but a real YAML consumer types an unquoted
2026-07-19as a Date, so everything the skill emits is quoted. doc_type: "skip"excludes a file entirely. Absent or unrecognizeddoc_typeis normalized touserby adocTypeOf()helper present in all three hooks — the default is enforced in code, not only described here.sync_procedureis a model-only hint: no hook parses it.syncmode and the gate’s block message tell Claude to read the doc and follow it.
Enable/disable polarity — the opposite of what you’d guess. The flag lives in config.json under "enabled", and an absent key means ENABLED: every hook’s loadConfig() reads enabled: c.enabled !== false. So "enabled": false is installed-but-inert, not missing — a config with no enabled key at all is live. This is the reverse of brewtools:agent-deadline-setup, whose gate checks cfg.enabled !== true, so an absent key there is INERT. Do not assume the two setups share a polarity.
Before this release, enable/disable required the version/generated_by/last_updated trio to already be present byte-identical, and failed when it was missing. They now ADD the trio when absent, so a pre-standard config.json gets backfilled instead of staying unstamped forever.
The three hooks
| File | Event | Matcher | Action |
|---|---|---|---|
docsync-track.mjs | PostToolUse | Write|Edit|MultiEdit | Records touched .md; nudges to add last_updated when missing |
docsync-watch.mjs | PostToolUse | Read | Records touched .md. Silent by design — a Read fires constantly, so mid-turn context injection on every one would be noise |
docsync-gate.mjs | Stop | — | Re-applies scope (exclude globs + doc_type: skip) to the touched set, then blocks at most once per session, listing every stale and every undated touched doc, and tells Claude to ask about syncing |
The gate’s asked flag is one boolean per session: after that single block, docs that go stale or get touched later in the same session produce no further signal until the next session. That is deliberate — a Stop hook that blocks repeatedly loops. A doc that is only ever read and carries no last_updated still produces a signal: the gate lists it under no last_updated.
Project root resolution — the hooks resolve it themselves, at runtime. Each hook tries CLAUDE_PROJECT_DIR first, then walks upward from the hook’s own cwd for the nearest .git or .claude marker, then falls back to that same cwd — there is no git rev-parse rung inside the hooks. The skill’s own install/upgrade/status Bash snippets add one extra rung, git rev-parse --show-toplevel, between the env var and the walk; the two agree on every layout except a nested .claude, where the hooks — not the skill’s shell snippet — decide where config and state actually land. input.cwd is never treated as the root: it drifts mid-session, so it is used only to resolve a relative tool_input path.
Hooks are self-contained ESM (Node built-ins only), read state from .claude/docsync/ at runtime, and take effect starting the next session. Line 2 of each carries a // brewcode-meta: version=… generated_by=brewdoc:docsync-setup stamp baked at release, so an installed copy can be compared byte-for-byte against the plugin’s. tests/suite-hooks.mjs exercises all three hooks directly — session isolation, exclude globs, the disabled-polarity fix, gate blocking at most once, a missing config file — 26 checks, all passing.
State / config — .claude/docsync/:
config.json—{ "version": "X.Y.Z", "generated_by": "brewdoc:docsync-setup", "last_updated": "2026-08-08", "enabled": true, "threshold_days": 7, "exclude": ["node_modules/**", ...] }— the three provenance keys come first, in that order, ahead of the skill-private onesstate.json— install writes{ "session_id": null, "touched": [], "asked": false }; the hooks own it from then on, replacingnullwith the live session id and resettingtouched/askedon every session change
Scope — all *.md in the project minus exclude globs and any file marked doc_type: "skip". All three hooks apply both filters, the Stop gate included, so marking a doc skip mid-session silences it immediately — even if it was already touched. Only docs actually read or edited in a session are candidates for the end-of-turn nag; untouched docs are never nagged.
uninstall removes only the 3 docsync hook entries from settings.json (foreign entries preserved), deletes the 3 hook files, and asks separately whether to drop .claude/docsync/ — keeping it means a later install reuses the config. purge is uninstall with no survivors: .claude/docsync/ is deleted outright, foreign settings and the .bak backup untouched.
memory-sync-setup
Generates a project-local /memory-sync skill that keeps CLAUDE.md and rules truthful — pairs well with keeping docs fresh.
GitHub source
Source code, hook assets, and full SKILL.md for docsync.
Brewdoc overview
All brewdoc skills in one place.
Setup Status
Read-only dashboard across every -setup skill — installed, stale or missing, with the hand-run command for each.
Updating plugins
/brewtools:plugin-update to check and update the brewcode plugin suite in one command.
See the FAQ for details.