| name | dotfiles-sync |
| description | Use when reconciling chezmoi local-vs-remote drift, reviewing pending machine-state changes, or performing safe remote-first dotfiles sync across machines. |
Workflow: Dotfiles Sync
NEVER REBASE. ALWAYS MERGE.
Terminology
| Term | Meaning | Path |
|---|
| local | The actual dotfiles on this machine (chezmoi target) | $HOME/.<file> |
| remote | The chezmoi source repo, git-tracked and pushed | $HOME/.local/share/chezmoi |
Always use "local" and "remote" when presenting drift reports. Never use chezmoi's internal "source/target" terminology in user-facing output.
Trigger
Run this workflow when you need to:
- reconcile local-vs-remote dotfile drift
- inspect pending chezmoi changes before apply
- perform a safe remote-first sync across machines
Inputs
- Chezmoi remote repo at
$HOME/.local/share/chezmoi
- Local state under
$HOME
dotfiles helper available on PATH
- Optional handoff context in
.lev/pm/handoffs/
- Workstream state at
.lev/pm/workstreams/dotfiles-sync/state/workstream.yaml
Steps
Phase 1: Scan + Auto-Classify (immediate, no user interaction)
-
Run dotfiles status and dotfiles sync in parallel.
-
Run dotfiles inspect chezmoi-diff to get full diff content for every entry.
-
Auto-classify every diff into exactly one of these buckets. There is NO "manual review" bucket — every diff MUST be classified:
- remote-only — exists in remote, not reflected locally. This does not automatically mean local intentionally removed it; it can also mean this machine never installed or does not use an optional file.
- local-drift — local changed intentionally, remote is stale (MM where target newer)
- conflict — both changed in the same area (MM where both recent, content diverges structurally)
- volatile — runtime noise:
.tmp, installation_id, cache files, lockfiles, IDE state (auto NO-GO, silent skip)
- script-rename — chezmoi script naming convention change (R status). Inspect source to confirm equivalence, then classify as GO.
- intentional-removal — file deleted locally on purpose (check memory + handoff history for prior decisions before asking user)
Classification rules:
- If the path is in
.chezmoiignore, skip entirely — it should not appear in the report.
- If the path matches known volatile patterns (
.tmp, installation_id, plugins/, cache/, *.log, *.lock), classify as volatile automatically.
- If the diff shows only whitespace, line-ending, or timestamp changes, classify as volatile.
- If DA and source is newer, inspect intent before recommending apply. Local absence can mean "not installed on this machine", not "removed globally". Example:
.config/opencode/oh-my-opencode.json existed only in remote while local opencode.json was newer; the correct action was to forget the remote-only optional file, not apply it locally.
- If MM and target is newer, classify as local-drift.
- If R (rename), read both old and new source files to determine if content is equivalent or changed.
- If any parent source path starts with
exact_, audit the parent ownership before classifying child diffs. Exact directories make local runtime files appear as delete/re-add drift. Example: exact_dot_codex caused Codex sqlite/native-host/runtime files to recur every sync; the correct action was to convert to non-exact dot_codex/AGENTS.md, not classify every runtime file independently.
- Treat
run_onchange_* scripts as executable side effects, not normal file updates. Inspect the script before GO. Example: the QMD installer was narrow; install-deps triggered broad Homebrew upgrades and was not safe as a QMD-scoped action.
- Check
.lev/pm/handoffs/ and memory for prior decisions about specific paths before classifying ambiguous items.
-
For each classified diff, determine GO/NO-GO:
- remote-only → GO only when the remote file is clearly desired on this machine; otherwise ESCALATE or intentional-removal/forget
- local-drift → GO (re-add from local to remote)
- conflict → ESCALATE (present both sides, require explicit user decision)
- volatile → NO-GO (skip silently, recommend adding to
.chezmoiignore if not already there)
- script-rename → GO only after side-effect review confirms the script is safe for this sync scope
- intentional-removal → GO (remove from remote via
chezmoi forget)
Phase 2: Enriched Report (first response to user)
Present a single enriched report. Use /now or a clear table format. The report MUST include:
DOTFILES SYNC REPORT — {hostname} — {date}
SUMMARY
Total diffs: N
Auto-resolved: N (GO)
Escalations: N (need your call)
Volatile skipped: N
ACTIONS (GO)
| # | Path | Direction | What changes | Classification |
|---|------|-----------|-------------|----------------|
| 1 | .claude/settings.json | local → remote | +3 hooks, -1 deprecated key | local-drift |
| 2 | .codex/AGENTS.md | local → remote | updated agent list | local-drift |
| ... | | | | |
ESCALATIONS (need decision)
| # | Path | Local intent | Remote intent | My recommendation |
|---|------|-----------|-------------|-------------------|
| (none if clean) | | | | |
VOLATILE (auto-skipped)
| Path | Reason |
|------|--------|
| .codex/.tmp | runtime temp dir |
CHEZMOIIGNORE CANDIDATES
| Path | Why |
|------|-----|
| (paths that keep showing up as volatile but aren't in .chezmoiignore yet) |
For escalations, explain intent, not just timestamps. Timestamp is evidence only. Example: local OpenCode config newer than remote means local may win, but a single remote package-name correction can still be merged intentionally.
End the report with a single go/no-go prompt:
- If zero escalations: "All N actions are auto-classified GO. Execute? [y/n]"
- If escalations exist: "N actions ready, M need your call. Resolve escalations first, then execute."
Phase 3: Execute (only after user confirms)
- Re-add local-drift files to remote:
chezmoi re-add <paths>
- Remove intentional-removal files from remote:
chezmoi forget --force <paths>
- Review
git -C ~/.local/share/chezmoi status --short
- Commit with descriptive message
- Fetch, then merge remote changes (NEVER rebase)
- Push
- Only after remote is settled:
chezmoi apply --force for remote-only and script-rename paths
- Re-run
dotfiles sync to verify clean state
- Report final state
Team
| Role | Responsibility |
|---|
| dotfiles operator | Scans, auto-classifies ALL diffs, builds enriched report |
| git reconciler | Commits, merges, pushes the chezmoi remote repo |
| escalation agent | Only activates when true conflicts exist (both sides changed structurally) |
Outputs
- Enriched drift report (first response, always)
- Updated chezmoi remote repo state
- Updated local home state after apply
- Dry-run report at
~/.local/state/lev/dotfiles-sync-plan.latest.txt
- Workstream state update at
.lev/pm/workstreams/dotfiles-sync/state/workstream.yaml
Validation
- Every diff is classified — zero items in any "manual review" or "unclassified" bucket
dotfiles sync completes clean after execution
- Any committed remote changes are pushed successfully
chezmoi apply --force runs only after remote is reconciled
- True conflicts (both sides changed structurally) are escalated, never auto-merged
- Final
dotfiles status reflects either clean state or explicitly accepted volatile noise
- Volatile paths not in
.chezmoiignore are flagged as candidates for addition