Drift Detection & Remediation¶
This page documents how this repo detects drift between the chezmoi source-of-truth and what's actually deployed on a host, how to read the report, and how to remediate. Managed under #875.
What "drift" means here¶
Four distinct classes are tracked. The same dot drift command (and
the nightly CI workflow) surfaces all four.
| Class | Meaning | How to detect | Typical fix |
|---|---|---|---|
| Managed drift | A chezmoi-managed file's deployed copy differs from what a fresh chezmoi apply would produce. The standard case. |
chezmoi status (M / MM / A / R rows) |
Either update the source so apply is idempotent, or accept the deployed change and re-add. |
| Untracked source | The chezmoi source tree contains files git doesn't know about — usually in-progress local edits that haven't been committed. | git -C <source-dir> ls-files --others --exclude-standard |
Commit, stash, or .gitignore the file. |
| Orphan deployed | A file under $HOME was previously chezmoi-managed but the source has since been deleted. Chezmoi no longer claims it, so a fresh apply leaves it behind silently. |
Inventoried in ${XDG_STATE_HOME}/dotfiles/orphans (populated by dot heal / dot drift) |
chezmoi remove --force the path, or re-add the source if the file is still wanted. |
| Stale source | The deployed file is newer than its source. The next chezmoi apply would silently revert the user's hand-edit. Reverse-drift trap. |
Compare mtimes for each managed target vs the resolved source-path | Promote the deployed change into the source (chezmoi re-add) or revert the deployed file. |
Reading the report¶
dot drift # human-readable (uses the ui.sh formatting)
dot drift --json # single JSON object — used by the nightly workflow
dot drift --diff # also print `chezmoi diff` for managed drift
JSON shape:
Exit code: 0 if every class is clean; 1 if any drift is found;
2 if a prerequisite (chezmoi, git) is missing.
How the nightly check works¶
.github/workflows/drift-detection.yml runs dot drift --json against
a fresh checkout of main every day at 04:00 UTC. If total != 0
it opens (or updates) a tracking issue labelled
type:chore + priority:medium with the JSON summary, full
chezmoi diff, and chezmoi status attached as a workflow artifact.
The workflow itself ignores the failing exit code (|| true) for the
dashboard step — the actionable signal is the issue, not a red CI
indicator.
Force a local drift check¶
dot drift # default — what you'd run before opening a PR
dot drift --json | jq '.' # for scripting / dashboards
To force a full re-comparison after a tool upgrade or a force-apply:
Historical incidents¶
2026-05-12 — core.hooksPath drift¶
The deployed ~/.gitconfig contained a hooksPath = ~/.git-templates/hooks
line that wasn't in dot_gitconfig.tmpl. The line had been added
directly to the deployed file (manually, not via chezmoi), then sat
silently for weeks while the global commit-msg hook (at
~/.config/git/hooks/commit-msg) never fired — because hooksPath
was pointing at an empty directory. The result: every commit
authored on this machine silently shipped without the
Assisted-by: trailer mandated by dot_claude/CLAUDE.md.
Detection failure: no nightly drift check existed at the time.
Resolution: commit f060683b brought hooksPath into the chezmoi
template; this drift class is exactly what the new stale_source
signal catches going forward.
This incident is the canonical worked example for why the four-class
report exists rather than just chezmoi status.
Configuration surface¶
| Variable | Default | Purpose |
|---|---|---|
DOTFILES_DRIFT_SHOW_DIFF |
0 |
When 1, append chezmoi diff (excluding scripts/install/tests) to the report. Equivalent to --diff. |
References¶
scripts/diagnostics/drift-dashboard.sh— the dashboard itself..github/workflows/drift-detection.yml— the nightly scanner.tests/unit/diagnostics/test_drift_dashboard.sh— JSON contract test.dot heal/dot rollback— drift remediation commands.- Issue #875.