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

FieldValue
Command/brewdoc:docsync-setup [mode]
Canonical modesstatus · install · upgrade · enable · disable · uninstall · purge
Extrassync [--all] · reread · frontmatter · free text (RU/EN)
No argumentstatus when the hooks are installed, install when they are not
Invocationuser-invoked only — disable-model-invocation: true
Modelsonnet
ToolsRead, 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.

ScenarioCommand
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

  1. 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 picks install when the hooks are not there yet, otherwise status. The old init alias parses as free text and is echoed back as install.

  2. Install (once)

    install asks 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 .bak first. It never adds frontmatter to your docs. upgrade replays the same copy + idempotent merge against the current plugin version while preserving config.json — threshold and excludes survive.

  3. Track silently

    From the next session on, docsync-track.mjs (PostToolUse on Write|Edit|MultiEdit) and docsync-watch.mjs (PostToolUse on Read) record every .md file touched during a session — no output, no interruption.

  4. Nag once

    At Stop, docsync-gate.mjs checks touched docs against their own last_updated frontmatter. If any is older than threshold_days — or carries no date at all — it blocks the turn once per session, listing every offender, and tells Claude to ask about syncing.

  5. Sync on confirmation

    /brewdoc:docsync-setup sync lists the stale set, asks via AskUserQuestion, then reads each confirmed doc, follows its sync_procedure (prose Claude acts on — no hook parses it) and bumps last_updated to today. Compression depth follows doc_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-19 as a Date, so everything the skill emits is quoted.
  • doc_type: "skip" excludes a file entirely. Absent or unrecognized doc_type is normalized to user by a docTypeOf() helper present in all three hooks — the default is enforced in code, not only described here.
  • sync_procedure is a model-only hint: no hook parses it. sync mode 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

FileEventMatcherAction
docsync-track.mjsPostToolUseWrite|Edit|MultiEditRecords touched .md; nudges to add last_updated when missing
docsync-watch.mjsPostToolUseReadRecords touched .md. Silent by design — a Read fires constantly, so mid-turn context injection on every one would be noise
docsync-gate.mjsStopRe-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 ones
  • state.json — install writes { "session_id": null, "touched": [], "asked": false }; the hooks own it from then on, replacing null with the live session id and resetting touched/asked on 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

Use /brewtools:plugin-update to check and update the brewcode plugin suite in one command. See the FAQ for details.