Personal, reproducible shell environment managed with chezmoi.
This repository provides a portable developer workstation setup with shared shell improvements, automated CLI tooling installation, machine-specific configuration profiles, and support for machine-local configuration outside version control.
- Centralized ZSH configuration
- Cross-platform support (macOS, Linux, EC2, Steam Deck)
- Automated CLI installation
- Machine/profile separation
- Machine-local configuration via
local-envandlocal.zsh - Secret scanner pre-commit hook (
check-dotfiles) - SSH manager (
sshm) - Local shell manager (
localz) - Git identity managed per-machine via
local-env - bat and delta pre-configured
- tmux configured with persistent sessions (TPM, resurrect, continuum)
- Secret-safe — never commits credentials or private references
- Test suite for all custom tools
dotcmds— in-shell command reference for aliases, functions, and CLI tools
| Machine | Profile | Role |
|---|---|---|
| Personal Mac | personal |
mac |
| Work Mac | work |
work-mac |
| Raspberry Pi | personal |
homelab |
| Steam Deck | personal |
steamdeck |
| EC2 (Amazon Linux) | work |
ec2 |
.
├── .chezmoi.yaml.tmpl
├── .chezmoiscripts/
├── .chezmoiignore
├── AGENTS.md
├── CLAUDE.md
├── README.md
├── requirements-dev.txt
├── tests/ ← integration tests (repo structure, shell syntax)
├── tools/ ← custom CLI utilities
│ ├── check-dotfiles/
│ ├── local-env/
│ ├── localz/
│ └── sshm/
├── dot_zshrc.tmpl
└── private_dot_config/
├── bat/config
├── delta/config
├── starship.toml
├── tmux/tmux.conf
└── zsh/
├── common.zsh
├── personal.zsh
├── work.zsh
└── hosts/
├── mac.zsh
├── work-mac.zsh
├── homelab.zsh
├── steamdeck.zsh
└── ec2.zsh
Each tool under tools/ has its own bin/, README.md, CLAUDE.md, and AGENTS.md.
.zshrc
├── common.zsh
├── profile
│ ├── personal.zsh
│ └── work.zsh
└── host
├── mac.zsh
├── work-mac.zsh
├── homelab.zsh
├── steamdeck.zsh
└── ec2.zsh
Shared behavior belongs in common.zsh. Profile-specific in personal.zsh / work.zsh. Machine-specific in the corresponding host file.
Machine-local configuration that must not be synchronized is kept outside chezmoi.
| Tool | Description |
|---|---|
sshm |
Manage SSH hosts — add, list, copy keys, generate key pairs |
local-env |
Machine-local environment variables, outside chezmoi |
localz |
Manage ~/.config/zsh/local.zsh |
check-dotfiles |
Secret scanner, runs as pre-commit hook |
Installed to ~/.local/bin/ via run_onchange_install-tools.sh.tmpl. See each tool's README.md for usage.
- zsh-autosuggestions
- zsh-syntax-highlighting
- zsh-history-substring-search
- zoxide
- fzf
- ripgrep (
rgrepalias) - fd (
ffalias)
POSIX find and grep are not replaced — shell tools and SDKs may depend on their standard behavior.
- bat (
catalias) - eza (
ll,treealiases)
- git-delta (side-by-side diffs, navigation)
- tmux — configured via
private_dot_config/tmux/tmux.conf- Prefix:
Ctrl+a; mouse enabled; splits with|/-; pane navigation withhjkl tm [name]— attach to session or create it (default:main)tls/tks/td— list, kill, detach- TPM installed automatically via
run_once_05-install-tpm tmux-resurrect+tmux-continuum— sessions survive reboots and broken pipes
- Prefix:
The prompt uses Starship with icons that require a Nerd Font in your terminal.
FiraCode Nerd Font is installed automatically by chezmoi apply on both macOS and Linux. No manual step needed on managed machines.
On remote machines (EC2, homelab) the font must be installed on the client, not the server.
The iTerm2 Dynamic Profile (FiraCode Nerd Font + ligatures) is only deployed on macOS and only applies to iTerm2. Other terminals (Ghostty, Alacritty, WezTerm, Termius, etc.) must be configured manually — set the font to FiraCode Nerd Font Mono in their own settings. Ligature support also varies by terminal.
brew install chezmoi
chezmoi init <repository>
chezmoi applysh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply <repository>When prompted, enter the profile (personal or work) and role (mac, work-mac, homelab, steamdeck, or ec2).
If get.chezmoi.io returns a 503, install chezmoi directly from GitHub:
curl -fLo /tmp/chezmoi.tar.gz https://github.com/twpayne/chezmoi/releases/download/v2.72.0/chezmoi_2.72.0_linux_arm64.tar.gz \
&& sudo tar -xzf /tmp/chezmoi.tar.gz -C /usr/local/bin chezmoi \
&& chezmoi init https://github.com/dgaramos/dotfiles.git \
&& rm -f ~/.local/share/chezmoi/.chezmoi.toml.tmpl \
&& chezmoi applyWhen prompted, enter profile work and role ec2.
| Problem | Cause | Fix |
|---|---|---|
get.chezmoi.io returns 503 |
Installer service unavailable | Use the direct GitHub release above |
multiple config file templates |
Legacy .chezmoi.toml.tmpl in source |
rm ~/.local/share/chezmoi/.chezmoi.toml.tmpl then re-run |
multiple config files |
.chezmoi.toml and .chezmoi.yaml coexist |
rm ~/.config/chezmoi/chezmoi.toml then chezmoi apply |
chsh: command not found |
Amazon Linux omits chsh by default |
sudo dnf install -y util-linux-user |
zsh: command not found after apply |
chezmoi installed manually skips bootstrap | sudo dnf install -y zsh then chezmoi apply |
The custom tools in tools/ can be installed on any machine without chezmoi, straight from the latest GitHub Release:
# Install all tools
curl -fsSL https://github.com/dgaramos/dotfiles/releases/latest/download/install.sh | bash
# Install a single tool
curl -fsSL https://github.com/dgaramos/dotfiles/releases/latest/download/install.sh | bash -s -- sshmBinaries are written to ~/.local/bin. No sudo is used, no system directories are touched, and no shell files are modified. If ~/.local/bin is not on your PATH, the installer prints the line to add to your shell profile.
The tools are Python scripts, so Python 3 must be available on the target machine.
Scripts in .chezmoiscripts/ run automatically during chezmoi apply:
01-install-zsh → installs zsh + sets as default shell (Linux only)
02-install-homebrew → installs Homebrew (macOS only)
03-install-oh-my-zsh → installs oh-my-zsh
04-install-zsh-plugins → clones zsh plugins
05-install-tpm → clones TPM into ~/.tmux/plugins/tpm
06-install-fonts → installs FiraCode Nerd Font (macOS: brew cask; Linux: GitHub Releases)
07-configure-gh-auth → authenticates gh if available
07-configure-git → sets git identity, wires delta config (macOS)
install-cli-tools → installs CLI tools (re-runs when content changes)
install-git-hooks → installs pre-commit scanner hook
install-tools → installs custom tools from tools/ (re-runs on version bump)
All scripts are idempotent.
common.zsh
├── local-env → machine-local environment variables
└── local.zsh → other machine-local shell configuration
Use local-env for environment variables. Use ~/.config/zsh/local.zsh as a generic escape hatch for local shell behavior.
Neither file is managed by chezmoi or committed.
The following paths are runtime state and never managed by chezmoi:
~/.config/local-env/env → local-env variable values
~/.config/local-env/names → local-env variable names
~/.tmux/plugins/ → TPM and tmux plugins
The shell function dotcmds provides an in-shell reference for everything defined in these dotfiles.
dotcmds # browse all aliases, functions and app commands via fzf
dotcmds apps # show only app/tool commands (no aliases)
dotcmds tmux # filter by keyword
dotcmds chezmoi # filter chezmoi aliasesAliases and functions defined in common.zsh must have an inline comment with the format # category: description — this is what dotcmds parses and displays.
App and tool commands are documented in:
private_dot_config/zsh/cmds.txt
When adding a new CLI tool or custom tool to the dotfiles, add its commands to cmds.txt using the same format:
tool <args> # toolname: what it does
Releases are fully automated. Every push to main runs .github/workflows/release.yml.
testjob — Python 3.12,pytest tools/ tests/ -v --cov, with coverage and test results uploaded to Codecov. It gates everything below: if it fails, no release is published.releasejob — computes the next version, writes and tags it, collects the tool binaries, and publishes the GitHub Release.
The next version is the root version file bumped according to the Conventional Commit subjects since the last vX.Y.Z tag:
| Commit subject contains | Bump |
|---|---|
!: or BREAKING CHANGE |
major |
feat(...) / feat: |
minor |
| anything else | patch (default) |
The workflow writes the new value to version, commits it as github-actions[bot] with the message chore(release): bump version to vX.Y.Z [skip ci], then creates and pushes the vX.Y.Z tag. Never edit version by hand.
tools/.version is a different, unrelated file: it is the chezmoi trigger that makes run_onchange_install-tools re-run on all machines. Bump it by hand after changing a tool binary. It has nothing to do with the release version.
Release notes are generated automatically from the commit subjects in the range, grouped into: BREAKING CHANGES, Features, Bug Fixes, Documentation, Refactors, Tests, Chores, Other.
- BREAKING CHANGES comes first. Any commit whose subject contains
!:orBREAKING CHANGEgets its own entry at the top of the notes, in addition to its normal type section. The change that drives a major bump is the first thing in the release body. - Merge commits are excluded.
Merge pull request #N from ...describes nothing, so the generator reads the range withgit log --no-mergesand the individual commits are listed instead. - Every entry links to its commit. Each line ends with a short hash linking to the commit on GitHub.
A gate job decides whether the range is worth releasing. If every non-merge commit since the last tag is a docs, chore, ci, build or test commit, the release job is skipped entirely — no version bump, no tag, no release, and no re-upload of identical tool binaries. The gate prints an explicit reason to the workflow log and the run summary.
A breaking change always releases, even when its type is one of the above.
The test job is never gated: it runs on every push to main, whether or not a release follows.
Each release publishes: sshm, local-env, localz, check-dotfiles, and install.sh — the same install.sh used by the standalone installation commands above.
pip install pytest # or: brew install pytest
pytest tools/ tests/ -v164 tests covering all custom tools, repo structure consistency, and zsh file syntax.
Never commit tokens, passwords, credentials, private keys, or authentication material. Machine-local values stay outside chezmoi. The check-dotfiles pre-commit hook enforces this automatically.
Conventional Commits — type(scope): description.
Allowed types: feat, fix, docs, refactor, chore, test, build, ci.
Before committing:
git diff && git status && chezmoi diff
pytest tools/ tests/MIT