Managing Team Dotfiles With chezmoi
A new hire copies a colleague's ~/.gitconfig from a gist and pushes their first commits as Jane Doe <[email protected]>, which the hosting service does not link to their work account; another developer's version manager never activates because their ~/.zshrc is missing one line the rest of the team has. Symlinking a shared dotfiles directory fixes neither, because the files that need sharing also need per-machine differences. chezmoi manages dotfiles from a repository with templates, so a team can share defaults while each machine renders its own values. This page sets that up, as part of workstation provisioning and team dotfiles.
The model is two layers: a team dotfiles repository with shared defaults and templates, and per-developer data (name, work email, preferences) that chezmoi prompts for once and stores locally.
Diagnostic
Compare the configuration that matters on two machines, and see what chezmoi would change if it were already in use:
#!/usr/bin/env bash
set -euo pipefail
git config --global --get-regexp '^(user\.|commit\.gpgsign|gpg\.format|pull\.rebase|init\.defaultBranch)' || true
grep -nE 'mise activate|direnv hook|brew shellenv' ~/.zshrc ~/.zprofile 2>/dev/null || echo "no toolchain activation lines found"
command -v chezmoi >/dev/null && chezmoi status 2>/dev/null | head -10 || echo "chezmoi not in use"
Expected bad output on a machine set up by hand:
user.name Jane Doe
user.email [email protected]
pull.rebase false
no toolchain activation lines found
chezmoi not in use
The Git identity is personal, commits are unsigned, pull.rebase differs from the team convention, and neither mise nor direnv is activated in the shell — so pinned project tools silently fall back to system versions.
Root cause
Dotfiles combine three kinds of content: team conventions (rebase on pull, signed commits, toolchain activation, aliases), per-person identity (name, email, signing key), and per-machine details (Homebrew prefix, OS-specific credential helpers, work versus personal context). Copying a file shares all three, including the parts that must differ. Symlinking a directory shares everything verbatim, so any per-machine difference becomes a local edit to a tracked file that either gets committed by mistake or blocks updates. Without a tool that separates the layers, teams give up on sharing and each machine drifts independently, which shows up as bugs that depend on who ran the command.
Resolution
- Create the team dotfiles repository with chezmoi's layout. Files named
dot_*map to~/.*, and a.tmplsuffix marks templates:
#!/usr/bin/env bash
set -euo pipefail
chezmoi init
cd "$(chezmoi source-path)"
cat > .chezmoi.toml.tmpl <<'EOF'
{{- $name := promptStringOnce . "name" "Full name" -}}
{{- $email := promptStringOnce . "email" "Work email" -}}
[data]
name = {{ $name | quote }}
email = {{ $email | quote }}
EOF
git remote add origin [email protected]:acme/dotfiles.git
promptStringOnce asks on the first chezmoi init and stores the answer in the local config, never in the repository.
- Template the Git config so identity comes from local data and conventions are shared:
[user]
name = {{ .name }}
email = {{ .email }}
signingkey = ~/.ssh/id_ed25519.pub
[gpg]
format = ssh
[commit]
gpgsign = true
[pull]
rebase = true
[init]
defaultBranch = main
[include]
path = ~/.gitconfig.local
Save as dot_gitconfig.tmpl. The [include] of ~/.gitconfig.local gives each developer an untracked place for personal settings that the team file does not manage.
- Template the shell profile with OS and architecture conditions, including toolchain activation:
{{- if eq .chezmoi.os "darwin" }}
{{- if eq .chezmoi.arch "arm64" }}
eval "$(/opt/homebrew/bin/brew shellenv)"
{{- else }}
eval "$(/usr/local/bin/brew shellenv)"
{{- end }}
{{- end }}
eval "$(mise activate zsh)"
eval "$(direnv hook zsh)"
[ -f ~/.zshrc.local ] && source ~/.zshrc.local
Save as dot_zshrc.tmpl. Personal aliases and prompts go in ~/.zshrc.local.
- Apply on a new machine in one command, which clones, prompts once and renders everything:
#!/usr/bin/env bash
set -euo pipefail
sh -c "$(curl -fsLS get.chezmoi.io)" -- -b "$HOME/.local/bin" init --apply acme/dotfiles
"$HOME/.local/bin/chezmoi" diff --no-pager | head -20 || true
git config --global --get user.email
- Keep secrets out. Templates can read secrets from a password manager at apply time (
{{ onepasswordRead "op://dev/npm/token" }}), so values never enter the repository. Anything personal and sensitive stays in the.localfiles.
Expected output
$ git config --global --get-regexp '^(user\.email|commit\.gpgsign|pull\.rebase)'
user.email [email protected]
commit.gpgsign true
pull.rebase true
$ zsh -ic 'command -v mise direnv'
/opt/homebrew/bin/mise
/opt/homebrew/bin/direnv
$ chezmoi status
$
The Git identity is the work account, team conventions apply, toolchain activation is present, and chezmoi status prints nothing — the machine matches the rendered source exactly.
When the team changes a convention — say, adding rerere.enabled = true to the Git template — each developer picks it up with chezmoi update, which pulls the repository and re-renders. Anyone who had edited ~/.gitconfig directly sees the conflict in chezmoi diff before anything is overwritten, which is the moment to move that personal setting into ~/.gitconfig.local where it belongs.
Prevention
Run
chezmoi statusin the doctor script and printchezmoi diffguidance when files have drifted, so local edits are noticed and either upstreamed or moved into.localfiles.Scan the dotfiles repository for secrets in CI with the same scanner used for application code, as described in blocking committed secrets with a gitleaks pre-commit hook.
Test rendering for each OS in CI with
chezmoi execute-templateand fixed data, so a template error does not break every machine on the next update.
Platform caveats
macOS: the default shell is zsh; template
dot_zshrcanddot_zprofile. Apple Silicon and Intel Homebrew paths differ; the.chezmoi.archcondition handles both.
WSL2: run chezmoi inside the distribution for the Linux shell and Git config. Windows-side Git (used by some Windows editors) has its own
.gitconfig; either configure it separately or make the editor use WSL's Git.
Linux: distributions differ in where packages install; prefer
command -vchecks in templates over hard-coded paths.
Apple Silicon (ARM64): nothing else differs for chezmoi itself, which is a single Go binary with native builds.
Rollback
chezmoi does not delete files it did not create. To stop managing dotfiles, remove its source and config; the rendered files stay as they are and can be edited by hand:
#!/usr/bin/env bash
set -euo pipefail
chezmoi purge --force
ls -la ~/.gitconfig ~/.zshrc
Frequently Asked Questions
How is chezmoi different from symlinking a dotfiles directory?
chezmoi renders files from templates with per-machine data, supports secrets from password managers, and tracks drift. Symlinks share files verbatim, so any per-machine difference becomes a local edit to a tracked file.
Where do personal preferences go?
In untracked local files included by the templates, such as ~/.gitconfig.local and ~/.zshrc.local, or in each developer's fork of the team repository. The team source holds only conventions everyone shares.
Can chezmoi manage secrets?
It can read them from 1Password, Bitwarden, the OS keychain and others at apply time, so they appear in rendered files without ever being committed. For team repositories, keep secret use minimal and personal.
What happens if I edit a managed file directly?
chezmoi status shows it as modified, and chezmoi diff shows the difference. Move the change into the template (for team settings) or a local override (for personal ones), or it will be overwritten on the next apply.