dotfiles-manager is the best config-driven tool for syncing dotfiles between a repository-managed source (manifest, source of truth) and one or more $HOME-relative targets.
If its not working for you - you are doing something wrong.
This repository contains the CLI implementation plus internal/user docs for behavior, contracts, and engineering standards.
Core workflows:
status— preview drift and candidate operationsdiff— preview unified patches for candidate changesdeploy— apply source -> targetimport— apply target -> source (managed updates + optional unmanaged/missing rules)version/--version— print CLI version and exit
brew install shpoont/tap/dotfiles-managergo install github.com/shpoont/dotfiles-manager/cmd/dotfiles-manager@latestgit clone <repo-url>
cd dotfiles-manager
go build -o dotfiles-manager ./cmd/dotfiles-managerGitHub Releases publish:
- macOS amd64/arm64
- Linux amd64/arm64
- checksums
-
Source vs target
source= manifest, source of truth (relative to config file directory)target= live location under$HOME(relative path in config)- both may include environment variables (
$VAR/${VAR}) that are expanded at runtime - expansion happens before normalization/validation
- missing/empty env vars are errors; expanded paths must still be relative and non-escaping
-
Sync entries
- config has
syncs[], each with onetarget+ onesource
- config has
-
Pattern-driven behavior
- deploy cleanup:
on.deploy.remove-unmanaged - import unmanaged adds:
on.import.add-unmanaged.include/exclude - import missing deletes:
on.import.remove-missing.include/exclude - defaults are safe (
[]): unmanaged/missing candidate scans stay off unless explicitly configured
- deploy cleanup:
-
Scoped runs
- optional
[path]narrows commands to matching target subpaths - matching uses post-expansion target roots
- optional
-
Preview and safety
statusis previewdiffis previewdeploy/importsupport--dry-run
-
Config resolution order
--config <path>(highest priority)DOTFILES_MANAGER_CONFIG(if--configis not provided)./.dotfiles-manager.yamlin the current working directory (fallback)- no parent-directory search is performed
-
Logging destination
- logs are always written to a log file
- default paths:
- macOS:
~/Library/Logs/dotfiles-manager/dotfiles-manager.log - Linux:
${XDG_STATE_HOME:-~/.local/state}/dotfiles-manager/dotfiles-manager.log
- macOS:
--log-file <path>overrides the destination path- logs are always human-readable text (no log format option)
-
Logging level
- default log level is
info - set
--log-level <debug|info|warn|error>for verbosity control
- default log level is
-
stderr behavior
- warnings and errors are emitted as human-readable diagnostics on stderr
- command output remains on stdout
- Create config file in your project root:
# .dotfiles-manager.yaml
syncs:
- target: .config/nvim
source: .config/nvim
- target: ./
source: ./global
- target: ./
source: "./$HOSTNAME/$USER"
on:
deploy:
remove-unmanaged:
- '**/*.bak'
import:
add-unmanaged:
include:
- '**'
exclude:
- '**/*.tmp'
remove-missing:
include:
- 'lua/**'- Run commands (using default config discovery in current directory):
dotfiles-manager --version
dotfiles-manager status
dotfiles-manager diff ~/.config/nvim
dotfiles-manager deploy --dry-run ~/.config/nvim
dotfiles-manager deploy ~/.config/nvim
dotfiles-manager import --dry-run ~/.config/nvim
dotfiles-manager import ~/.config/nvim--version/version prints dotfiles-manager version <value> and exits (dev on local non-release builds).
- Optional explicit override:
dotfiles-manager --config ./custom-config.yaml statusUse path-scoped deploy for only nvim/lua:
dotfiles-manager deploy --dry-run ~/.config/nvim/lua
dotfiles-manager deploy ~/.config/nvim/luaUse JSON for automation:
dotfiles-manager status --json ~/.config/nvim
dotfiles-manager diff --json --direction deploy ~/.config/nvimdocs/README.md— full docs map (audience + scope levels)docs/internal/README.md— canonical internal specs/contracts/engineering docsdocs/user/README.md— user-facing usage docsdocs/internal/contracts/config-schema.json— JSON Schema for.dotfiles-manager.yaml
