Steps around a module.
Install tmux’s plugins once .tmux.conf is linked, remove them before it goes. Each script is named in the module’s settings.json, and one that fails stops the run.
Dotfiles, from a git repository you own
configfile links each file of your dotfiles repository where your tools expect it, runs your setup scripts, and keeps every machine in sync with the repository.
Nothing is lost on the way. A file already in place is moved aside, and every deployment can be undone.
brew install mindsers/tap/configfile
Open sourceApache-2.0v1.1.0
~/dotfiles
Your working copy. You edit, commit and push here, as in any repository.
git push
github.com/me/dotfiles
The remote: the reference every machine follows.
configfile update
~/.configfile/dotfiles
configfile’s own copy, made identical to the remote.
configfile modules deploy
~/.zshrc → files/zsh/zshrc
A link into that copy, made once. Each update arrives through it.
Your repository, your files
configfile doesn’t store your configuration: it stays in a git repository you own. A module is a folder of files/, and its settings.json lists the module’s files and where each one goes.
files/
editorconfig/
git/
tmux/
zsh/
settings.json
zshrc
scripts/
setup.sh
tmux-plugins.sh
{
"$schema": "https://docs.configfile.sh/schemas/settings.json",
"files": [
{ "source_path": "zshrc", "target_path": "~/.zshrc", "deploy": "global" }
]
}
"global""local".editorconfig."none"A new machine
Captured from a real run, on a machine that already had its own .zshrc and .gitconfig. Replace the repository URL with yours.
01 / Connect
configfile clones it into its own folder and saves the URL in ~/.configfilerc, readable by you only. Any URL git can clone works.
configfile init --repo git@github.com:me/dotfiles.git
$ configfile init --repo git@github.com:me/dotfiles.git Info Cloning git@github.com:me/dotfiles.git into /Users/me/.configfile/dotfiles… Cloning into '/Users/me/.configfile/dotfiles'... done. Done configfile is ready. Configuration saved to /Users/me/.configfilerc.
02 / Deploy
Files in the way are moved aside to .old. The tmux module names a post_deploy script, which runs once its file is in place. Add --dry-run to see all of it first.
configfile modules deploy --all
$ configfile modules deploy --all Info Deploying editorconfig, git, tmux, zsh… - /Users/me/.gitconfig (deployed, previous file moved to /Users/me/.gitconfig.old) - /Users/me/.gitignore (deployed) - /Users/me/.tmux.conf (deployed) Info Running "tmux-plugins", the post_deploy script of tmux… Already installed "tpm" Installing "tmux-sensible" "tmux-sensible" download success - /Users/me/.zshrc (deployed, previous file moved to /Users/me/.zshrc.old) Done Deployment finished.
03 / Set up
Install your tools, set system defaults: whatever files alone can’t do. Scripts live in scripts/, in any language, and know where the repository is.
configfile scripts run setup
$ configfile scripts run setup Info Running "setup"… Setting up macos from /Users/me/.configfile/dotfiles Done Script "setup" finished.
04 / Later
Linked files are current as soon as the sync ends. Unpushed work and conflicts in your working copy never block it.
configfile update
$ configfile update Info Syncing /Users/me/.configfile/dotfiles… Done Synced with origin/main (cb2db8b). Global files are links, so they are up to date; run "configfile modules deploy --all" for new files.
No repository yet? Your first dotfiles repository builds one in ten minutes.
Safe on a machine you care about
.oldA file already where yours goes is moved to ~/.zshrc.old first. A link that’s already right is left alone, so deploying twice is safe.
--dry-runShows what a deployment would change and which scripts would run, without changing anything.
undeployRemoves the links and copies configfile made, and puts your previous file back. Your own files stay where they are.
target_pathYour home folder, the repository and configfile’s own files can’t be a target, whatever path reaches them.
$ configfile modules deploy --all --dry-run Info Dry run, nothing is changed. Deploying editorconfig, git, tmux, zsh would do: - /Users/me/.gitconfig (would be linked, the existing file moved to /Users/me/.gitconfig.old) - /Users/me/.gitignore (would be linked) - /Users/me/.tmux.conf (would be linked) - script "tmux-plugins" (post_deploy of tmux: would run) - /Users/me/.zshrc (would be linked, the existing file moved to /Users/me/.zshrc.old) Done Dry run finished.
$ configfile modules undeploy zsh Info Undeploying zsh… - /Users/me/.zshrc (removed, /Users/me/.zshrc.old restored) Done Undeployment finished.
configfile history recorded for the walkthrough
$ configfile history
2026-10-14 22:55 init --repo git@github.com:me/dotfiles.git ok
cloned git@github.com:me/dotfiles.git into ~/.configfile/dotfiles
saved configuration ~/.configfilerc
2026-10-14 22:55 modules deploy --all ok
linked ~/.gitconfig (previous file moved to ~/.gitconfig.old)
linked ~/.gitignore
linked ~/.tmux.conf
script tmux-plugins exit 0 (post_deploy of tmux)
linked ~/.zshrc (previous file moved to ~/.zshrc.old)
2026-10-14 22:55 scripts run setup ok
script setup exit 0
2026-10-14 22:55 update ok
synced ~/.configfile/dotfiles with origin/main (21fae97 → cb2db8b)
Script arguments, environment variables and file contents are never recorded, and passwords in URLs are hidden.
Beyond linking files
Install tmux’s plugins once .tmux.conf is linked, remove them before it goes. Each script is named in the module’s settings.json, and one that fails stops the run.
Next to setup.sh, add setup.macos.sh or setup.linux.sh, and each machine runs its own. Any language with a shebang line.
Copy an .editorconfig into the project you’re in. A copy you changed is only replaced after you say yes, and kept as .old.
Point a settings.json at its JSON Schema, and VS Code, Zed or a JetBrains IDE suggests the keys and flags a typo as you type.
configfile resets its copy to the remote. Anything changed inside it, such as an edit made through a link, is first saved as a patch in ~/.configfile/saved/.
One line of JSON per run: what was linked, copied or moved aside, which commits were synced, and each script’s exit code.
{
"$schema": "https://docs.configfile.sh/schemas/settings.json",
"files": [
{ "source_path": "tmux.conf", "target_path": "~/.tmux.conf", "deploy": "global" }
],
"post_deploy": "tmux-plugins"
}
#!/bin/sh set -e tpm="$HOME/.tmux/plugins/tpm" [ -d "$tpm" ] || git clone --quiet https://github.com/tmux-plugins/tpm "$tpm" "$tpm/bin/install_plugins"
No one at the keyboard
Without a terminal, configfile asks nothing: a command that needs an answer fails and names the option to pass instead. It exits non-zero when something fails, so set -e stops at the first problem.
In CI, a dry run in a throwaway home folder reads every module and checks every target: a broken settings.json fails the push, not your next new machine.
$ configfile modules deploy < /dev/null Error No module given. Pass module names, or --all to deploy every module.
name: Check dotfiles
on: [push, pull_request]
permissions:
contents: read
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
- run: npm install --global https://github.com/mindsers/configfile/releases/download/1.1.0/configfile-1.1.0.tgz
# The following steps use a throwaway home folder, not the runner's.
- run: |
mkdir -p "$RUNNER_TEMP/home"
echo "HOME=$RUNNER_TEMP/home" >> "$GITHUB_ENV"
- run: configfile init --repo "$GITHUB_WORKSPACE"
- run: configfile modules deploy --all --dry-run
Before you install
configfile changes files in your home folder. Here is what it does with them, and what it won’t do.
How it worksNo. They stay in a git repository you own, on GitHub, GitLab or any host git can clone from. configfile keeps a copy of it in ~/.configfile/dotfiles and deploys from there.
Deploying from your working copy would let uncommitted edits, a branch in progress or a conflict block every sync, and send changes to this machine before you pushed them. The copy only follows the remote. Don’t edit it: what changes in it, including edits made through a link, is saved as a patch in ~/.configfile/saved/ at the next update, ready for git am in your working copy. How it works.
A file, folder or link where a global file goes is moved to .old (then .old.1, .old.2…) before the link is made. A local copy that differs is only replaced after you answer yes, or with --force. configfile modules undeploy puts your previous file back.
Treat it like running their code. The files end up in your shell’s and tools’ configuration, and the scripts run with your permissions, including those a module runs when it is deployed. Read the repository first, and use --dry-run to see which scripts would run, or --no-hooks to run none. Safety and trust.
macOS and Linux, on x64 or arm64, with git. Installed with Homebrew or npm, it runs on Node.js 24.11 or later, which Homebrew installs with it. The standalone binary brings its own Node.js and needs macOS 13.5 or later, or Linux with the GNU C library 2.28 or later; on Alpine, install with npm. Windows is not supported.
Your repository’s layout and settings.json files work as before, and links made by 0.3 are recognised. If your configuration points at your own working copy, give configfile its own copy first. Upgrading from 0.3.
Yes: free and open source, under the Apache-2.0 license, and built in its author’s free time. If it saves you a setup, you can sponsor its development.
Try it on your dotfiles
Install configfile, point it at your dotfiles repository, and check what it would change before it changes anything.
You’ll need macOS or Linux, git and a dotfiles repository. Installed with Homebrew or npm, configfile also needs Node.js 24.11 or later, which Homebrew installs; the standalone binary brings its own.
configfile init --repo git@github.com:me/dotfiles.git
configfile modules deploy --all --dry-run
configfile modules deploy --all
Replace the URL with your repository’s. Then run your setup script, if you have one, with configfile scripts run setup.