Personal macOS development environment, managed declaratively with chezmoi (dotfiles), mise (CLI tools), and Homebrew (GUI apps). One command on a bare machine → a fully configured setup. Secrets never touch the repo.
git clone https://github.com/sanchpet/dotfiles ~/dotfiles && ~/dotfiles/bootstrap.shbootstrap.sh is idempotent and runs, in order (mise-first):
- mise — install the base tool manager (
curl https://mise.run), thenchezmoivia mise - chezmoi source — point chezmoi at this clone as its source (
chezmoi init --source), so edits apply with no commit/push/re-clone round-trip and no duplicate clone in~/.local/share/chezmoi. Also generate a per-machineed25519SSH key if missing (no passphrase; disk is encrypted) — it must exist before step 7 so the rendered git config turns commit signing on - apply mise config — lay down
~/.config/mise/config.tomlbefore installing tools (breaks the chicken-and-egg: the mise config is itself a managed dotfile) - mise install — install CLI tools from the config (bitwarden-cli, uv, …)
- Bitwarden unlock — only if the source contains
*.tmplsecrets (interactive) - Oh My Zsh — install the zsh framework (without touching
.zshrcor changing the shell) - chezmoi apply — render and place all dotfiles
- GitHub SSH keys — register this machine's key (generated in step 2) on GitHub as both an authentication key (push) and a signing key (Verified badge), then switch the dotfiles clone's origin from HTTPS to SSH so it's push-ready. Interactive on a TTY: runs
gh auth loginif unauthenticated and refreshes the token scope when needed. Idempotent; on CI / headless it prints the key and skips - brew bundle — GUI casks (Homebrew is installed lazily, only if the Brewfile needs it)
| Tool | Purpose | Link |
|---|---|---|
| mise | Polyglot tool & runtime manager — single declarative source for CLI tooling | https://mise.jdx.dev · github |
| chezmoi | Dotfiles manager — templating, per-machine, secrets | https://www.chezmoi.io · github |
| Oh My Zsh | Zsh configuration framework | https://ohmyz.sh · github |
| Homebrew | macOS package manager — used only for GUI casks | https://brew.sh |
| Tool | Purpose | Link |
|---|---|---|
Bitwarden CLI (bw) |
Secret retrieval at chezmoi apply |
https://bitwarden.com/help/cli/ · github |
| uv | Fast Python package & project manager — also backs mise's pipx: tools (settings.pipx.uvx) |
docs · github |
Yandex Cloud CLI (yc) |
Manage Yandex Cloud resources (IAM, compute, k8s, …) | docs |
Claude Code (claude) |
Anthropic agentic CLI — self-update off (DISABLE_AUTOUPDATER), update via mise up claude |
docs |
| claudeline | Real-time Claude Code statusline (quota / context / model) — wired via ~/.claude/settings.json statusLine |
github |
| aqua | Declarative CLI version manager — used to author/test aqua-registry packages | docs · github |
GitHub CLI (gh) |
GitHub from the terminal | docs |
GitLab CLI (glab) |
GitLab from the terminal | docs |
| kubectl | Kubernetes cluster CLI | docs |
| kubectx | Switch kubectl context / namespace | github |
| node | Node.js runtime | docs |
| Starship | Cross-shell prompt (zsh prompt; starship init in .zshrc) |
docs |
| zoxide | Frecency cd — replaces cd (--cmd cd); cdi = interactive |
github |
| fzf | Fuzzy finder (fzf --zsh in .zshrc) |
github |
ripgrep (rg) |
Fast recursive search | github |
| bat | cat with syntax highlighting & paging (aliased to cat) |
github |
| eza | Modern ls — git-aware, colors (aliased to ls/ll/la/tree) |
github |
| delta | Syntax-highlighting pager for git diffs (wired as git core.pager) |
github |
| dust | Intuitive du — disk-usage tree (aliased to du) |
github |
| duf | Better df — disk free, tabular (aliased to df) |
github |
dua (dua i) |
Interactive disk-usage explorer — find & delete big dirs | github |
| fd | Fast, user-friendly find |
github |
| hyperfine | Command-line benchmarking tool | github |
| opencode | Terminal-based AI coding agent | docs · github |
| python | Python runtime | docs |
| helm | Kubernetes package manager | docs |
| terragrunt | Terraform/OpenTofu wrapper | docs |
| yq | YAML/JSON processor | github |
awscli (aws) |
AWS CLI | docs |
| go | Go toolchain | docs |
| terraform | Infrastructure as code | docs |
| vault | Secrets management CLI | docs |
flux2 (flux) |
GitOps continuous delivery for Kubernetes | docs |
| cfssl | Cloudflare PKI/TLS toolkit | github |
| typst | Markup-based typesetting (LaTeX alternative) | github |
ansible (ansible-core) |
IT automation engine — installed via uv (pipx: backend) |
docs |
| ansible-lint | Ansible playbook linter (via uv) | github |
| yamllint | YAML linter (via uv) | github |
| crane | Inspect/copy/manage remote container images & registries | github |
| regctl | Registry client — manifests, tags, copy/retag without a daemon | github |
| oras | OCI registry client for arbitrary artifacts (push/pull non-image content) | docs · github |
| Tool | Purpose | Link |
|---|---|---|
| pre-commit | Git pre-commit hook framework | https://pre-commit.com · github |
| shellcheck | Static analysis for shell scripts (via shellcheck-py) |
shellcheck · hook |
| pre-commit-hooks | Standard hygiene hooks (whitespace, EOF, YAML, …) | github |
| Tool | Purpose | Profile | Link |
|---|---|---|---|
| Visual Studio Code | Primary code editor (self-updating; adopted into brew) | all | docs |
| Freelens | Kubernetes IDE (open-source Lens fork) | all | github |
| cmux | Ghostty-based terminal with vertical tabs + notifications for AI coding agents | all | site |
| WakaTime | Menu-bar time tracker — whole-system activity beyond editor plugins | all | docs |
| Pearcleaner | App uninstaller + orphaned-file finder (open-source CleanMyMac alt) | all | github |
| Docker Desktop | Container engine + CLI + VM (launch once to start the daemon) | all | docs |
| Yandex Music | Desktop music player (self-updating cask) | all | site |
| Tunnelblick | Free/open-source OpenVPN GUI client — double-click .ovpn to import, connect from the menu bar |
all | site |
| .NET SDK | .NET toolchain | work only |
docs |
| Tool | Purpose | Profile | Link |
|---|---|---|---|
| sshpass | Non-interactive ssh password auth (used by ansible) — not in the mise registry | all | docs |
| libpq | PostgreSQL client (psql, pg_dump, …) without the server — mise's postgres builds the full server; keg-only, so .zshrc adds its bin to PATH |
all | docs |
| skopeo | Inspect/copy/sign OCI & container images without a daemon — not in the mise registry | all | docs |
The prompt is Starship (dot_config/starship.toml — the kubernetes, aws
and terraform modules are on, so the active cluster / profile / workspace is always visible). Oh
My Zsh loads plugins only (theme off — Starship draws the prompt). Built-in plugins ship with
Oh My Zsh; external ones are cloned into $ZSH_CUSTOM/plugins by bootstrap.sh.
| Plugin | Source | Purpose |
|---|---|---|
| git | built-in | Git aliases (gst, gco, gp, …) |
| kubectl | built-in | k* aliases + completion (kgp, kgaa, kdp, …) |
| helm | built-in | Helm completion |
| terraform | built-in | tf* aliases + completion + workspace |
| aws | built-in | asp/acp profile switch + completion |
| ansible | built-in | Ansible aliases + completion |
| gh | built-in | GitHub CLI completion |
| colored-man-pages | built-in | Colored man pages |
| extract | built-in | x <archive> — extract any archive |
| sudo | built-in | Double-Esc prepends sudo |
| copypath / copybuffer | built-in | Copy $PWD / the current command line to the clipboard |
| dirhistory | built-in | Alt+←/→ directory history, Alt+↑ parent dir |
| forgit | external | fzf-powered git (ga, glo, gd) |
| zsh-completions | external | Extra completion definitions |
| zsh-autosuggestions | external | Fish-style suggestions from history |
| zsh-you-should-use | external | Reminds you when a typed command already has an alias |
| zsh-syntax-highlighting | external | Command-line syntax highlighting |
| zsh-autocomplete | external | Live menu completion (loaded last so its keybindings win) |
Load order matters.
zsh-autocompleteowns the completion/history UI, so it loads last, and plugins that fight over the same keys —fzf-tab,zsh-history-substring-search— are deliberately not used. Beyond the plugins,dot_zshrc.tmpladds custom aliases (kg,kgy,kctx; modern-CLI swapscat→bat,ls→eza,du→dust,df→duf) and themiseg/misermhelpers (add / remove a global mise tool and re-import the config).brewdiffreports drift between installed Homebrew packages and the renderedBrewfile.tmpl(brew has nomiseg-style auto-sync — the manifest is a curated template, so new packages are ported in by hand).updatesreports available mise + Homebrew package updates (cached; the first interactive shell of the day refreshes it in the background and prints the summary — never blocks the prompt;updates -rrechecks now, upgrades stay manual viabrew upgrade/mise upgrade/mise self-update).tgaliasesterragrunt(the omzterraformplugin coverstf*, but terragrunt has no plugin); terragrunt ships no completion script, so its built-inCOMP_LINEcompletion is wired viabashcompinit+complete -Cand shared with thetgalias throughcompdef.
| Path | Role |
|---|---|
dot_* |
Dotfiles rendered into $HOME by chezmoi (e.g. dot_gitconfig → ~/.gitconfig) |
dot_config/mise/config.toml |
Global mise config → ~/.config/mise/config.toml (user CLI tools) |
private_dot_claude/private_settings.json |
~/.claude/settings.json (0600) — Claude Code config: theme + claudeline statusline. Secrets/permissions stay in settings.local.json (untracked) |
dot_config/starship.toml |
Starship prompt config → ~/.config/starship.toml (kubernetes/aws/terraform modules) |
dot_zshrc.tmpl |
~/.zshrc — Oh My Zsh (plugins only) + Starship prompt + zoxide + mise + aliases (kubectl, modern CLI); secrets pending |
dot_local/bin/ |
Executable scripts symlinked to ~/.local/bin/ by chezmoi |
dot_local/bin/executable_cleanup |
~/.local/bin/cleanup — disk-reclaim tool (reports by default; --apply deletes Tier 1 caches + orphan caches of removed tools, --deep adds Go modcache) |
dot_local/bin/executable_updates |
~/.local/bin/updates — reports available mise + Homebrew package updates |
dot_local/bin/add-podkop-subnet |
~/.local/bin/add-podkop-subnet — route a domain through Podkop (VLESS) on Cudy router, then podkop reload. Default: resolve domain → subnet → user_subnets (for FortiClient VPN, where FakeIP routing fails). --domain: add the name verbatim → user_domains (FakeIP), e.g. for a domain whose anycast IPs are partially blackholed on the RU path |
.chezmoi.toml.tmpl |
Generates per-machine chezmoi config at init (prompts profile); never deployed |
bootstrap.sh |
Bare-machine bootstrap (operational, not deployed) |
Brewfile.tmpl |
GUI casks for brew bundle, templated per profile (operational; rendered at bootstrap) |
mise.toml |
Repo-local dev tooling (pre-commit) |
.pre-commit-config.yaml |
Lint hooks (shellcheck + hygiene) |
.chezmoiignore |
Keeps operational files in the repo but out of $HOME |
- chezmoi over GNU Stow / bare-git. Needed templating (per-machine values), first-class
secret handling, and a source tree where dotfiles stay visible (
dot_prefix) instead of hidden. Stow only symlinks; bare-git has no templating or secrets. - mise-first for CLI tools. All CLI tooling is declared in mise (
config.toml), versioned and cross-machine. Homebrew is reserved for what mise can't provide — GUI casks, plus the rare CLI with heavy native deps or no upstream release (e.g.sshpass). This keeps the toolchain reproducible and the Brewfile minimal. - Bitwarden for secrets. Secrets are pulled from Bitwarden at
chezmoi applyvia{{ bitwarden ... }}templates — nothing secret (encrypted or otherwise) lives in this public repo. Trade-off: bootstrap needs an interactivebw unlockbefore applying secret-bearing files (vs.age/secrets.env, which keep apply offline but place material in/near the repo). - pre-commit + shellcheck. Every commit lints shell scripts and runs hygiene checks, so
bootstrap.shand friends stay correct. pre-commit itself is installed via mise (postinstallwires the git hooks automatically). - Bootstrap ordering. The mise config is itself a managed dotfile, so it is applied before
mise installto break the chicken-and-egg; Homebrew is installed lazily, only when GUI casks are present. - mise hooks enabled (
settings.experimental). Turned on globally so a project'smise.tomlcan self-activate its git hooks with[hooks] enter = "git config core.hooksPath .githooks", instead of a manualgit configon every clone/machine. Kept at the machine level (not duplicated per repo) so individual projects only declare the[hooks]they need. - Per-machine via
profile, not per-machine directories. One source tree; machine-specific variation is driven by a singleprofilevalue (work/personal), prompted once atchezmoi init(override in CI/headless withDOTFILES_PROFILE) and stored in the machine-local chezmoi config (never in this repo). Templates branch on it —Brewfile.tmplinstalls the .NET SDK only whenprofile == "work". Git identity, by contrast, is directory-based:dot_gitconfig.tmpldefaults to the personal identity (sanchpet) with SSH commit signing everywhere, and anincludeIf "gitdir:~/IWE/magnit/"pulls indot_config/git/work.incto switch to the work identity (signing off) for the corporate repos under~/IWE/magnit/. Defaulting to personal keeps the corporate email opt-in — it can only ever appear under~/IWE/magnit/— and signing is gated on the key existing so a machine without it still commits. This keeps a single declarative source of truth and avoids the duplication/drift of per-machine dirs.
chezmoi has two locations: the source (this repo, chezmoi source-path) and the live files
in $HOME. Always edit the source, then push it to live — never edit the live file directly.
| Scenario | Command |
|---|---|
Changed a dotfile (e.g. .zshrc) |
edit the source (dot_zshrc.tmpl), then chezmoi apply ~/.zshrc (alias cza) |
| Pull latest on another machine | chezmoi update (= git pull + apply) (alias czu) |
| Check source ↔ live drift | chezmoi diff (alias czd) |
A tool wrote to a non-templated target (e.g. mise use -g → ~/.config/mise/config.toml) |
re-import: chezmoi add <target> (see the miseg helper) |
Never run
chezmoi add ~/.zshrc. It is a template (dot_zshrc.tmpl) —addwould overwrite it with the rendered content and destroy the{{ ... }}directives (incl. future secrets). Templated files are source-edited only;chezmoi addis for non-templated targets.
Secrets are never committed. They are resolved at apply time from
Bitwarden via chezmoi templates. On a fresh machine, bootstrap.sh
prompts for bw unlock only when the source actually contains secret templates.
The zsh config (dot_zshrc.tmpl) is kept as a .tmpl so a {{ bitwarden ... }} secret line can
be added later without a rename — see Zsh shell for the plugin set and
prompt.
Pending: the
OBSIDIAN_API_KEYsecret reference (via Bitwarden) is not wired yet —.zshrcis kept as a.tmplso the{{ bitwarden ... }}line can be added without a rename.