Keep your shell, editor, and tool config in sync across every machine you use.
Dotfiler manages a git repository of your dotfiles, unpacking them as symlinks
into your home directory. When you edit ~/.vimrc, you're editing the file in
your repo — commit and push, and every other machine can pull and instantly get
the update. On a new machine, clone your repo and run one command to recreate
every symlink.
On top of that, dotfiler auto-checks for updates at login and offers to pull them in — so your dotfiles stay current without you having to think about it.
Why not GNU Stow? Stow does the symlink-tree part fine. Dotfiler adds the auto-update-on-login loop, a modular install system for bootstrapping new machines (packages, languages, apps), and a GUI for exploring and managing tracked files. If you just want symlinks, Stow is simpler. If you want a full dotfile lifecycle — track, sync, install, update — dotfiler has you covered.
zdot: dotfiler integrates with zdot, a modular zsh configuration manager, but does not require it. See docs/zdot-integration.md for details.
- Authoring install files
- How updates work — user perspective, deployment tradeoffs
- Update hooks — authoring hooks, internals, API reference
- zdot integration
# Clone your dotfiles repo (or create one)
git clone <your-repo> ~/.dotfiles && cd ~/.dotfiles
# Add dotfiler as a git submodule inside the repo
git submodule add https://github.com/georgeharker/dotfiler .nounpack/dotfiler
chmod +x .nounpack/dotfiler/dotfiler
# Copy exclusionn rules
cp ~/.dotfiles/.nounpack/dotfiler/dotfiles_exclude ~/.dotfiles/
# Track some dotfiles and create symlinks
.nounpack/dotfiler/dotfiler setup -i ~/.zshrc ~/.vimrc ~/.gitconfig
.nounpack/dotfiler/dotfiler setup -u
# Enable auto-update on login (add to your .zshrc)
echo '[[ -f ~/.dotfiles/.nounpack/dotfiler/check_update.zsh ]] && source ~/.dotfiles/.nounpack/dotfiler/check_update.zsh' >> ~/.zshrc
# Enable shell completions (optional)
echo 'source ~/.dotfiles/.nounpack/dotfiler/completions.zsh' >> ~/.zshrcKeeps dotfiler as a versioned dependency inside your dotfiles repo.
cd ~/.dotfiles
git submodule add https://github.com/georgeharker/dotfiler .nounpack/dotfiler
chmod +x .nounpack/dotfiler/dotfiler
git commit -m "Add dotfiler as submodule"To update dotfiler later:
cd ~/.dotfiles/.nounpack/dotfiler
git pull
cd ~/.dotfiles
git add .nounpack/dotfiler
git commit -m "Update dotfiler"On a new machine, after cloning your dotfiles repo:
git submodule update --init --recursiveEmbeds dotfiler's history directly into your dotfiles repo — no submodule dependency at clone time.
cd ~/.dotfiles
git remote add dotfiler https://github.com/georgeharker/dotfiler.git
git subtree add --prefix=.nounpack/dotfiler dotfiler main --squash
chmod +x .nounpack/dotfiler/dotfilerTo update:
git subtree pull --prefix=.nounpack/dotfiler dotfiler main --squashRequired: Tell dotfiler which remote to track for self-updates. Add this
to your .zshrc before sourcing check_update.zsh:
zstyle ':dotfiler:update' subtree-remote 'dotfiler main'Without this, dotfiler cannot detect that it is installed as a subtree and will silently skip self-update checks.
Simplest option: just clone dotfiler somewhere and point your dotfiles at it.
git clone https://github.com/georgeharker/dotfiler ~/.dotfiler
chmod +x ~/.dotfiler/dotfilerThen configure your dotfiles repo to find the scripts:
# In your .zshrc, before sourcing check_update.zsh:
zstyle ':dotfiles:scripts' path "$HOME/.dotfiler"Clone your repo and restore all symlinks in one go:
git clone <your-repo> ~/.dotfiles
cd ~/.dotfiles
git submodule update --init --recursive # if using submodule
chmod +x .nounpack/dotfiler/dotfiler .nounpack/dotfiler/.zsh
.nounpack/dotfiler/dotfiler setup -uThen optionally bootstrap your full environment:
.nounpack/dotfiler/dotfiler installdotfiler setup [options]
| Flag | Long form | Description |
|---|---|---|
-s |
--setup |
Auto-ingest dotfiles found under ~/ |
-i path … |
--ingest path … |
Track specific files and create symlinks |
-u [file …] |
--unpack [file …] |
Create/restore symlinks (respects exclusions) |
-U [file …] |
--force-unpack [file …] |
Force-unpack, ignoring exclusions |
-t path … |
--track path … |
Track without creating a symlink |
-x path … |
--untrack path … |
Untrack (remove from repo management) |
-d |
--diff |
Show diff between repo and home |
-D |
--dry-run |
Show what would happen without doing it |
-q |
--quiet |
Suppress non-error output |
-y |
--yes |
Default yes to all prompts |
-n |
--no |
Default no to all prompts |
Examples:
dotfiler setup -i ~/.zshrc ~/.gitconfig # Track and link specific files
dotfiler setup -u # Restore all symlinks
dotfiler setup -u -D # Dry run: show what unpack would do
dotfiler setup -s -y # Auto-ingest, answer yes to all prompts
dotfiler setup -x ~/.old-secret # Stop tracking a fileUsually sourced automatically at login (see Auto-Update on Login), but can also be run directly:
dotfiler check-updates [options]
| Flag | Long form | Description |
|---|---|---|
-f |
--force |
Force check, ignoring the timestamp cache |
-v |
--verbose |
Show progress output |
-d |
--debug |
Show debug tracing (implies --verbose) |
dotfiler check-updates # Check against git remote
dotfiler check-updates --force # Ignore cache, check now
dotfiler check-updates --verbose # Show progress output
dotfiler check-updates --debug # Show full debug tracingdotfiler update [options]
| Flag | Long form | Description |
|---|---|---|
-q |
--quiet |
Suppress non-error output |
-v |
--verbose |
Verbose output |
-D |
--dry-run |
Show what would happen without doing it |
-c hash |
--commit-hash hash |
Replay a specific commit (manual use — no git pull) |
-r range |
--range range |
Replay an arbitrary revision range (manual use — no git pull) |
dotfiler update # Default: fetch → diff pending commits → git pull → re-unpack
dotfiler update -D # Dry run — print what would change, touch nothing
dotfiler update -c abc1234 # Replay a single commit's file changes into $HOME (no pull)
dotfiler update -r HEAD~3..HEAD # Replay an arbitrary range's file changes (no pull)Default mode (dotfiler update with no flags) is the normal upgrade path:
it fetches the tracked remote, computes the diff of all incoming commits, runs
git pull, then re-unpacks only the files that changed. Nothing is re-linked
unnecessarily.
-c / -r modes are strictly for manual, surgical use — replaying a
specific commit (or range) into $HOME without touching git history. They are
never called by the auto-update machinery.
--dry-run suppresses the pull and all filesystem writes in every mode.
Only files that changed in the relevant commits get re-unpacked — fast and safe.
If any .nounpack/install/.zsh scripts changed, dotfiler warns you to re-run
dotfiler install.
dotfiler install [--force]
dotfiler install-module <name> [--force]
dotfiler install # Run all install modules in order
dotfiler install --force # Reinstall even if already present
dotfiler install-module shell-utils # Run one module by name
dotfiler install-module shell-utils --force # Force reinstall one modulepip install -r .nounpack/dotfiler/requirements.txt
dotfiler guiGUI features:
- Add Mode: Browse the filesystem and track config files
- Manage Mode: View status (linked, broken, conflicted) of tracked files
- Batch Operations: Select multiple files for tracking or unlinking
Controls: ↑↓←→ navigate, Space/Enter select, I track, F file info, Q quit.
check_update.zsh must be sourced (not executed) so it can interact with
the current shell — prompt the user, run zsh hooks, etc.
Add this to your .zshrc, after any zstyle configuration:
[[ -f ~/.dotfiles/.nounpack/dotfiler/check_update.zsh ]] && \
source ~/.dotfiles/.nounpack/dotfiler/check_update.zshThat's all that's required. On each new login shell, dotfiler checks whether the remote has new commits, then acts based on your configured mode.
If you're using the standalone install, point to the script directly:
[[ -f ~/.dotfiler/check_update.zsh ]] && source ~/.dotfiler/check_update.zshControl the behaviour with zstyle (set this before the source line):
zstyle ':dotfiler:update' mode 'prompt' # ask [Y/n] at login — default
zstyle ':dotfiler:update' mode 'auto' # pull silently, no interaction
zstyle ':dotfiler:update' mode 'background' # fetch and apply in background subshell
zstyle ':dotfiler:update' mode 'reminder' # print a reminder only, never update
zstyle ':dotfiler:update' mode 'disabled' # skip the check entirely| Mode | Behaviour |
|---|---|
prompt |
Asks [Y/n] at login. Default is Y. If you have already typed input when the prompt fires, shows a reminder instead. |
auto |
Fetches and applies updates silently in the foreground at login. |
background |
Launches the update check and apply in background subshells. The result (success or error) is surfaced on the next prompt via a precmd hook, so the login shell is never blocked. |
reminder |
Prints a message but never pulls. Useful if you prefer manual control. |
disabled |
Does nothing. No network activity. |
Oh-My-Zsh users: dotfiler falls back to reading :omz:update mode if no
:dotfiler:update mode is set. The legacy env vars DISABLE_UPDATE_PROMPT=true
(→ auto) and DISABLE_AUTO_UPDATE=true (→ disabled) are still honoured.
To see exactly what check_update.zsh is doing at login, enable debug output
in any of these ways:
# 1. Environment variable — set before opening a shell (e.g. in .zshenv)
export DOTFILER_DEBUG=1 # debug tracing
export DOTFILER_VERBOSE=1 # progress output only
# 2. Flag — when running check-updates directly
dotfiler check-updates --verbose
dotfiler check-updates --debugWith DOTFILER_DEBUG set, every phase is printed with a [debug] prefix so
you can trace exactly which branch is taken, what lock files are acquired, and
whether an update is detected. DOTFILER_VERBOSE shows higher-level progress
without the low-level tracing.
By default, dotfiler checks at most once per hour (3600 seconds). Override with:
zstyle ':dotfiler:update' frequency 86400 # once per day
zstyle ':dotfiler:update' frequency 3600 # once per hour (default)The timestamp is stored in ${XDG_CACHE_HOME:-$HOME/.cache}/dotfiles/dotfiles_update.
Delete that file or run dotfiler check-updates --force to trigger an immediate check.
- Attempts a
git fetchof the tracked remote and branch (silent). - Compares local
HEADagainstremote/branch— if they differ, updates are available. - If
git fetchfails (e.g. no network): falls back to the GitHub REST API (curl/wget) to compare SHAs. - If no network tools are available at all: assumes updates are available (fail-open).
A lock file (~/.cache/dotfiles/update.lock) prevents concurrent update runs.
Locks older than 24 hours are automatically removed.
Set GH_TOKEN in your environment to use an authenticated request for the
GitHub API fallback (step 3 above). This raises the rate limit from 60 to
5 000 requests per hour and avoids throttling on shared IP addresses:
export GH_TOKEN=ghp_xxxxxxxxxxxxxxxxxxxx # in .zshenv or similarWhen mode is background, the update check and apply run in background
subshells so the login shell is never blocked. The result is surfaced on the
next prompt via a precmd hook.
If you have already typed input into the prompt when the result arrives,
dotfiler will not interrupt you with an interactive [Y/n] question. Instead
it falls back to a reminder message (same as reminder mode) and lets you
decide when to run dotfiler update manually.
- Mode is
disabled - The dotfiles directory is not owned or writable by the current user
- Not running in an interactive terminal (
ttycheck) gitis not installed- The dotfiles directory is not a git repo
Dotfiler includes a numbered module system for bootstrapping a full development environment on a new machine. Copy the example templates, customise them, then commit them to your dotfiles repo alongside your config.
# Copy templates into your dotfiles repo
cp -r .nounpack/dotfiler/example_install/ .nounpack/install/
# Customise as needed
vim .nounpack/install/02-shell-utils.zsh
# Run all modules
dotfiler install
# Or run a single module
dotfiler install-module shell-utilsAvailable modules (in run order):
| Module | Purpose |
|---|---|
00-dotfiler-install.zsh |
Symlink dotfiler command to ~/bin |
01-package-manager.zsh |
Homebrew (macOS) / APT + extras (Linux), fonts |
02-shell-utils.zsh |
eza, fzf, zoxide, antidote, etc. |
03-development-tools.zsh |
git, delta, cmake, and friends |
04-editors-terminals.zsh |
tmux, neovim, terminal emulators |
05-programming-languages.zsh |
Language runtimes and version managers |
06-applications.zsh |
End-user applications |
07-post-install.zsh |
Final configuration and cleanup |
Install scripts are idempotent by default — already-installed items are skipped.
--force triggers reinstall (brew reinstall, cargo install -f, etc.).
See example_install/README.md for full documentation of the helper functions
available inside install modules.
All configuration is via zstyle. Add these to your .zshrc before sourcing
check_update.zsh:
# Override the dotfiles repo path (default: auto-detected)
zstyle ':dotfiles:directory' path '/path/to/dotfiles'
# Override the scripts directory (default: .nounpack/dotfiler inside the dotfiles repo)
# Can be an absolute path or relative to ':dotfiles:directory' path
zstyle ':dotfiles:scripts' path '/path/to/scripts'
# Override the install modules directory
# Can be absolute or relative to ':dotfiles:directory' path
zstyle ':dotfiles:install' directory '/path/to/install'
# Override the exclusions file
# Can be absolute or relative to ':dotfiles:directory' path
zstyle ':dotfiles:exclude' path '/path/to/dotfiles_exclude'
# Update behaviour
zstyle ':dotfiler:update' mode 'prompt' # auto | prompt | background | reminder | disabled
zstyle ':dotfiler:update' frequency 86400 # check interval in seconds
# Subtree remote (required when dotfiler is embedded as a git subtree)
zstyle ':dotfiler:update' subtree-remote 'dotfiler main'
# Hooks directory (optional — for component check-update / update hooks)
zstyle ':dotfiler:hooks' dir "$HOME/.config/dotfiler/hooks"Dotfiler uses gitignore-style patterns to decide which files to skip during
auto-ingest and unpack. The default exclusion file is dotfiles_exclude in
your dotfiles repo root.
# Copy the example exclusion file into your repo root
cp ~/.dotfiles/.nounpack/dotfiler/dotfiles_exclude ~/.dotfiles/Example patterns:
.git/ # Never track version control internals
.nounpack/ # Never track dotfiler itself
dotfiles_exclude # Never track the exclusion file
node_modules/ # Dependencies
.DS_Store # macOS metadata
*.swp # Vim swap files
.vscode/ # IDE state
Pattern types:
- Directory: ends with
/— matches the directory and all its contents - Path: contains
/— matched against the full relative path - Name: no
/— matched against the filename only - Glob: standard shell wildcards apply
Override the file location:
zstyle ':dotfiles:exclude' path '/path/to/my-exclusions.txt'# Add to .zshrc
source ~/.dotfiles/.nounpack/dotfiler/completions.zshProvides tab completion for all commands and options:
dotfiler <TAB> # gui, setup, check-updates, update, install, …
dotfiler setup -<TAB> # -i, -s, -u, -U, -t, -x, -D, … with descriptions
dotfiler setup -i <TAB> # file completion
dotfiler check-updates --<TAB> # --force, --debug, --help~/.dotfiles/
├── .nounpack/
│ ├── scripts/ # Dotfiler (submodule, subtree, or symlink)
│ │ ├── dotfiler # Main command dispatcher
│ │ ├── setup.zsh
│ │ ├── update.zsh
│ │ ├── check_update.zsh
│ │ ├── install.zsh
│ │ ├── helpers.zsh
│ │ ├── completions.zsh
│ │ ├── dotfiles_exclude
│ │ └── example_install/
│ └── install/ # Your customised install modules (committed to repo)
│ ├── 00-dotfiler-install.zsh
│ └── …
├── dotfiles_exclude # Your exclusion patterns
├── .zshrc # Tracked dotfiles (symlinked to ~/)
├── .vimrc
└── .config/
└── nvim/init.lua
dotfiler setup -i ~/.config/newsoftware/config.toml
git add -A && git commit -m "Track newsoftware config"
git pushBecause tracked files are symlinks, edits go directly into your repo:
vim ~/.vimrc
cd ~/.dotfiles
git add .vimrc && git commit -m "Update vim config"
git push# Manually:
dotfiler update
# Or just open a new shell — check_update.zsh handles it at logingit clone <your-repo> ~/.dotfiles
cd ~/.dotfiles
git submodule update --init --recursive
chmod +x .nounpack/dotfiler/dotfiler .nounpack/dotfiler/.zsh
.nounpack/dotfiler/dotfiler setup -u
.nounpack/dotfiler/dotfiler install# Scripts not executable after clone
chmod +x ~/.dotfiles/.nounpack/dotfiler/dotfiler ~/.dotfiles/.nounpack/dotfiler/.zsh
# Re-create all symlinks
dotfiler setup -u
# Force update check
dotfiler check-updates --force
# See what setup would do without making changes
dotfiler setup -u -D
# Inspect git state
cd ~/.dotfiles && git status