- Go Template 41.8%
- Python 28.5%
- Shell 28.1%
- Lua 1.6%
| .chezmoidata | ||
| .chezmoiscripts | ||
| .forgejo/workflows | ||
| docs | ||
| dot_claude | ||
| dot_config | ||
| dot_local/bin | ||
| examples | ||
| Library/LaunchAgents | ||
| private_dot_gnupg | ||
| private_dot_ssh | ||
| test | ||
| tools/dotfiles-bootstrap | ||
| .chezmoi.toml.tmpl | ||
| .chezmoidata.yaml.tmpl | ||
| .chezmoiexternal.toml.tmpl | ||
| .chezmoiignore | ||
| .chezmoiremove | ||
| .gitattributes | ||
| .gitignore | ||
| .gitleaks.toml | ||
| .pre-commit-config.yaml | ||
| .yamllint.yaml | ||
| bootstrap.sh | ||
| CLAUDE.md | ||
| dot_gitconfig.tmpl | ||
| dot_gitignore_global | ||
| dot_npmrc.tmpl | ||
| dot_profile.tmpl | ||
| dot_zshenv.tmpl | ||
| dot_zshrc.tmpl | ||
| README.md | ||
| sync-staging.sh | ||
Shareable Dotfiles with Chezmoi
A user-friendly, shareable dotfiles repository that keeps secrets private while enabling easy adoption by multiple users.
Architecture: .local Override System
Shared Repository (Git-tracked)
├── Base dotfiles & templates
├── Generic SSH/GPG configs
├── Base packages (13 essentials)
├── Optional packages (108 packages in 7 categories)
└── Examples & documentation
↓
.local/ Directory (Gitignored, User-specific)
├── .chezmoi.toml.local # Your age keys, email
├── .chezmoidata.local/ # Package selection
│ └── packages.yaml
├── secrets/ # Encrypted SSH/GPG keys
│ ├── ssh/
│ ├── gnupg/
│ └── zsh/
├── private_dot_ssh/ # Your SSH key templates
└── private_dot_gnupg/ # Your GPG key templates
↓
Merged at Runtime by Chezmoi
Key Principle: Shared repo provides the framework, .local/ provides your personal secrets and customizations.
Personal Configuration via .local
What Goes in .local?
The .local directory (or submodule) contains ALL personal and machine-specific data:
Core Configuration
.chezmoi.toml.local- Your age keys, email, git config, personal URLs.chezmoidata.local/packages.yaml- Package selections for this machine
SSH Configuration
private_dot_ssh/config.local.tmpl- Your personal SSH hosts (NAS, servers, devices)private_dot_ssh/*.pub.tmpl- SSH public keys (contain your email)private_dot_ssh/private_authorized_keys.tmpl- Keys for remote accesssecrets/ssh/- Encrypted SSH private keys
Public repo keeps: GitHub config, global SSH defaults
GPG Configuration
private_dot_gnupg/publickey.*.asc- Your GPG public keysprivate_dot_gnupg/pubring.kbx- Public keyringprivate_dot_gnupg/private_trustdb.gpg- Trust databaseprivate_dot_gnupg/private_openpgp-revocs.d/- Revocation certificatesprivate_dot_gnupg/sshcontrol- SSH authentication configprivate_dot_gnupg/private_onlykey/- OnlyKey hardware security configsecrets/gnupg/- Encrypted GPG private keys
Public repo keeps: Generic gpg.conf, gpg-agent.conf.tmpl
Password Store
private_dot_password-store/- Entire password store (encrypted passwords)
Even though pass data is GPG-encrypted, directory names reveal service accounts, so it belongs in .local.
Work Configuration
dot_gitconfig-sbt- Work-specific git config (email, signing key)
Template Integration
The public repo includes your .local config via templates:
SSH Config (private_dot_ssh/config.tmpl):
{{- $localSSHConfig := joinPath .chezmoi.sourceDir ".local/private_dot_ssh/config.local.tmpl" -}}
{{- if stat $localSSHConfig -}}
{{ includeTemplate ".local/private_dot_ssh/config.local.tmpl" . -}}
{{- end }}
URLs and Domains (from .chezmoi.toml.local):
atuin_sync_url = "https://atuin.yourdomain.com"
claude_config_repo = "https://git.yourdomain.com/you/claude-config.git"
Setup Examples
Comprehensive guides are provided in the examples/ directory:
examples/ssh/- SSH config structure, public key managementexamples/gnupg/- GPG setup, OnlyKey integration, agent forwardingexamples/password-store/- Password store setup, organization, best practices
Security Benefits
This split architecture ensures:
✅ Public repo is shareable - No personal hostnames, IPs, emails, or network topology
✅ Private data stays private - All PII in .local submodule
✅ Easy adoption - Others can fork and customize without seeing your data
✅ No accidental leaks - Template system prevents mixing public/private
✅ Version control for secrets - .local can be a private git submodule
Quick Start
Option 1: Bootstrap Script (Recommended)
# Download and run bootstrap script (recommended)
bash <(curl -fsSL https://github.com/YOUR_USERNAME/dotfiles/raw/branch/master/bootstrap.sh)
# Or clone first, then run
git clone https://github.com/YOUR_USERNAME/dotfiles.git ~/dotfiles-temp
bash ~/dotfiles-temp/bootstrap.sh
This will:
- Install prerequisites (age, chezmoi, Homebrew)
- Generate your age keypair
- Initialize chezmoi
- Let you select package categories
- Apply dotfiles
Option 2: Manual Setup
If you prefer manual control:
# 1. Install chezmoi
sh -c "$(curl -fsLS get.chezmoi.io/lb)"
# 2. Initialize from repository (fork and use your own URL)
chezmoi init https://github.com/YOUR_USERNAME/dotfiles.git
# 3. Generate age encryption key
mkdir -p ~/.config/chezmoi
age-keygen > ~/.config/chezmoi/key.txt
chmod 600 ~/.config/chezmoi/key.txt
# 4. Configure .local settings
cd ~/.local/share/chezmoi
mkdir -p .local
cat > .local/.chezmoi.toml.local <<EOF
email = "your-email@example.com"
work = false
[age]
identity = "$HOME/.config/chezmoi/key.txt"
recipient = "age1..." # from age-keygen output above
EOF
# 5. Select package categories (see below)
mkdir -p .local/.chezmoidata.local
cat > .local/.chezmoidata.local/packages.yaml <<EOF
enabledCategories:
- 'development'
- 'cli-tools'
customPackages:
darwin:
brews: []
casks: []
debian: []
EOF
# 6. Apply dotfiles
chezmoi apply
Package Categories
Base Packages (Installed for Everyone)
13 essential packages:
- age, chezmoi, coreutils, git, git-delta
- gnupg, jq, neovim, pinentry, pinentry-mac, zsh
- iterm2 (cask), tailscale-app (cask)
Optional Categories (User Choice)
Enable categories in .local/.chezmoidata.local/packages.yaml:
1. development (24 packages)
Python, Node.js, Docker, development tools
- Brews: direnv, node, python@3.13, pipx, pre-commit, docker
- Casks: docker-desktop
- NPM: @anthropic-ai/claude-code
- UV: uv (Python package manager)
2. cli-tools (33 packages)
Modern CLI replacements and productivity tools
- Brews: bat, bottom, eza, fd, fzf, ripgrep, starship, tmux, zoxide, atuin, lazygit, tldr, dust, duf, procs, sd, tokei, hyperfine, xh, yq, jo, glow, fx
- Debian: bat, fd-find, fzf, ripgrep, tmux, zoxide, tldr
3. security (17 packages)
Password managers, VPN, security tools
- Brews: bitwarden-cli, pass, wireguard-go, wireguard-tools, age, gnupg, pinentry
- Casks: keepassxc, veracrypt, tor-browser
- UV: onlykey-agent
4. network (5 packages)
Network utilities and monitoring
- Brews: bandwhich, doggo, gping, mosh
- Debian: mosh
5. backup (4 packages)
Backup and archival tools
- Brews: restic, borgbackup
- UV: borgbackup, borgmatic
6. desktop-apps (24 packages)
GUI applications for macOS
- Casks: alfred, firefox, rectangle, signal, spotify, vlc, discord, slack, obsidian, visual-studio-code, 1password
7. work (5 packages)
Work-specific applications
- Casks: citrix-workspace, microsoft-teams, zoom, microsoft-office
8. data-science (2 packages)
Data processing tools
- Brews: csvkit
- UV: fowl
Selecting Packages
Edit .local/.chezmoidata.local/packages.yaml:
# Enable all categories
enabledCategories:
- 'development'
- 'cli-tools'
- 'security'
- 'network'
- 'backup'
- 'desktop-apps'
- 'work'
- 'data-science'
# Add your own packages
customPackages:
darwin:
brews:
- 'your-package'
casks:
- 'your-app'
debian:
- 'your-debian-package'
npm:
- 'your-npm-package'
uv:
- 'your-python-package'
Adding Your Secrets
SSH Keys
See examples/ssh/README.md for detailed instructions.
Quick version:
cd ~/.local/share/chezmoi
# 1. Get your age recipient
AGE_RECIPIENT=$(grep recipient .local/.chezmoi.toml.local | awk '{print $3}' | tr -d '"')
# 2. Encrypt your SSH key
mkdir -p .local/secrets/ssh
age -e -r "$AGE_RECIPIENT" \
-o .local/secrets/ssh/__ignore__encrypted_private_mykey.age \
< ~/.ssh/your_private_key
# 3. Create template
mkdir -p .local/private_dot_ssh
cat > .local/private_dot_ssh/private_mykey.tmpl <<'EOF'
{{- $localMachines := .localMachines | default list -}}
{{- if has .chezmoi.hostname $localMachines -}}
{{- if not .work -}}
{{- $localKeyPath := joinPath .chezmoi.sourceDir ".local/secrets/ssh/__ignore__encrypted_private_mykey.age" -}}
{{- if stat $localKeyPath -}}
{{- include $localKeyPath | decrypt -}}
{{- else -}}
{{- writeToStdout "WARNING: No mykey SSH key found\n" -}}
{{- end -}}
{{- end -}}
{{- end -}}
EOF
# 4. Apply
chezmoi apply ~/.ssh/mykey
GPG Keys
See examples/gnupg/README.md for detailed instructions.
Quick version:
# 1. Export your GPG key
gpg --export-secret-keys --armor KEYID > /tmp/gpg_key.asc
# 2. Encrypt it
AGE_RECIPIENT=$(grep recipient .local/.chezmoi.toml.local | awk '{print $3}' | tr -d '"')
mkdir -p .local/secrets/gnupg
age -e -r "$AGE_RECIPIENT" \
-o .local/secrets/gnupg/__ignore__encrypted_private_KEYID.age \
< /tmp/gpg_key.asc
shred -u /tmp/gpg_key.asc
# 3. Import on apply (see examples/gnupg/README.md for import script)
Shell Secrets (API Keys, Tokens)
Create .local/secrets/zsh/encrypted_secrets.zsh.age:
# Create plaintext secrets file
cat > /tmp/secrets.zsh <<'EOF'
export ANTHROPIC_API_KEY="sk-ant-..."
export GITHUB_TOKEN="ghp_..."
export OPENAI_API_KEY="sk-..."
EOF
# Encrypt it
AGE_RECIPIENT=$(grep recipient .local/.chezmoi.toml.local | awk '{print $3}' | tr -d '"')
mkdir -p .local/secrets/zsh
age -e -r "$AGE_RECIPIENT" \
-o .local/secrets/zsh/encrypted_secrets.zsh.age \
< /tmp/secrets.zsh
shred -u /tmp/secrets.zsh
# It will be automatically sourced by ~/.config/zsh/secrets.zsh
Storing and Syncing .local
The .local directory contains your personal configuration and secrets. You have several options for storing and syncing it:
Option 1: Local Only (Default)
.local stays only on your machine (gitignored). Simple and secure, but no backup or sync.
Option 2: Private Git Submodule (Recommended!)
Turn .local into a private git repository for version control, backup, and multi-machine sync.
Quick setup:
# 1. Create a PRIVATE repo on your git server (e.g., dotfiles-local)
# 2. Run the helper script
cd ~/.local/share/chezmoi
bash .scripts/setup-local-submodule.sh
Benefits:
- ✅ Version controlled
- ✅ Backed up to private git server
- ✅ Sync across machines
- ✅ Use branches for different machine configs
Cloning on new machines:
chezmoi init --recurse-submodules https://github.com/YOUR_USERNAME/dotfiles.git
Option 3: Cloud Storage
Symlink .local to Dropbox/iCloud for automatic sync (no version control).
Option 4: Encrypted Backup
Use restic, borg, or tarsnap for encrypted backups.
Full documentation: See .local-storage.md for detailed comparison and setup instructions.
Claude Config Sync
If you maintain a separate repository for Claude Code configuration (rules, skills, etc.), this setup can automatically sync it to ~/.claude.
Setup
Add the repository URL to .local/.chezmoi.toml.local:
claude_config_repo = "https://git.yourdomain.com/you/claude-config.git"
The sync script (.chezmoiscripts/run_onchange_after_sync-claude-config.sh.tmpl) will:
- Clone your claude-config repository
- Sync to
~/.claude(excluding runtime files) - Auto-detect upstream changes using
git ls-remote - Notify you about deleted files without automatically removing them
Deletion Notification
When files are removed from the upstream claude-config repository, the sync script will notify you but will NOT automatically delete them locally.
If you see deletion notifications:
- Review the list displayed during
chezmoi apply - Check
~/.claude/.sync-deletions.txtfor details - Manually delete if appropriate:
cd ~/.claude && rm -rf <path> - Delete the notification file to acknowledge:
rm ~/.claude/.sync-deletions.txt
Note: Runtime files (history.jsonl, debug/, plans/, projects/, etc.) are never synced or reported as deletions.
What Gets Synced
Synced:
- Claude Code rules (
rules/) - Skills and configurations
- MCP server configs
- CLAUDE.md navigation files
- settings.json (from
.local/dot_claude/settings.jsonif it exists)
Excluded (never synced):
.git/,docs/,upstream/- Runtime files:
history.jsonl,stats-cache.json,debug/,file-history/,shell-snapshots/ - User data:
plans/,projects/,todos/,session-env/ - Metadata:
README.md,LICENSE,.gitignore
Customization
Machine-Specific Settings
Edit .local/.chezmoi.toml.local:
email = "your-email@example.com"
work = false # Set to true on work machines
[age]
identity = "/Users/you/.config/chezmoi/key.txt"
recipient = "age1..."
# Optional: Override localMachines list
# localMachines = ["my-laptop", "my-desktop"]
Conditional Deployment
Templates support conditionals based on:
{{ .chezmoi.hostname }}- Machine hostname{{ .work }}- Work machine flag{{ .localMachines }}- List of local machines (vs forwarded){{ .osid }}- OS identifier (darwin, debian, etc.)
Example: Deploy key only on specific machine:
{{- if eq .chezmoi.hostname "my-laptop" -}}
{{- $localKeyPath := joinPath .chezmoi.sourceDir ".local/secrets/ssh/__ignore__encrypted_private_laptop_key.age" -}}
{{- if stat $localKeyPath -}}
{{- include $localKeyPath | decrypt -}}
{{- end -}}
{{- end -}}
Repository Structure
├── .chezmoi.toml.tmpl # Main config (loads .local overrides)
├── .chezmoiignore # Selective ignore (.local secrets)
├── .chezmoidata/
│ ├── packages.yaml # Base packages (13 essentials)
│ └── packages.optional.yaml # Optional categories (108 packages)
├── .chezmoiscripts/
│ └── run_onchange_install-packages.sh.tmpl
├── bootstrap.sh # One-command setup
├── dot_config/ # Generic configs
│ ├── git/
│ ├── nvim/
│ ├── tmux/
│ └── zsh/
├── private_dot_ssh/
│ ├── config.tmpl # Generic SSH config
│ ├── authorized_keys.tmpl
│ └── *.pub.tmpl # Public keys
├── private_dot_gnupg/
│ ├── gpg-agent.conf.tmpl # Generic GPG config
│ └── gpg.conf.tmpl
├── examples/
│ ├── ssh/README.md # How to add SSH keys
│ ├── ssh/private_example.tmpl
│ └── gnupg/README.md # How to add GPG keys
└── .local/ (gitignored)
├── .chezmoi.toml.local # Your age keys, email
├── .chezmoidata.local/
│ └── packages.yaml # Your package selection
├── secrets/
│ ├── ssh/ # Encrypted SSH keys
│ ├── gnupg/ # Encrypted GPG keys
│ └── zsh/ # Encrypted shell secrets
├── private_dot_ssh/ # Your SSH key templates
└── private_dot_gnupg/ # Your GPG key templates
Security
Encryption
- All secrets encrypted with age (modern, secure)
- Each user generates their own keypair
- Private key stored at
~/.config/chezmoi/key.txt(600 permissions) - BACKUP YOUR AGE KEY - if you lose it, your secrets are unrecoverable
What's Safe to Commit
✅ Git-tracked (safe to share):
- All templates and configs
- Generic SSH/GPG configurations
- Package definitions
- Public keys
- Documentation
❌ Gitignored (private):
.local/directory (entire directory)- Age private keys
- Encrypted secrets (in .local/secrets/)
- User-specific templates (in .local/private_dot_*/)
Best Practices
- Backup your age key to a password manager (1Password, Bitwarden)
- Use different age keys on different machines if needed (update
.local/.chezmoi.toml.local) - Never commit plaintext secrets - always encrypt with age first
- Verify
.local/is gitignored before committing - Use
__ignore__prefix for encrypted files to ensure they're never applied directly
Philosophy
Why .local Override System?
Single source of truth: Everyone uses the same repo, customizations are layered on top
No duplication: Base configs, scripts, and templates maintained in one place
Privacy by design: Secrets never enter git, gitignore prevents accidents
User-friendly: One command to bootstrap, clear examples for customization
Flexible: Choose which packages to install, which secrets to deploy
Why Package Categories?
Not everyone needs 100+ packages. Categories let users pick what they need:
- Minimal laptop: base only (13 packages)
- Developer workstation: base + development + cli-tools (70 packages)
- Full setup: all categories (121 packages)
Why age encryption?
- Modern: Designed for file encryption (vs gpg's email focus)
- Simple: One command to encrypt, one to decrypt
- Secure: Strong defaults, hard to misuse
- Fast: Efficient for many files
Troubleshooting
"No age encryption configuration found"
Cause: Missing .local/.chezmoi.toml.local
Fix:
cd ~/.local/share/chezmoi
mkdir -p .local
age-keygen > ~/.config/chezmoi/key.txt
# Copy the public key (age1...) from output
cat > .local/.chezmoi.toml.local <<EOF
email = "your-email@example.com"
work = false
[age]
identity = "$HOME/.config/chezmoi/key.txt"
recipient = "age1YOUR_PUBLIC_KEY"
EOF
"WARNING: No [keyname] SSH key found"
Cause: Template exists but encrypted key file doesn't
Fix: Encrypt your SSH key and place it in .local/secrets/ssh/
age -e -r "YOUR_AGE_RECIPIENT" \
-o .local/secrets/ssh/__ignore__encrypted_private_keyname.age \
< ~/.ssh/your_key
"chezmoi apply" shows unexpected changes
Cause: Package categories changed or templates updated
Fix: Review with chezmoi diff before applying
chezmoi diff
Homebrew package installation fails
Cause: Package name changed or no longer available
Fix: Update .local/.chezmoidata.local/packages.yaml to remove or replace the package
Contributing
Have improvements? Found a bug? Want to add examples?
- Fork the repository
- Create a feature branch
- Make your changes
- Test on a fresh machine
- Submit a pull request
License
MIT License - feel free to use and modify
Credits
Built with chezmoi by Tom Payne