CLAUDE.md Drift-Check Skill
The instruction file is alias-resolved per
skills/_shared/instruction-file-resolution.md:CLAUDE.md(Claude Code / Cursor IDE) wins ties;AGENTS.md(Codex CLI) is picked up as a transparent alias whenCLAUDE.mdis absent. The resolved path and kind are surfaced in the JSON output (resolved_path,resolved_kind).
Status
PHASE 1 IMPLEMENTED (2026-04-19). Session-end opt-in quality gate. Upstream of /close commit preparation, downstream of vault-sync.
Why this exists
CLAUDE.md is narrative SSOT for a repo's Session Config and project context. It decays quickly when surrounding state changes — paths get renamed, project counts shift, issues close, session files get pruned in digests. The drift-cluster closed by agents/vault#57 (3 items in one sweep) was the 4th incidence of the issue-description-drift-stale-filecount learning. Manual curation does not scale; this skill turns drift detection into a repeatable gate.
Checks
| # | Check | What it scans | How |
|---|---|---|---|
| 1 | path-resolver |
Every absolute path /Users/… in scope files |
existsSync(path) |
| 2 | project-count-sync |
Hardcoded "N registered" / "N projects" claims next to 01-projects/ |
compare to ls -d 01-projects/*/ |
| 3 | issue-reference-freshness |
#NN in forward-looking sections (What's Next, Backlog, Open Issues, Offene Themen, Todo, Next Steps) |
glab issue view NN --repo <origin> |
| 4 | session-file-existence |
50-sessions/YYYY-MM-DD-*.md references anywhere in scope |
existsSync(vault/50-sessions/<file>) |
| 5 | command-count |
"N commands" / "N /commands" claims in prose | compare to ls commands/*.md | wc -l; skipped if no commands/ dir |
| 6 | session-config-parity |
Top-level keys under ## Session Config in CLAUDE.md / AGENTS.md |
diff against docs/session-config-template.md; missing keys flagged as errors |
| 7 | vault-dir-parity |
vault-integration.vault-dir in BOTH CLAUDE.md AND AGENTS.md |
reuse _parseVaultIntegration; flag when the two files disagree |
Check 3 deliberately scopes to forward-looking sections. Mentions inside "Recently Closed", "Decisions", "Archive", etc. describe history and must not be flagged.
Check 5 counts *.md files directly inside commands/ (non-recursive, non-hidden). The commands/ directory is resolved relative to VAULT_DIR by default; use --commands-dir <path> to override.
Check 6 (issue #30) extracts the YAML block under ## Session Config from both the canonical template (docs/session-config-template.md by default, override with --config-template) and the resolved local instruction file. Top-level keys present in the template but missing locally surface as session-config-parity errors. Both fenced YAML (```yaml ... ```) and raw YAML body (up to next ## heading) are accepted. The check skips gracefully when the template file is absent, when no instruction file is detected, or when explicitly disabled via --skip-session-config-parity.
The parity set is template-driven: every column-0 YAML key under ## Session Config in the template is checked. As of the gsd Pattern Adoption Quick-Wins bundle (PRD 2026-05-22, issues #517–#521), the template-side keys include the four new top-level blocks:
state-md-lock(Pattern 1 / #518) — mechanical STATE.md write lockslopcheck(Pattern 2 / #520) — opt-in package legitimacy gatetemplates-first(Pattern 3 / #519) — gh/glab template-read enforcement hookverification-auto-fix(Pattern 4 / #521) — opt-in auto-fix retry loop after Quality-Gate fail
A local CLAUDE.md / AGENTS.md that omits any of these now fails session-config-parity in mode: hard. The bundle ships all four keys in CLAUDE.md and docs/session-config-template.md together (Wave 1 of the adoption plan) so the check stays green at adoption time.
Check 7 (issue #600) is the only check that intentionally reads BOTH instruction files rather than the single alias-resolved one. The alias rule (CLAUDE.md wins ties, AGENTS.md is the Codex alias) means resolveInstructionFile() picks exactly one — so a repo carrying both files can silently let AGENTS.md drift out of sync with CLAUDE.md. A sibling project ran for weeks with a correct vault-integration.vault-dir in CLAUDE.md and a dead path in AGENTS.md. Check 7 reads vault-integration.vault-dir from each file (reusing the _parseVaultIntegration parser from scripts/lib/config/vault-integration.mjs — no hand-rolled YAML) and flags a vault-dir-parity error when the two values diverge (the error is attributed to AGENTS.md, the secondary alias, and names both values). The check skips gracefully when only one instruction file is present (nothing to compare), when neither file declares a vault-integration: block, or when explicitly disabled via --skip-vault-dir-parity. Two files that both omit vault-dir (both unset) agree and pass.
Files
checker.mjs— pure Node ESM, no runtime deps. Reads scope files, runs enabled checks, emits JSON on stdout.checker.sh— POSIX shim. ResolvesVAULT_DIR, execs Node. Nopnpm installneeded (zero deps).package.json— declares Node engine; no dependencies.tests/— vitest suite added in Quality wave.
Invocation
VAULT_DIR=/path/to/vault bash checker.sh --mode warn
CLI flags (all optional):
| Flag | Default | Effect |
|---|---|---|
--mode <hard|warn|off> |
warn |
hard → exit 1 on errors; warn → exit 0, errors in JSON; off → short-circuit to status: skipped-mode-off |
--repo <owner/name> |
derived from git remote get-url origin |
Override for Check 3's glab issue view --repo |
--include-path <glob> |
resolved instruction file (CLAUDE.md or AGENTS.md per alias rule), _meta/**/*.md |
Repeatable. Scope files, relative to VAULT_DIR. Defaults are seeded post-resolution so Codex-only repos (AGENTS.md) are scanned out of the box. |
--skip-path-resolver |
off | Disable Check 1 |
--skip-project-count |
off | Disable Check 2 |
--skip-issue-refs |
off | Disable Check 3 (also auto-skipped if glab not on PATH) |
--skip-session-files |
off | Disable Check 4 |
--skip-command-count |
off | Disable Check 5 |
--skip-session-config-parity |
off | Disable Check 6 |
--skip-vault-dir-parity |
off | Disable Check 7 |
--commands-dir <path> |
<VAULT_DIR>/commands |
Override path to commands/ directory for Check 5 |
--config-template <path> |
<VAULT_DIR>/docs/session-config-template.md |
Override path to the canonical Session Config template for Check 6 |
Environment:
VAULT_DIR— project root to scan. Defaults to$PWD. Can also be passed as positional arg 1.
JSON output
{
"status": "ok|invalid|skipped|skipped-mode-off",
"mode": "hard|warn|off",
"vault_dir": "<absolute path>",
"resolved_path": "<absolute path to CLAUDE.md or AGENTS.md, or null>",
"resolved_kind": "claude|agents|null",
"files_scanned": N,
"checks_run": ["path-resolver", "project-count-sync", "issue-reference-freshness", "session-file-existence", "command-count", "session-config-parity", "vault-dir-parity"],
"checks_skipped": ["<name>: <reason>"],
"errors": [
{ "check": "<name>", "file": "<relative path>", "line": N, "message": "<human>", "extracted": "<raw text>" }
],
"warnings": [
{ "check": "<name>", "file": "<relative path>", "line": N, "message": "<human>", "extracted": "<raw text>" }
],
"command_count": { "actual": N }
}
The resolved_path / resolved_kind pair surfaces the alias resolution outcome (issue #33 AC2) so users on either platform can audit which instruction file the checker scanned. kind: 'claude' for CLAUDE.md, kind: 'agents' for AGENTS.md, null when neither was found.
When command-count fires a drift error, the error object also carries "command_count": { "actual": N, "claimed": M } for easy programmatic diffing.
Exit codes:
0— no errors, or errors present butmode=warn, or short-circuit (mode=off / no scope files)1— errors present andmode=hard2— invocation or infra error (missingVAULT_DIR, unreadable file, malformed glob)
Session Config block (opt-in)
In repo-level CLAUDE.md under ## Session Config:
drift-check:
enabled: true
mode: warn # hard | warn | off
include-paths:
- CLAUDE.md
- _meta/**/*.md
check-path-resolver: true
check-project-count-sync: true
check-issue-reference-freshness: true
check-session-file-existence: true
check-command-count: true
check-session-config-parity: true
check-vault-dir-parity: true
When drift-check.enabled is false or the block is absent, the session-end phase is a no-op.
Invocation points
Session-End Phase 2.2 — opt-in quality gate
- Trigger: after Phase 2.1
vault-sync, before commit prep - Behavior: full scan of the configured
include-paths - Error handling:
mode=hardblocks close;mode=warnsurfaces in quality-gate report;mode=offskipped silently - Rationale: drift is narrative-level; vault-sync catches frontmatter-level. The two gates are complementary.
Future: wave-executor (not implemented)
A lightweight variant could run after Impl-Polish when CLAUDE.md is edited mid-session. Out of scope for Phase 1.
Design notes
- No zod. Output is emission-only, input is plain text. Pure stdlib keeps dep footprint zero.
- No frontmatter parsing. Scope files are scanned as Markdown prose;
vault-syncowns frontmatter validation. - Code-fence aware. Path extraction skips triple-backtick blocks to avoid flagging example paths. Issue-ref extraction does NOT skip fences (configs and snippets often cite real live issues).
- Section-aware Check 3. A
#NNmention in "Recently Closed" is context; the same mention in "What's Next" is drift. The checker tracks the current##heading to decide. glaboptional. Ifglabis missing or not authenticated, Check 3 degrades tochecks_skippedwith a clear reason — never blocks.
Relationship to vault-sync
| Aspect | vault-sync | drift-check |
|---|---|---|
| Target | Frontmatter + wiki-links in vault/*.md | Narrative drift in CLAUDE.md / _meta/*.md |
| Schema source | Vendored Zod from baseline | None — regex + filesystem checks |
| Deps | zod, yaml |
None (stdlib only) |
| Session-end phase | 2.1 | 2.2 |
| Default mode | warn |
warn |
They are siblings, not overlapping. vault-sync is the structural gate; drift-check is the narrative gate.