---
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 <glob-or-path>] [--category <class\|subtype>]` | 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 <glob-or-path>] [--category <class\|subtype>]` | Apply the latest report's findings; confirm-tier entries gated. |
| `/os-doc-hygiene:sweep [--scope <glob-or-path>] [--category <class\|subtype>]` | 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.
