--- type: user-guide title: os-doc-hygiene — User Guide summary: Mental model and gotchas for using the os-doc-hygiene Claude Code plugin well — the stale-vs-bloat distinction, check/calibrate/clean/sweep semantics, the two rule files, rule-activation pitfalls, safety tiers, and report-reading traps. tags: - type/user-guide - tool/os-doc-hygiene - tool/claude-code - project/cc-os - domain/documentation scope: global last_updated: 2026-07-15 date: 2026-07-15 related: - cc-os-hub source: cc-os --- # os-doc-hygiene — User Guide ## What it is `os-doc-hygiene` is a globally-installed Claude Code plugin that monitors and manages project documentation across two independent failure modes: docs that are *wrong* and docs that are *true but bloated*. It reminds passively every session (zero tokens, no mutation) and does its actual work — scanning, classifying, and cleaning — only on demand via skills. It is not a linter for prose quality; it is a lifecycle manager for `.md` files, extended (2026-07-15) with a rulebook that can also delete or archive files on a schedule or trigger. ## Mental model - **Stale vs bloat is the core axis, and they are not the same failure.** *Stale* means the doc is *wrong* — contradicted, orphaned, superseded, provisional-but-abandoned, completed-in-place, or duplicated; the remedy is fix or remove. *Bloat* means the doc is *true but mostly irrelevant at its current altitude* — the remedy is to distill, split, or freeze it, almost never to delete history. Conflating the two leads to either deleting a correct-but-long doc or leaving a wrong-but-short one in place. - **Severity scales with injection frequency, not file size.** A stale line in a file that gets auto-injected every session (e.g. a `CLAUDE.md`) is worse than the same line in a doc nobody opens, even if the second doc is larger. - **The skill quartet has distinct jobs, not a redundant surface:** - `:check` — scan + classify against the *existing* rules, writes a report. Does not learn anything new. - `:calibrate` — the *learn-new-rules* loop. It clusters the pool of files no existing rule governs, proposes candidate globs, and gets them judged and persisted. It is recurring and self-narrowing: each pass only looks at what's still unmatched, and it hard-caps at 3 rounds per invocation (stops early at <2 new rules or <10% unmatched shrink). - `:clean` — applies the *most recent* report's findings (deterministic ops mechanically, generative ops via a subagent, confirm-tier gated). - `:sweep` — check-then-clean back to back; it does not skip clean's confirm gate, it just chains the two. - **Rules live in two places with different trust levels.** A committed, per-project `.dochygiene-rules.json` at the repo root (reviewable, versioned, travels with the repo — ADR-0038) holds the project's actual lifecycle rules. A gitignored `.cc-os/dochygiene/` directory (legacy fallback path: `.dochygiene/`) holds only local state and reports — never rules. The plugin also ships a global `plugins/os-doc-hygiene/rulebook.json`, but that ships **only IGNORE seeds** (`graphify-out/**`, `.dochygiene/**` as of 2026-07-15) — it is not a source of general-purpose lifecycle rules for your project. - **A rule with no `lifetime` field is not "keep" — it's an IGNORE sentinel.** IGNORE paths are pruned from the scan walk entirely; they never appear in a report, not even as "kept." This is distinct from `lifetime: keep`, which *is* walked and reported, just never deleted. If you expect a file to show up as "kept" in a report and it never does, check whether an IGNORE rule (no `lifetime`) is silently swallowing it. ## Nuances & gotchas - **`temporary` deletion needs BOTH conditions, not either.** A file is only deleted under a `temporary`-lifetime rule once it is *both* ranked past `retain_recent` (i.e. not one of the N newest matches) *and* older than `max_age_days`. A file that is old but still in the top-N-newest survives; a file that's low-ranked but too young also survives. Don't reason about temporary rules with just one of the two numbers. - **`served_when` vs `served_when_path` is a trust boundary, not a style choice.** `served_when` is free text judged by the LLM classifier — it is *always* confirm-tier, never auto, regardless of git state. `served_when_path` is a path pattern the scanner can prove deterministically (e.g. "moved into `archive/{id}/`") — it *can* be auto-tier. If you want a rule to eventually run silently, the path is to graduate it from `served_when` to `served_when_path` by adopting a structural completion convention (see conventions.json below) — not to just mark it `confirm: true` and move on. - **A rule missing `confirmed_by` is silently inactive.** It doesn't error, doesn't warn loudly — it just never acts. If a rule you added doesn't seem to be doing anything, check for a missing or malformed `confirmed_by`/`confirmed_on` before assuming the glob is wrong. - **Safety tiers are about evidence quality and recoverability, not file type.** `auto` requires deterministic + reversible + objective; `confirm` covers anything destructive, subjective, or generative. Critically: a dirty or untracked file *always* escalates to confirm even if a rule would otherwise make it auto — there's no git history to recover from. And git state is **re-verified at apply time**, not trusted from when the report was generated — a file that was tracked+clean at `:check` time but got dirtied before `:clean` runs will be downgraded to confirm (or skipped as `git-state-changed-since-check`). - **The report's "cleared" count is not what it sounds like.** `cleared` = shortlist entries minus findings — i.e. candidates the deterministic scanner surfaced but the classifier decided need no action. It can coincidentally equal (or come close to) the total files-scanned count, which misleads you into thinking "cleared" means "scanned." The AI classifier only ever sees the signal-bearing subset the scanner flagged — in the first cc-os run, that was 44 of 384 files scanned; the rest were cleared deterministically and never reached the model at all. - **There is deliberately no calendar-date staleness signal.** The scanner does not flag "this file hasn't been touched in N days" as a signal by itself. Staleness comes from broken references, file length (bloat), edit-recency-vs-churn patterns, and lifecycle rules — not raw age. A quiet-but-correct doc is not stale just because it's old. - **The scanner is a pure script — no tokens, full re-walk every run, `.md` files only.** It doesn't cache or incrementally diff; every `:check` re-walks the whole tree (minus exclusions). Don't expect it to pick up non-Markdown docs (READMEs in other formats, code comments) as scan targets. - **The `SessionStart` hook only reminds — never analyzes or mutates.** It fires at most once per calendar day while docs are stale (keyed off `last_reminded`), and spends zero AI tokens. If you see the reminder banner, no scan has happened yet — you still have to run `:check` to get real findings. - **New-project onboarding is not automatic.** A fresh project has no `.dochygiene-rules.json`. Until you run `:calibrate` once to generate it, only the global IGNORE seeds apply and *no* lifecycle deletions happen — `:check`/`:clean` still work for stale/bloat findings, but the lifecycle layer is inert. ## When NOT to use it / limits - Not a prose/style linter — it does not check grammar, tone, or writing quality. - Not a general file manager — the lifecycle/delete features only ever act on paths matched by an explicitly confirmed rule; unmatched files are left alone ("unmatched = unmanaged"). - Not a substitute for `:calibrate` when a project has structurally unusual doc conventions (e.g. specs/plans as the shipped product) — the global rulebook alone will under- or over-match. - Don't expect deletions to be silently recoverable outside git — deletion is a real `git rm` in a dedicated commit; git history is the only archive. There is no graveyard directory. - Not designed to run unattended in CI as a destructive step without review — confirm- tier gates exist precisely because classifier judgment and dirty/untracked state need a human. ## Command reference | Command | Effect | |---|---| | `/os-doc-hygiene:status` | Read-only timestamps (last check/clean/reminded) + report presence. No model, no scan. | | `/os-doc-hygiene:check [--scope ] [--category ]` | Scan + classify against existing rules, write a report. | | `/os-doc-hygiene:calibrate` | Learn-new-rules loop over the unmatched pool; hard cap 3 rounds; nothing persists without the human seeing the rule report first. | | `/os-doc-hygiene:clean [--scope ] [--category ]` | Apply the latest report's findings; confirm-tier entries gated. | | `/os-doc-hygiene:sweep [--scope ] [--category ]` | check then clean, same confirm gate as standalone clean. | ## Pointers - `plugins/os-doc-hygiene/CLAUDE.md` (cc-os repo) — build map + the stale-vs-bloat invariant summary. - `plugins/os-doc-hygiene/lifecycle-spec.md` (cc-os repo) — the full lifecycle-rules design: taxonomy, rulebook schema, deletion semantics, `:calibrate` protocol. - `plugins/os-doc-hygiene/invariants.md` (cc-os repo) — the reversion-protection contract; read before changing any behavioral invariant. - `plugins/os-doc-hygiene/conventions.json` (cc-os repo) — the determinism-promotion catalog (`archive-bucket`, `status-frontmatter`) that graduates `served_when` rules to auto-tier `served_when_path`. - `docs/adr/0038-*.md` through `0041-*.md` (cc-os repo) — rulebook location, deletion autonomy tiers, no-ignore-propagation, and determinism-promotion decisions. ## Related - [[cc-os-hub]] — the cc-os plugin family this tool belongs to.