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

FieldValue
Command/brewdoc:docsync-setup [prompt] [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 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.

  2. Install (once)

    install asks 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 existing settings.json.bak is retained; a backup is created only when absent. upgrade refreshes provenance and hooks while preserving enabled state, threshold, excludes and other config fields. Neither mode resets session state or adds document frontmatter.

  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.

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

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.mjsStop—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 to state.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

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