Claude-skill-registry gradual-dotfiles-migration

How to progressively migrate config from dotfiles to home-manager on Darwin. Use when moving shell config, nvim config, or other dotfiles-managed files to workstation.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/gradual-dotfiles-migration" ~/.claude/skills/majiayu000-claude-skill-registry-gradual-dotfiles-migration && rm -rf "$T"
manifest: skills/data/gradual-dotfiles-migration/SKILL.md
source content

Gradual Dotfiles Migration

On Darwin, dotfiles and home-manager coexist. This skill covers how to migrate config safely without breaking the working system.

The Setup

PlatformConfig SourceNotes
DevboxWorkstation (home-manager)Full control, no dotfiles
DarwinDotfiles + workstation overlayGradual migration in progress

On Darwin,

home.darwin.nix
disables several home-manager programs to avoid conflicts:

programs.bash.enable = lib.mkForce false;    # dotfiles owns .bashrc
programs.neovim.enable = lib.mkForce false;  # dotfiles owns nvim
programs.ssh.enable = lib.mkForce false;     # dotfiles owns .ssh/config
# GPG is now fully migrated to home-manager (services.gpg-agent)

The Key Limitation

Home-manager cannot overlay files into a symlinked directory.

If dotfiles creates

~/.config/nvim/lua/user/
as a symlink to the dotfiles repo, home-manager cannot deploy individual files into it. It will try to create a real directory, breaking the symlink and all other files in that directory.

What Doesn't Work

# DON'T DO THIS if dotfiles symlinks the parent directory
xdg.configFile."nvim/lua" = {
  source = "${assetsPath}/nvim/lua";
  recursive = true;
  force = true;  # This won't help - it still breaks the symlink
};

What Works

Option 1: Deploy individual files to a non-symlinked location

# Safe: ccremote.lua is a single file, not inside a symlinked directory
xdg.configFile."nvim/lua/ccremote.lua".source = "${assetsPath}/nvim/lua/ccremote.lua";

Option 2: Let dotfiles own the entire directory

Keep the config in dotfiles until ready to migrate the whole thing at once.

Option 3: Full migration (flip the switch)

# Enable home-manager to own the program entirely
programs.neovim.enable = true;  # Remove the mkForce false

# Now home-manager owns ~/.config/nvim/ - remove from dotfiles first!

Safe Migration Patterns

Pattern 1: Single File Deployment

Best for: Adding one new file that doesn't conflict with dotfiles structure.

# Works: deploying a single file alongside dotfiles-managed files
xdg.configFile."nvim/lua/ccremote.lua".source = "${assetsPath}/nvim/lua/ccremote.lua";

Requires: The target path must not be inside a symlinked directory.

When deploying Lua modules that Lazy needs to load, update the Lazy plugin spec to require it:

-- In deprecated-dotfiles lua/plugins/vim-obsession.lua
{
  "tpope/vim-obsession",
  config = function()
    require("user.sessions")  -- Loads HM-deployed module
  end,
}

Pattern 2: Nix Plugin Without Init.lua Takeover

Best for: Installing a Nix-managed plugin on Darwin while dotfiles still owns init.lua.

Neovim auto-loads plugins from

~/.local/share/nvim/site/pack/*/start/
. Use
xdg.dataFile
to install there:

# Install vim-obsession via Nix, but let dotfiles keep init.lua
xdg.dataFile."nvim/site/pack/nix/start/vim-obsession" = {
  source = pkgs.vimPlugins.vim-obsession;
  recursive = true;
};

Then either:

  • Remove/disable the Lazy spec for that plugin, OR
  • Keep Lazy spec but mark it disabled when Nix version exists

This lets you migrate plugins one-by-one from Lazy to Nix without big-bang neovim migration.

Pattern 3: Parallel Directory

Best for: New functionality that dotfiles doesn't have.

# Works: creating a new directory that dotfiles doesn't manage
xdg.configFile."myapp/config" = {
  source = "${assetsPath}/myapp/config";
  recursive = true;
};

Pattern 5: Full Program Migration

Best for: When you're ready to move everything at once.

  1. On Darwin, remove from dotfiles first:

    cd ~/Code/deprecated-dotfiles
    rm -rf .config/nvim
    git commit -am "chore: migrate nvim to workstation"
    
  2. Enable in home-manager:

    # Remove the mkForce false
    programs.neovim.enable = true;
    
  3. Apply:

    darwin-rebuild switch --flake .#hostname
    

Pattern 6: prepareForHM Cleanup

For files that might exist from dotfiles, add cleanup:

home.activation.prepareForHM = lib.hm.dag.entryBefore ["checkLinkTargets"] ''
  rm -f ~/.config/nvim/lua/ccremote.lua 2>/dev/null || true
'';

Pattern 7: NVIM_APPNAME for Parallel Testing

Best for: Testing a new neovim config without breaking the existing one.

Neovim supports

NVIM_APPNAME
to select a separate config/data/state namespace:

# Run with workstation-managed config
NVIM_APPNAME=nvim-workstation nvim

# Config lives in ~/.config/nvim-workstation/
# Data lives in ~/.local/share/nvim-workstation/

This lets you:

  • Keep dotfiles config untouched as
    nvim
  • Build new HM-managed config as
    nvim-workstation
  • Flip between them with a shell alias
  • Merge when ready

Pattern 8: Resilient Lua Modules

Best for: Shared modules that might load before their dependencies.

When deploying Lua modules that depend on plugins (which may load in different order on Nix vs Lazy), make them resilient:

-- Check if Obsession command exists before using it
vim.api.nvim_create_autocmd("VimEnter", {
  callback = function()
    if vim.fn.exists(":Obsession") == 2 then
      vim.cmd("silent! Obsess")
    end
  end,
})

This removes ordering sensitivity between Nix packpath autoload vs Lazy event loading.

Platform-Specific Config

Use conditionals when behavior differs between platforms:

-- In Lua files
if vim.env.SSH_TTY then
  vim.g.clipboard = "osc52"  -- Remote: use OSC 52
end
-- Local macOS uses native clipboard automatically
-- In Nix
lib.optionalAttrs (!pkgs.stdenv.isDarwin) {
  # Linux-only config
}

Current Migration Status

ProgramDevboxDarwinNotes
NeovimWorkstationDotfiles + overlaysccremote.lua, sessions.lua via Pattern 1
BashWorkstationDotfilesNeed full migration
SSHWorkstationDotfilesNeed full migration
GPGWorkstationWorkstation (pinentry-op)1Password Touch ID integration
TmuxWorkstationWorkstationFully migrated
ClaudeWorkstationWorkstationFully migrated

Detecting Drift

Dotfiles includes a verification script to detect missing or broken symlinks:

cd ~/Code/dotfiles  # or wherever your dotfiles clone is
./verify.sh         # Check all expected symlinks
./install.sh        # Fix any issues

The script:

  • Checks core symlinks (shell, git, vim, nvim, tmux)
  • Allows files to be managed by home-manager (won't flag nix store symlinks)
  • Reports INFO for regular files that might be home-manager managed
  • Returns exit code 1 if any errors found

Run

verify.sh
after migrations or if things break unexpectedly.

Lessons Learned

  1. Don't try recursive overlay into symlinked directories - it breaks everything
  2. Test on Darwin before pushing - devbox success doesn't guarantee Darwin works
  3. Single file deployments are safest during gradual migration
  4. Full migration is cleaner when you're ready to move a whole program
  5. Run verify.sh after migrations - catches missing symlinks early
  6. Nix plugins can coexist with Lazy - use
    xdg.dataFile
    to install to pack path
  7. Update Lazy specs to load HM-deployed modules - use
    config = function() require(...) end
  8. Make shared modules resilient - check if dependencies exist before using them