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
Shared setup metadata. Docsync is one of 11 setup skills across Brewcode, Brewtools and Brewdoc.
Its config carries provenance and a separate content_version for content-drift checks.
Quick reference
| Field | Value |
|---|---|
| Command | /brewdoc:docsync-setup [prompt] [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 resolves a canonical lifecycle verb or an extra mode. Empty input means install when absent, otherwise status. Explicit modes can appear anywhere in the prompt; material unresolved write decisions are bundled before work.
- Install (once)
installasks for a threshold (default 7 days) and excludes, then validates inputs, settings and hook syntax before writes. It copies three hooks, writes configuration and idempotently merges settings. An existingsettings.json.bakis retained; a backup is created only when absent.upgraderefreshes provenance and hooks while preserving enabled state, threshold, excludes and other config fields. Neither mode resets session state or adds document frontmatter. - 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.
Enable/disable refreshes provenance and content-version metadata while preserving threshold and excludes.
Write failure and recovery. Install/upgrade snapshots every destination before mutation and publishes each file through a checked atomic rename. If a destination changes meanwhile, setup stops rather than overwriting it. On failure, rollback restores each committed file’s prior bytes and mode only if it still matches this run’s output; concurrent edits remain intact and recovery gaps are reported. This is per-file publication and recovery, not an all-files atomic transaction.
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. Their line-2 metadata supports installed-copy comparison.
Local hook tests cover session isolation, excludes, enabled polarity, gate blocking and missing
config. Suite output supplies the current pass/fail totals; this page does not claim a live runtime run.
State / config — .claude/docsync/:
config.json— version/content-version provenance, enabled state, threshold and exclude globs; upgrade retains configuration values while refreshing metadata.state-<session_id>.json— hook-owned touched set and asked flag for each session. A missing session id falls back tostate.json. Install/upgrade preserves existing session files.
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 three docsync hook entries and files, preserving foreign entries,
then asks whether to remove .claude/docsync/. Keeping that directory retains configuration and
session history. purge removes it as well; foreign settings and the settings backup remain.
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.