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 — point the
bwCLI at.bitwarden.server(self-hosted, blank = cloud), then login + unlock; interactive, skipped without a TTY (CI) - 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 |
| sweb | CLI for the SpaceWeb (sweb.ru) hosting API — my own tool (github backend) | 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 |
| kubectl-view-secret | kubectl view-secret <secret> [key] — decode a Secret in place, no base64 -d pipeline. Installed via the aqua backend, which renames the binary to kubectl-view_secret so kubectl picks it up as a plugin |
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 |
| 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 |
Antigravity CLI (agy) |
Google's Antigravity agentic CLI — the successor Google moved the Gemini CLI terminal experience to | docs |
| 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 |
teleport-community (tsh, tctl, teleport) |
Access plane for infrastructure — client, admin CLI, and the node agent this Mac runs (see Remote access) | 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 |
| mcp-tg | Telegram MCP server — lets an agent read chats over MTProto. Version-pinned, not latest: it holds a session that authorises the whole account. See Agent access. |
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 |
| Obsidian | Markdown knowledge base / vault editor (hypomnemata exocortex; self-updating cask) | all | site |
| 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 |
| OrbStack | Docker-compatible container & Linux VM runtime, replaces Docker Desktop (launch once to start the engine) | all | docs |
| Yandex Music | Desktop music player (self-updating cask) | all | site |
| .NET SDK | .NET toolchain | work only |
docs |
Installed via the mas CLI. A one-time App Store sign-in is the only step that can't live in code; the entries are skipped in CI (the runner isn't signed in).
| App | Purpose | Profile | Link |
|---|---|---|---|
| one sec | Delay distracting apps — digital-hygiene gate (a mindful pause before Telegram/feeds) | all | site |
| Bitwarden | Password manager | all | site |
| Focus To-Do | Pomodoro timer + time tracking — core to the self-development practice | all | site |
| WireGuard | WireGuard VPN client | all | site |
| v2RayTun | V2Ray / proxy client | all | site |
| Endel | Adaptive focus/sleep soundscapes | all | site |
| MKPlayer | Media player | all | — |
| 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 |
| eza | Modern ls — git-aware, colors (aliased to ls/ll/la/tree); eza ships no macOS binary upstream so mise can't provide it cleanly (asdf 404s, cargo needs Rust) — brew has a bottle |
all | github |
| mas | Mac App Store CLI — installs/declares the App Store apps above | all | github |
| ffmpeg | Video/audio transcoding — mise offers only conda: (a conda backend with its own cache) or an asdf plugin that builds from source; brew's bottle ships the encoders needed (svt-av1, libvpx, x264, opus) |
all | site |
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/miserm/miseuphelpers (add / remove a global mise tool and re-import the config;miseupupgrades with a fresh version list — clears mise's cached release list first so a just-published release is picked up).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.
Two MCP servers give Claude Code a browser and a Telegram reader. Both hand an agent something with real reach, so what bounds that reach is written down here rather than left implicit.
Registered at user scope in the personal profile pointing at http://127.0.0.1:9222, i.e. it attaches to a browser that is already running rather than launching its own. cometdbg in dot_zshrc.tmpl starts that browser: Comet is Chromium, so it speaks the DevTools protocol unchanged (verified — it reports Chrome/150, protocol 1.3).
The registration is reproducible (run_onchange_after_register-chrome-devtools.sh), for the same reason mcp-tg's is: --scope user writes into whichever profile CLAUDE_CONFIG_DIR names, so a hand-typed registration silently belongs to one contour and is missing from the other. That is not hypothetical — this server sat in the default profile alone until a session under the personal one reported having no browser at all. The script pins the profile and is idempotent.
It runs a dedicated profile (~/.cache/comet-debug), not the everyday one. Whoever holds a CDP endpoint can read every open tab and its cookies, and act as you on any site you are signed into. The separate profile keeps that to one window. This is the whole reason cometdbg exists instead of a note saying "pass --remote-debugging-port".
MTProto with a user session, because a bot cannot read a conversation between two people. mcp-tg login takes the phone, code and 2FA on a TTY — the credentials never pass through an agent's transcript — and stores the session in the login keychain rather than a file on disk.
The registration itself is reproducible (run_onchange_after_register-mcp-tg.sh.tmpl): mise pins the binary, and that script registers the server in the personal profile, reading the API credentials from Vault (homelab/telegram/mcp-tg) at apply time so they stay out of git. It is idempotent, personal-profile-only, and skips with a message when Vault is off the mesh — a bootstrap must not fail on reachability. The account login stays manual and interactive by design: that is the one step that should never be automated.
All 78 tools are enabled, deliberately. The server has no read-only mode, and an earlier permissions.deny listing every write tool was removed at the owner's decision: the agent is a working instrument on a machine the owner controls, and a tool that cannot act is worth less than the risk it avoids here.
What that means concretely, so it is never a surprise: an agent can send, edit and delete messages as the account holder, forward, react, join and leave chats, block users, and change the profile. Messages it sends are indistinguishable from the owner's to whoever receives them — this is the only capability that reaches other people, and the one worth thinking about before granting a session to any agent.
Two properties of the mechanism, unchanged by the above:
- The session authorises the entire account. It is a bearer credential that has already passed 2FA. Keychain storage protects it at rest; nothing protects it from a process that can ask the keychain.
- No Telegram MCP server can be scoped to a single conversation. Access is per-account, never per-chat. Upstream tracks per-chat allowlists as an open request.
To reinstate a restriction later, permissions.deny in ~/.claude/settings.json takes tool names as mcp__mcp-tg__<tool>. The current surface:
gh api repos/lexfrei/mcp-tg/contents/docs/tools.md --jq .content | base64 -dThis Mac is a Teleport SSH node: the agent dials out to the cluster proxy on :443 and holds a reverse tunnel, so there is no inbound port, no port forwarding on the router, and no dependence on the network it sits behind — home, office or a cafe are the same to it.
Two properties are worth stating because they are choices, not accidents:
- It runs as a login agent, not a system daemon. A non-root Teleport node can only serve sessions as the user it runs as, so the blast radius is that one account even if cluster RBAC were wrong. The cost is that the machine is reachable only while that user is logged in: after a cold boot FileVault holds the disk and nothing starts. No remote-access scheme fixes that — plan around it rather than expect it to be solved.
- Only a personal machine becomes a node.
.chezmoiignorewithholds the config and the agent on any other profile: reaching a node and being one are different things, and a corporate machine should only ever do the former.
Joining is a one-time out-of-band step, like every other bootstrap credential — the token never lives in this repo:
tctl tokens add --type=node --ttl=15m --format=text > ~/.config/teleport/join-token
chezmoi apply ~/.config/teleport/teleport.yaml # then the agent picks it upThe node writes its own certificates into ~/.local/share/teleport on first start and never reads the token again. Diagnostics: ~/Library/Logs/teleport-node.log.
Reaching the desktop from another Mac — home-desktop forwards the port and opens Apple's own Screen Sharing client (Mac-to-Mac negotiates a far better path than a generic VNC viewer):
home-desktop # tunnel + viewer; closing the shell closes both
tsh ssh sanchpet@macbook-air # terminal onlyScreen Sharing itself is a macOS service, enabled once per machine outside chezmoi (it needs root). The reliable path is System Settings → General → Sharing → Screen Sharing; the launchctl equivalent (enable then kickstart -k system/com.apple.screensharing) is fussy about ordering and silently unhelpful when the service is still disabled. Sleep is separate and matters as much:
sudo pmset -c sleep 0 # a sleeping laptop answers nothingThe tunnel is the access path, not a shield. macOS binds Screen Sharing on 0.0.0.0:5900, so the port answers on every network the machine joins — authenticated, but answering. Reaching it through Teleport is what gives the audited, certificate-gated path; if the machine sits on networks you do not trust, turn on the application firewall (currently off on this Mac) or narrow the allowed users in the same Sharing pane.
Note that a closed lid still sleeps an Apple Silicon laptop without an external display, so a machine meant to be reachable stays open.
| 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) |
.chezmoitemplates/claude-settings.json |
Single source for Claude Code's settings.json (model, theme, claudeline statusline, screencapture sandbox exclusion), included by every account profile below |
private_dot_claude/private_settings.json.tmpl |
~/.claude/settings.json (0600) — default profile. Secrets/permissions stay in settings.local.json (untracked) |
private_dot_claude-personal/, private_dot_claude-work/ |
~/.claude-personal, ~/.claude-work (0700) — separate accounts selected by CLAUDE_CONFIG_DIR (claude-personal / claude-work functions in .zshrc). Same settings as the default profile; CLAUDE.md is a symlink to the canonical ~/.claude/CLAUDE.md |
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/executable_statusline |
~/.local/bin/statusline — Claude Code statusline: prefixes a marker for the active account profile (🏢 work, 🏠 personal, read from CLAUDE_CONFIG_DIR), then execs claudeline with every segment intact |
dot_local/bin/executable_git-agent-sign.tmpl |
~/.local/bin/git-agent-sign — signing shim: forces this machine's vault agent socket, then execs ssh-keygen, so non-login shells sign too |
private_dot_ssh/private_config.tmpl |
~/.ssh/config (0600) — IdentityAgent pointed at the machine's vault agent, plus OrbStack's include |
dot_local/bin/executable_login-agents |
~/.local/bin/login-agents — bootout/bootstrap cycle for the login agents below; run by the run_onchange hook and by bootstrap step 10 |
Library/LaunchAgents/*.plist |
~/Library/LaunchAgents/ — launchd agents started at login, one file per app (dev.sanchpet.orbstack starts the OrbStack engine so the Docker socket is up without opening the app). Add an app = add a plist |
run_onchange_after_login-agents.sh.tmpl |
Reloads the login agents on chezmoi apply whenever a plist changes (keyed on their hashes) |
run_onchange_after_sudo-touch-id.sh |
Installs /etc/pam.d/sudo_local (Touch ID for sudo) + /etc/sudoers.d/timestamp (no credential cache). Idempotent, macOS-only, skips rather than prompts without a terminal |
dot_config/teleport/teleport.yaml.tmpl |
~/.config/teleport/teleport.yaml — SSH node config: reverse tunnel to the personal cluster's proxy, SSH service only. Personal profile only (.chezmoiignore) |
Library/LaunchAgents/dev.sanchpet.teleport-node.plist |
Runs the node as a login agent, so it serves sessions only as the logged-in user. Personal profile only |
dot_local/bin/executable_home-desktop |
~/.local/bin/home-desktop — forwards a local port to a node's Screen Sharing over Teleport and opens the viewer; the shell it drops you in is the tunnel's lifetime |
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 |
dot_local/bin/executable_age-archive |
~/.local/bin/age-archive — encrypt a directory to your age key with a verify gate (-s DIR [-o FILE] [-d DEST]… [--rclone REMOTE]… [-R age1…]…), or restore one (--restore ARCHIVE TARGET). The secret key comes from --identity-cmd (default $AGE_IDENTITY_CMD, e.g. bw get item <item>) or stdin; the self-recipient is derived from it, so no age1… on the CLI. Plaintext and the secret key never hit disk; distribution is gated behind a passing round-trip decrypt; pass -R recipients to widen access (e.g. add a YubiKey key). --help for the full interface |
.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 + Mac App Store apps 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 |
- Touch ID for
sudo, never aNOPASSWDrule. An agent working in this shell cannot type a password — its commands run without a controlling terminal. The tempting fix, aNOPASSWDline insudoers.d, hands those rights not to one agent but to every process running as this user (a packagepostinstall, any script that gets executed), and no honestly narrow allowlist exists:launchctlas root is a loaded arbitrary daemon, i.e. full root anyway.auth sufficient pam_tid.soin/etc/pam.d/sudo_localinverts that — the module short-circuits before the password prompt, so the missing terminal never matters, and the prompt is a system dialog raised insidesudoitself. Verified: asudoissued from a non-TTY agent process does raise it, because the process inherits the GUI session's bootstrap namespace. Paired withDefaults timestamp_timeout=0(/etc/sudoers.d/timestamp) it means every single root action costs one live fingerprint — an agent can ask for root and never hold it. Consequences worth remembering: insidetmuxthis needspam_reattach; over a remote session (Teleport, SSH) it cannot work at all, since there is no finger at that end — remotesudofalls back to a password, which a non-TTY caller cannot supply. Both files live outside$HOMEand need root, so chezmoi cannot own them as targets;run_onchange_after_sudo-touch-id.shinstalls them instead. It costs one password on a fresh machine and nothing ever after — it exits early when both files are already right, and refuses to prompt when there is no terminal, so a headless bootstrap prints what is left to do rather than hanging on a prompt nobody can answer. The sudoers drop-in is validated withvisudo -cbefore it is installed, because a malformed one locks the account out of root entirely. - 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. - Backend preference: aqua first, then github, then http, and a language manager last. mise's registry has acceptance tiers, and the ladder here mirrors them:
aqua(most features and security, and no plugin code runs at install), thengithub/gitlabfor what aqua lacks (attestation + SLSA verification), thenhttpfor vendor binaries with no Git host — pinned by version and sha256, because nothing else vouches for them — andpipx/npm/gem/cargoonly where the tool is native to that ecosystem. A bare tool name is left alone unless the registry's own first choice is wrong here; an explicit backend always carries a comment saying why.aquaalso wins overgithubwhen the aqua-registry entry does something the raw release does not:kubectl-view-secretships its binary dash-named, and only aqua'sfiles:rename tokubectl-view_secretmakes kubectl discover it as a plugin. - Three tools stay on
vfox, deliberately. mise no longer accepts vfox/asdf tools, since a plugin is arbitrary code run at install time.redis,1password-cliandteleport-communityare grandfathered because none of them can move, and the reasons are worth recording so nobody re-litigates them: Redis publishes no prebuilt binaries at all (the newest release carries a singleredis-full.tar.gzsource archive, older ones no assets), and aqua only places prebuilt artifacts — it has no build step, so this is a model mismatch rather than a missing package, and no aqua-registry PR can fix it. TheopCLI is closed-source with no public GitHub repository, so although an aqua package exists it is a baretype: httpagainst the AgileBits CDN with norepo_owner/repo_name; aqua resolves version lists only from GitHub releases or tags, solatestcannot work and the tool would have to be hand-pinned and hand-bumped. For a credential CLI, falling behind on updates is the worse trade.teleport-communitylooks like the easiest of the three to move — an aqua package exists, resolveslatest, and halves the install — but on macOS it ships only the client tools (tsh,tctl). The personal profile also runs theteleportnode daemon out of that same install dir (see Remote access), and the launch agent execs it by absolute path with an[ -x ] || exit 0guard — so dropping the server binary would not fail loudly, it would leave the node silently serving nothing. All three plugins come from mise's own orgs (mise-plugins,jdx), so they share a trust root with mise itself — which is what makes the exception tolerable. New vfox/asdf tools are still refused. - The
ubi:backend is banned (settings.disable_backends). mise deprecated it in favour ofgithub:, which resolves the same GitHub releases and additionally verifies artifact attestations and SLSA provenance; it disappears in mise 2027.1.0. Rather than let aubi:tool be added and quietly rot until that release, mise is told to refuse the backend outright. The mise registry itself no longer routes any tool through ubi, so the ban costs nothing — only a hand-writtenubi:line can hit it. Because a setting binds one machine at install time, CI carries the matching repo-level guard, which rejects both aubi:declaration and the removal of the ban itself. - 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 and kept out of this public repo:dot_gitconfig.tmpldefaults to the personal identity (sanchpet) with SSH commit signing everywhere. A machine that also does corporate work sets its work identity (work.name/work.email) and the dir its repos live under (work.gitdir) in the machine-local chezmoi data — never in this repo. When both are set, anincludeIf "gitdir:…"pulls indot_config/git/work.incto switch to that identity under the work dir; a machine with no corporate identity gets neither theincludeIfnorwork.inc. Settingwork.emailwhile leavingwork.gitdirblank is refused at render time — git reads an emptygitdir:pattern as matching every repository, which would make the corporate identity the global default. Corporate commits are signed by a key declared for work (work.signingKey, machine-local like the rest), trusted inallowed_signersunder the work email as its own principal; registering that key with the corporate host is what makes them verify there. Declare no work key and signing is switched off under the work dir rather than inheriting the personal key — a host that has never seen that key cannot verify it, so such a signature only discloses which personal key made the commit, and unlike an unsigned commit it does not look like one. Personal signing is separately gated on its key existing, so a machine without it still commits. This keeps a single declarative source of truth, keeps the employer identity out of the public repo, and avoids the duplication/drift of per-machine dirs. - SSH keys live in a vault agent — never on disk. Each machine keeps its own
auth@…andsigning@…keys in its password manager and serves them over that app's SSH agent, unlocked by Touch ID: personal machines use Bitwarden, work machines 1Password. The choice is one value in the machine-local chezmoi data (agent.kind), alongside that machine's signing public key (agent.signingKey), whichdot_gitconfig.tmplrenders as akey::literal foruser.signingKey. Onboarding a machine means creating the keys in the vault, not runningssh-keygen—bootstrap.shgenerates nothing and reads the public halves from the agent when registering them on GitHub.~/.ssh/configsetsIdentityAgentto the machine's socket for all hosts. Because that only coversssh, git additionally pointsgpg.ssh.programat a shim (~/.local/bin/git-agent-sign) forcing the same socket for signing, so non-login shells (Claude Code, background agents, scripts) sign too. Verification is declarative: every machine's signing public key is listed inallowed_signers, which is how commits made elsewhere verify locally. Corporate repos sign as well — only the identity differs there, not the key. Under the same flag, Teleport'stshis told not to load its short-lived cert into this sign-only agent (TELEPORT_USE_LOCAL_SSH_AGENT=false) — the add would fail and abort the login;tshkeeps its certs in~/.tshregardless. - Bitwarden server — self-hosted, per machine.
bootstrap.shpoints thebwCLI at.bitwarden.server(asked once atchezmoi init, overrideDOTFILES_BW_SERVER) before login, so a self-hosted Vaultwarden works out of the box; blank keeps thebitwarden.comdefault. The URL is live infra, so it is never defaulted in this public repo — it lives only in the machine-local chezmoi config. Login is TTY-gated: a non-TTY run (CI/headless) skips it instead of hanging.
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.
For interactive use, bwu (defined in .zshrc) logs in once per machine and unlocks per session, exporting BW_SESSION. ~/.local/bin/age-archive then reads its key via AGE_IDENTITY_CMD (a bw get item …), so the age secret is fetched from the vault at run time — never pasted, never on disk.
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.