Tool managers — who installs what¶
A map of every tool installed by this repo, organised by the manager that
owns it. Companion to upgrades.md — that one is the
upgrade-side reference; this one is install-side.
If you want to know:
| Question | Section |
|---|---|
| "Where does tool X come from?" | § Tool index (A–Z) |
| "What does manager Y install?" | § Per-manager catalog |
| "I'm adding a new tool — which manager should I use?" | § Decision tree |
| "Why is X managed by Z and not W?" | § Multi-mechanism tools + rationale |
| "How is X upgraded?" | upgrades.md |
Convention reminder: install-only by design (see
upgrades.md§ Why install and upgrade are split). Every install path here usesstate: present/creates:/[[ -d … ]]guards; nothing inchezmoi applysilently bumps a tool's version. The only thing that intentionally moves tools forward isscripts/upgrade_tools.sh(viajust upgrade-*).
TL;DR — manager landscape¶
| Manager | Owns | Where listed | Upgrade category |
|---|---|---|---|
Bootstrap (run_once_before_*.sh.tmpl) |
Homebrew itself, uv, mise, ansible-core, apt prereqs | run_once_before_00_bootstrap.sh.tmpl |
n/a (one-shot, idempotent) |
| Homebrew formulae | General macOS GUI helpers (mas) + role-driven formulae (in base, devtools, coding_agents, …) |
dot_config/homebrew/Brewfile.tmpl + scattered community.general.homebrew: tasks |
brew |
| Homebrew casks | macOS GUI apps, gated by installBrewApps, installAiDesktopApps, or installGamingApps |
Brewfile.darwin.tmpl |
brew (--cask --greedy) |
| Ansible roles | The majority — 26 roles spanning shells, devtools, agents, language toolchains, networking, security | dot_ansible/roles/*/tasks/main.yml |
indirect (each role calls one of: brew / apt / mise / uv / npm / cargo / go / gem / dotnet / curl-installer / github-release) |
| mise | Runtime versions: node, bun, rust, go, dotnet, ruby (oldEL: ruby only) |
dot_config/mise/config.toml.tmpl |
mise |
| uv | Python CLI tools (~13 entries in python_uv_tools + 1 in llm_tools + litellm) |
dot_ansible/roles/python_uv_tools/defaults/main.yml, dot_ansible/roles/llm_tools/defaults/main.yml, ad-hoc uv tool install in coding_agents (specify-cli) + security_tools (pre-commit) |
uv (uv tool upgrade --all) |
| npm (global) | JS CLIs (Pi, copilot-cli, codex, gemini-cli, openchamber, bitwarden, readability-cli, summarize on Linux) + tldr + tree-sitter-cli |
js_cli_tools/defaults/main.yml, scattered community.general.npm: + mise exec -- npm install -g |
npm (npm -g update); Pi's stable prefix is handled by agents |
| Mason (Neovim tools) | Editor-local language servers, linters, formatters; shared Prettier fallback | LazyVim defaults/extras + dot_config/nvim/lua/exact_plugins/mason.lua |
Manual :MasonUpdate, then :MasonInstall prettier; not upgrade-plugins |
| cargo | Rust crates: recon, pueue (Linux), tree-sitter-cli (fallback), alacritty (Linux build), modelsdev (Linux fallback) |
rust_cargo_tools/defaults/main.yml (currently []) + hard-coded in tasks |
cargo (cargo install-update -a) |
go (go install) |
Linux gopls; personal legacy source installs have their own owner |
go_tools/defaults/main.yml |
go; personal CLIs use personal |
| dotnet (global tools) | azure-cost-cli (binary azure-cost) |
dotnet_tools/defaults/main.yml |
dotnet |
| gem | try-cli (binary try), tmuxinator |
ruby_gem_tools/defaults/main.yml |
gem |
curl-installer (vendor install.sh) |
Self-managed coding agents + a handful of system tools: claude, opencode, omp, cursor-agent, agy, rtk, ollama, atuin, docker, zoxide, direnv, just, llmfit, starship | dot_ansible/roles/coding_agents/, llm_tools/, devtools/, starship/, atuin/, docker/, bootstrap |
agents (subset of these has a known self-update subcommand) |
| GitHub-release downloads | ~30 Linux user-level fallbacks for ripgrep/fd/jq/git-lfs/gh/glab/bat/eza/git-delta/diffnav/glow/gum/vhs/freeze/yazi/superfile/btop/zellij/sesh/worktrunk/workmux/tailspin/dasel/yq/jnv/doggo/gping/trippy/bandwhich/rustscan/cloudflared/ngrok/speedtest/gitleaks/lazygit/neovim + macOS fallbacks for specstory, lazygit, and neovim | Each role's _release_fallback block |
none (install-only except explicit minimum-version repairs — see Coverage gaps) |
| chezmoi externals | git checkouts on weekly refresh: oh-my-zsh + plugins, oh-my-bash, ble.sh, TPM, fzf (Linux), pi-agents, toolkami.rb | .chezmoiexternal.toml.tmpl |
externals (chezmoi apply --refresh-externals) |
| apt / yum | Distro packages (build deps, ffmpeg, audit, fontconfig, libnotify-bin, system git/zsh/bash, Steam launcher/runtime, …) | scattered ansible.builtin.apt: / ansible.builtin.yum: in roles |
none (relies on apt upgrade outside this repo) |
| flatpak | Discord (default channel on Linux) | gui_apps_linux/tasks/main.yml |
flatpak (flatpak update -y) |
| personal tools | Thirteen CLIs: macOS Homebrew / Linux verified release binaries | personal_tools/files/tools.json |
personal (just upgrade-personal) |
Install order¶
Five phases in chronological order. Anything earlier MUST be present before the next phase can run.
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase 0 — Bootstrap (run_once_before_*) │
│ curl-installer: Homebrew, uv, mise │
│ apt: build-essential, git, curl (Linux prereqs for Linuxbrew) │
│ apt: libffi-dev, libyaml-dev, python3-dev (Linux non-noRoot only) │
│ uv: ansible-core (pinned Python 3.13) │
│ ansible-galaxy: community.general collection │
│ mise: install runtimes from ~/.config/mise/config.toml │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase 1 — chezmoi apply (config file deploy) │
│ Deploys dot_* files; doesn't install tools directly │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase 2 — Ansible (run_onchange_after_20_ansible_roles) │
│ Runs all 26 roles; each dispatches to its underlying manager │
│ (brew / apt / mise / uv / npm / cargo / gem / dotnet / curl / gh-rel) │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase 3 — Brew bundle (run_onchange_after_30_brew_bundle) │
│ `brew bundle --no-upgrade` against ~/.config/homebrew/Brewfile* │
│ Mostly GUI casks (macOS); skipped unless Brew/AI/gaming apps enabled │
└─────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ Phase 4 — Per-application onchange scripts │
│ Completion regen (run_after_50_generate_completions) │
│ bat theme cache (run_onchange_after_25_bat_theme) │
│ ble.sh compile (run_onchange_after_26_install_blesh) │
│ Global skills restore (run_onchange_after_40_install_global_skills) │
└─────────────────────────────────────────────────────────────────────────┘
The bootstrap is single-source-of-truth for the foundational tools
that everything else depends on. If you find yourself wanting to add
mise / uv / Homebrew elsewhere, the answer is almost always "extend
the bootstrap, then assume it in the role". The Linux noRoot path
(profile=ubuntu_server or RHEL/CentOS without sudo) skips the apt
prereqs and Linuxbrew, falling back to per-tool curl-installers and
GitHub releases — coverage is intentionally narrower (no GUI, no
system-wide installs).
Per-manager catalog¶
Each subsection below covers one manager. Format:
- Owns — what tools end up under this manager's control
- How a tool gets there — the canonical "I want to add tool X via this manager" recipe
- Per-OS notes — branching when relevant
- Upgrade path — link to the matching category in
upgrades.md
The ansible roles section is split into thematic subsections because 26 roles in one table is unreadable.
1. Bootstrap scripts (pre-chezmoi)¶
Files: run_once_before_00_bootstrap.sh.tmpl,
run_once_before_02_fix_intel_homebrew.sh.tmpl,
run_once_before_50_opencode_migrate.sh.tmpl
Owns — the four foundational managers that everything else assumes:
| Tool | Mechanism | Per-OS |
|---|---|---|
| Homebrew | Homebrew/install curl script |
macOS always; Linux only when installBrewApps=true & non-noRoot & arch∈{amd64,arm64}. Mirrors via HOMEBREW_BREW_GIT_REMOTE + bottle/API domains (BFSU baseline; brew-mirror switches to USTC/Aliyun/TUNA) when useChineseMirror=true. Aliyun's brew.git is broken (hangs) and HOMEBREW_CORE_GIT_REMOTE stays unset (API mode) — see mirrors.md |
| uv | curl -LsSf https://astral.sh/uv/install.sh \| sh |
all OSes if missing |
| mise | curl https://mise.run \| sh, with direct mise.jdx.dev/mise-latest-linux-${arch}[-musl] fallback (musl forced when glibc < 2.28) |
all OSes; arch detection (x64/arm64/armv7) |
| ansible-core | uv tool install --force --python 3.13 ansible-core |
all OSes; force-refreshes if existing ansible runs on Python < 3.10 |
| ansible-galaxy: community.general | ansible-galaxy collection install --force-with-deps |
all OSes |
| mise runtimes | mise install --yes (consumes ~/.config/mise/config.toml) |
all OSes — see § mise |
Linux prereqs (apt) to make Linuxbrew + mise Ruby + ansible-cryptography
buildable: build-essential, procps, curl, file, git (always);
libffi-dev, libyaml-dev, python3-dev (non-noRoot only).
Intel-Mac-only side script: run_once_before_02_fix_intel_homebrew.sh.tmpl
runs softwareupdate -i for Xcode CLT, then brew cleanup +
brew link --overwrite git.
One-shot migrations: run_once_before_50_opencode_migrate.sh.tmpl
moves a legacy config file (~/.config/opencode/config.json →
opencode.json). Not an install.
Adding to bootstrap: only when the new tool is required before
ansible runs — e.g. ansible itself, mise (because ansible roles call
mise exec), uv (because some roles call uv tool install). Otherwise
prefer an ansible role.
2. Homebrew (formulae + casks)¶
Three Brewfile templates + scattered community.general.homebrew[_cask]
tasks in ansible roles. The split is macOS-centric: Linux Brewfile is
empty, Linuxbrew use happens role-by-role.
Linux glibc floor: bootstrap skips the Linuxbrew install entirely when glibc < 2.28 (RHEL 8 / Debian 10 floor — excludes CentOS/RHEL 7's 2.17, leaves Ubuntu 20.04's 2.31 alone). Below that, every bottle would be compiled from source and the installer usually aborts first anyway. Same threshold the mise installer uses to force its musl build.
Probe brew by output, never by exit status. Every "is brew here?" check
in this repo tests that brew --prefix prints a non-empty path:
A fake brew stub (#!/bin/sh + exit 0) — historically recommended in
the README to stop bootstrap installing Linuxbrew on CentOS 7 — satisfies
command -v brew / which brew and returns 0 for every subcommand while
printing nothing. That makes brew list --formula <x> succeed (misclassifying
install styles, §2.3), makes brew bundle "succeed" while installing
nothing, and kills community.general.homebrew on empty JSON with
Expecting value: line 1 column 1 (char 0). See
pitfalls/ansible-homebrew-expecting-value-line-1-column-1.md.
2.1 Brewfiles¶
| File | Owns |
|---|---|
Brewfile.tmpl (shared) |
General macOS GUI support under installBrewApps, plus the personal tap and gopls. Personal CLI formulas are ensured by personal_tools, outside Brewfile, so upgrade-brew cannot reinstall missing personal tools through brew bundle. |
Brewfile.darwin.tmpl |
All GUI casks — see table below |
Brewfile.linux.tmpl |
Empty (commented-out placeholders only) |
macOS casks (~25 active, gated by feature flags):
| Cask | Gating |
|---|---|
alacritty, ghostty, warp, cmux, cursor, visual-studio-code |
installBrewApps |
claude, chatgpt, opencode-desktop, antigravity |
installAiDesktopApps |
codeisland (tap wxtsky/tap) |
installAiDesktopApps |
ollama-app |
installAiDesktopApps + installLlmTools |
steam |
installGamingApps |
nikitabobko/tap/aerospace, alt-tab, raycast, maccy, scroll-reverser, the-unarchiver, applite |
installBrewApps |
discord |
installBrewApps |
arc |
installBrewApps |
obsidian, google-drive, grammarly-desktop, super-productivity |
installBrewApps |
spotify, anki |
installBrewApps |
tailscale-app |
installBrewApps |
tailscale (Linux) |
installTailscale |
dbeaver-community |
installBrewApps |
superset |
installBrewApps + arm64 |
Many more entries exist in commented-out form (minecraft, telegram, openvpn-connect, etc.) — uncomment to enable.
2.2 Role-driven brew (formulae + casks)¶
Most CLI formulae land via ansible roles, not the Brewfile. Examples:
base/tasks/main.yml:git,git-lfs,curl,wget,ripgrep,fd,jq,tree,justdevtools/tasks/main.yml: large set —bat,gh,glab,diffnav,git-delta,eza,tlrc,glow,gum,vhs,freeze,thefuck,zoxide,direnv,yazi,superfile,tmux,sesh,worktrunk,workmux,zellij,btop,htop,duckdb,rclone,coreutils,taplo,television,pandoc,tailspin,lnav,grc,dasel,yq,jnv,witr,figlet,toilet,lolcat,fastfetch,shellcheck,shfmt,bats-core,git-graph,mosh,doxx,libreofficecoding_agents/tasks/main.yml:claude-code(cask),codex(cask),gemini-cli(formula),rtk(formula),specstory,codexbar,td,sidecar(all with tap setup exceptcodexbar, whose cask is in homebrew-cask core)coding_agents/tasks/tasks.yml(imported bymain.yml):marcus/tap/tasks, installingtasks,tasks-tui, andtasks-apitogethernetworking_tools/:nmap,arp-scan,mtr,iperf3,doggo,httpie,gping,trippy,bandwhich,rustscan,speedtest(tapteamookla/speedtest),ngrok,cloudflaredmedia_tools/:ffmpeg,imagemagick,exiftool,vipsiac_tools/:azure-cli,hashicorp/tap/terraform,opentofullm_tools/:llmfit,models,ollamasecurity_tools/:gitleaksstarship/,atuin/,bash/(gawk),zsh/— each installs its primary tool via homebrew formula on macOS;neovim/uses Homebrew on Apple Silicon and the official release fallback when neededpueue(fromrust_cargo_tools/): macOS uses homebrew formula
Per-OS dispatch is the universal pattern in roles: macOS branch uses
community.general.homebrew / homebrew_cask; Linux branches use apt,
Linuxbrew (when present), or GitHub releases. See each role for the
explicit when: ansible_facts["os_family"] == "Darwin" guard.
Tap inventory (added by various roles): nikitabobko/tap,
wxtsky/tap (CodeIsland), hashicorp/tap (terraform),
teamookla/speedtest, specstoryai/tap, steipete/tap (CodexBar CLI formula — the
cask is in homebrew-cask core, no tap needed),
marcus/tap (td, sidecar, Tasks), dlvhdr/formulae, raine/workmux.
Homebrew 6+ refuses formulae/casks from untrusted third-party taps. Role-driven
taps must run a brew tap-info <tap> → brew trust <tap> check before installing
from that tap; Brewfile taps are handled by the brew-bundle run-script before it
calls brew bundle. See
pitfalls/homebrew-6-refuses-untrusted-tap-formula.md.
Adding via homebrew: prefer a brew formula/cask only when the tool is macOS-shipped and Linux equivalents go via apt or release tarball. Linux-first tools rarely belong in the Brewfile.
2.3 Homebrew prefix dispatch helper¶
scripts/upgrade_tools.sh::_uv_install_style (and mirrored helpers in
python_uv_tools/tasks/main.yml) inspect whether a binary lives under
brew --prefix to decide between brew upgrade <pkg> and the tool's
native self-update. This avoids the "I uv self update on a brew-uv
host and silently no-op" trap — see
pitfalls/uv-self-update-homebrew-noop.md.
3. Ansible roles¶
26 roles under dot_ansible/roles/. Triggered by
.chezmoiscripts/global/run_onchange_after_20_ansible_roles.sh.tmpl
(re-runs when any role's tasks/main.yml or defaults/main.yml changes
— SHA256-tracked).
Universal patterns:
state: present/creates:— install-only, no silent upgrades- Per-OS branching via
when: ansible_facts["os_family"] == "Darwin"(or["Debian", "RedHat"]) tags: [sudo]on anybecome: truetask (so noRoot mode skips them — seepitfalls/ansible-missing-sudo-tag.md)failed_when: false+register:for "is the tool installed?" probes- Linux user-level fallback to
~/.local/binwhen distro package is missing or too old (handles RHEL/CentOS + noRoot + Apple-Silicon-not-bottled scenarios)
Role feature flags (set in .chezmoi.toml.tmpl via prompts):
installBrewApps, installAiDesktopApps, installCodingAgents,
installLlmTools, installPythonUvTools, installJsCliTools,
installDotnetTools, installIacTools, installMediaTools,
installNetworkingTools, installTunnelTools, installBitwarden,
installGamingApps, installInputMethod, installAuditd. Roles gate themselves on the
matching flag; the bundle script scripts/init/dotfiles_init.py defines
profile presets (minimal, cloud-vm planned, default-developer, …).
3.1 Base / shared CLI¶
| Role | Owns | Per-OS mechanism |
|---|---|---|
atuin |
atuin shell history sync |
macOS: brew formula · Linux: curl https://setup.atuin.sh \| sh -s -- --non-interactive → ~/.atuin/bin/ |
homebrew |
(no installs — refresh + cleanup only, 24h-throttled) | n/a |
base |
git, git-lfs, curl, wget, ripgrep, fd, jq, tree, just, gcc/build-essential/make |
macOS: brew formulae · Debian: apt (fd-find + /usr/bin/fd symlink) · RedHat: yum · just always via curl https://just.systems/install.sh · Linux user-level GitHub-release fallback to ~/.local/bin for ripgrep/fd/jq/git-lfs |
nerdfonts |
font-hack-nerd-font |
macOS: brew cask · Linux: download Hack.zip from nerd-fonts/releases/latest → ~/.local/share/fonts + fc-cache -fv |
3.2 Shells (bash, zsh, starship)¶
| Role | Owns | Per-OS mechanism |
|---|---|---|
bash |
gawk (ble.sh dep, unconditional); homebrew bash 5.x + /etc/shells + chsh (macOS only when primary_shell="bash") |
macOS: brew · Debian: apt · RedHat: yum |
zsh |
system zsh package; source-build zsh 5.9 to ~/.local on RHEL/CentOS 7 if system zsh < 5.3; chsh when primary_shell="zsh" (default) |
macOS: brew · Debian: apt · RedHat: yum + ncurses-devel source build fallback. oh-my-zsh + plugins ship as externals (§12) |
starship |
starship prompt |
macOS: brew formula · Linux: curl https://starship.rs/install.sh \| sh -s -- --yes → sudo /usr/local/bin → user ~/.local/bin fallback |
The primaryShell chezmoi prompt gates the chsh call only — both
shells deploy their configs everywhere. See AGENTS.md → "primaryShell
choice gates chsh only" hard invariant.
3.3 DevTools (mega-role)¶
Single role, ~50 tools — the kitchen sink for terminal-life CLIs.
There's a backlog item (backlog/devtools-role-split.md)
considering whether to break this up.
macOS (homebrew, taps: dlvhdr/formulae for gh-dash,
raine/workmux): bat, bats-core, gh, glab, diffnav, git-delta,
git-graph, eza, tlrc, glow, gum, vhs, freeze, thefuck,
zoxide, direnv, yazi, superfile, tmux, sesh, worktrunk,
workmux, zellij, btop, htop, duckdb, rclone, coreutils,
taplo, television, pandoc, tailspin, lnav, grc, dasel,
yq, jnv, witr, figlet, toilet, lolcat, fastfetch,
shellcheck, shfmt, doxx.
Linux — per-tool dispatch. The pattern is apt/yum (system) → GitHub release tarball or installer (user):
| Tool | Linux mechanism (preferred → fallback) |
|---|---|
gh |
GitHub .deb → user tarball |
glab |
GitHub .deb → user tarball |
bat |
apt + /usr/bin/bat symlink → GitHub musl release |
bats |
apt → GitHub release install.sh |
zoxide |
curl install.sh |
eza |
gierens.de apt repo → user GitHub musl → brew aarch64 |
git-delta |
.deb (x86_64) → brew aarch64 → user GitHub musl |
diffnav |
user GitHub release |
gh-dash |
gh extension install dlvhdr/gh-dash |
gh-notify |
gh extension install meiji163/gh-notify |
gh-select |
gh extension install remcostoeten/gh-select |
git-graph |
user GitHub release (x86_64 only) |
tldr |
system npm or mise exec -- npm install -g tldr |
glow, gum, vhs, freeze, duckdb |
user GitHub release tarballs |
rclone |
official installer or GitHub release |
thefuck |
uv tool install thefuck (Python 3.11, distutils workaround) |
shellcheck, shfmt |
apt or release |
direnv |
apt or installer |
yazi, superfile, btop, zellij, sesh, worktrunk, workmux |
Linuxbrew or GitHub release |
tmux |
apt → nelsonenzo/tmux-appimage AppImage to ~/.local/share/tmux-appimage/ when system version < 3.3 (required by display-menu popup) — see AGENTS.md "Tmux popup menu ≥ 3.3 required" |
htop, pandoc |
apt |
taplo, television (tv), lnav, grc, tailspin (tspin), dasel, yq, jnv, witr |
mix of apt + release |
ccze |
apt only (Linux-only tool, no macOS equivalent) |
3.4 Coding agents¶
Role: dot_ansible/roles/coding_agents/tasks/main.yml. Gated on
installCodingAgents. The most heterogeneous role — every agent
ships via a different mechanism upstream, and the role mirrors that.
| Agent | macOS | Linux | Notes / source lines |
|---|---|---|---|
terminal-notifier / libnotify-bin |
brew formula / apt (Debian) | apt (Debian only) | notifier helpers, lines 9–29 |
| Claude Code | brew cask claude-code |
curl https://claude.ai/install.sh \| bash → ~/.claude/local/bin/claude |
lines 33–71; arch skip for non-amd64/arm64 |
| OpenCode | curl https://opencode.ai/install \| bash -s -- --no-modify-path → ~/.opencode/bin/opencode |
same | lines 73–107. Cross-platform unified install; --no-modify-path keeps installer from editing chezmoi-managed shell rc. See pitfalls/opencode-docker-opentui-glibc-loader-missing.md for the container-image variant |
Pi (pi) |
mise exec -- npm install -g --ignore-scripts --prefix ~/.local @earendil-works/pi-coding-agent |
same | Final role section. Requires Node ≥ 22.19; transactionally installs and verifies the stable canonical copy before removing exact deprecated/duplicate packages from another npm prefix |
Oh My Pi (omp) |
curl https://omp.sh/install \| sh -s -- --binary → ~/.local/bin/omp |
same | Final role section. Detects package copies read-only, then installs/verifies the binary before removing exact @oh-my-pi/pi-coding-agent npm/Bun globals; failed installs restore the old command, and unsupported hosts do not migrate |
| pia | chezmoi external → ~/.local/share/pi-agents/bin/pia |
same | installCodingAgents-gated CLI + combo checkout; 08_pi_agents.sh reasserts canonical Pi/OMP/pia precedence after mise/Bun, with runtime/session state outside the checkout |
Cursor CLI (cursor-agent) |
curl https://cursor.com/install \| bash |
same | lines 112–124; brew cask was abandoned due to code-signing |
| GitHub Copilot CLI | npm @githubnext/github-copilot-cli (global) |
mise exec -- npm install -g |
lines 134–168 |
| OpenAI Codex CLI | brew cask codex |
mise exec -- npm install -g @openai/codex |
lines 172–236 |
| Gemini CLI | brew formula gemini-cli → npm @google/gemini-cli fallback |
mise exec -- npm install -g |
lines 240–300 |
Antigravity CLI (agy + agyc symlink) |
curl https://antigravity.google/cli/install.sh patched mid-flight via sed → --skip-path --skip-aliases |
same | lines 317–355. Two gotchas: (a) name collision with the Antigravity IDE — collision-free agyc symlink keys the AICAP stack; (b) installer mutates shell profiles by default — patched away. Both documented in-source |
| RTK | brew formula rtk |
curl https://raw.githubusercontent.com/rtk-ai/rtk/refs/heads/master/install.sh \| sh → ~/.local/bin/rtk |
lines 359–413 |
| SpecStory | brew tap specstoryai/tap + formula → GitHub release zip fallback |
GitHub release tarball | lines 417–595 |
| CodexBar | brew cask codexbar — homebrew-cask core (no tap), universal binary, macOS 14+ |
glibc ≥ 2.38: Linuxbrew steipete/tap/codexbar → GitHub release; below that: static-musl GitHub release only |
lines 597–864. Guards on /Applications/CodexBar.app, not which codexbar; removes a conflicting CLI formula first (both link the same binary name); the prebuilt Linux CLI needs GLIBC_2.38, so old-glibc hosts skip Linuxbrew entirely. See codexbar.md |
| td | brew tap marcus/tap + formula |
Linuxbrew → GitHub release marcus/td |
lines 865–1078. Includes "is current td Sidecar-compatible?" version check; removes conflicting formula if not |
| sidecar | brew tap marcus/tap + formula |
Linuxbrew → GitHub release marcus/sidecar |
lines 1080–1256 |
Tasks (tasks / tasks-tui / tasks-api) |
marcus/tap/tasks formula |
Linuxbrew → checksum-verified amd64/arm64 GitHub release | tasks.yml, imported by main.yml; a complete suite is left installed, and fallback installs all three into ~/.local/bin. First-use XDG config: tasks.md |
| Specify CLI | uv tool install specify-cli --from git+https://github.com/github/spec-kit.git |
same | lines 1153–1184. Only role-installed tool currently using uv tool install directly (not via python_uv_tools defaults) |
| OpenChamber | npm @openchamber/web |
mise exec -- npm install -g |
lines 1188–1222 |
| claude-hud plugin | runs files/claude_hud_sync.py --only-if-missing |
same | lines 1239–1247; install-only, explicit just upgrade-plugins for refresh |
SSOT for the AICAP agent stack: dot_config/shell/04_ai_agents.sh
defines autodetect order + AICAP_*_MODEL defaults. Four Python
consumers regex-parse the same file (see AGENTS.md cross-file rule).
Adding a new agent here means: install in this role + add to the SSOT
+ four Python AGENT_CONFIG dicts.
Pi, OMP, and pia are deliberately not added to AICAP in this change: they are
harness-combo research/runtime surfaces, not new defaults for the generic
one-shot AI shell helpers.
3.5 Language toolchains (python_uv_tools, ruby_gem_tools, rust_cargo_tools, go_tools, dotnet_tools, js_cli_tools)¶
Wrappers around language-specific package managers. All consume a
defaults/main.yml list so the role's tasks/main.yml is a loop. See
§ uv, § npm,
§ cargo, § go,
§ dotnet, § gem for the tool lists.
| Role | Underlying manager | Tools (count) | Gate |
|---|---|---|---|
python_uv_tools |
uv tool install |
13 | installPythonUvTools |
llm_tools |
uv tool install + brew + cargo + curl-installer |
4 (litellm, llmfit, models, ollama) | installLlmTools |
js_cli_tools |
mise exec -- npm install -g / system npm |
1 (readability-cli) |
installJsCliTools |
summarize |
brew (macOS arm64) / mise exec -- npm install -g (macOS Intel/Linux) |
1 (summarize) |
installSummarize |
rust_cargo_tools |
cargo install (after mise install rust@stable) |
[] in defaults + 2 hardcoded (recon, pueue) |
(no gate; rust runtime always installed via mise) |
ruby_gem_tools |
gem install (after mise install ruby@3) |
2 (try-cli, tmuxinator) |
(no gate; gated by mise install ruby skip on noRoot Linux) |
dotnet_tools |
dotnet tool install --global (after mise install --yes dotnet@10) |
1 (azure-cost-cli) |
installDotnetTools |
Key invariant (with_executables_from: entries MUST also declare
extra_binaries:): see AGENTS.md "Install vs upgrade is split on
purpose" hard invariant for the audit one-liner and the pitfall.
3.6 Editors + plugins (neovim, lazyvim_deps, nerdfonts)¶
| Role | Owns | Per-OS mechanism |
|---|---|---|
neovim |
neovim ≥ 0.11.2 (healthy installs are left untouched) |
macOS: Apple Silicon brew formula (state: present) → checksum-verified official macOS release to ~/.local when still missing/old; Intel goes directly to the official release · Linux: apt → GitHub release tarball if too old (/opt/nvim + /usr/local/bin/nvim symlink) → user-level tarball to ~/.local; oldEL pulls from neovim/neovim-releases (glibc 2.17 rebuild repo). No snap (dropped 2026-06 — see linux-package-sources.md → snap in this repo). See pitfalls/centos7-neovim-apt-register-defined.md |
lazyvim_deps |
fzf, lazygit >= 0.64.0, tree-sitter, node |
macOS: brew for the four packages; stale brew LazyGit gets a targeted formula upgrade, then a checksum-verified official release fallback to ~/.local/bin if the active binary remains old. Also runs config-driven mise install --yes (the only thing that installs go/rust from dot_config/mise/config.toml.tmpl on Darwin) · Linux: mise (node@lts general; node@20 + rust@stable on armv7l); fzf built from chezmoi external clone; LazyGit brew detection → checksum-verified system release → user release fallback (its PPA was deprecated upstream in 2021 and is purged); tree-sitter-cli via mise exec -- npm install -g tree-sitter-cli (timeout 180) → cargo fallback with libclang-dev/clang-devel |
nerdfonts |
(see § 3.1 base/shared) | — |
Mason manages editor tools inside Neovim's data directory (~/.local/share/nvim/mason
by default). LazyVim merges ensure_installed from its defaults, language extras,
and dot_config/nvim/lua/exact_plugins/mason.lua, which adds Prettier. Missing tools
install when Mason loads in Neovim; existing versions are left alone. Conform
prefers project-local Prettier, then the Mason copy on Neovim's PATH. Project
dependencies should pin their own version for CI consistency.
3.7 Networking + IaC + LLM + media (networking_tools, iac_tools, llm_tools, media_tools)¶
| Role | Owns | Per-OS mechanism |
|---|---|---|
networking_tools |
nmap, arp-scan, mtr, iperf3, doggo, httpie, gping, trippy, bandwhich, rustscan, speedtest (Ookla) |
macOS: brew (tap teamookla/speedtest for speedtest) · Linux: apt + GitHub-release fallback (musl) to ~/.local/bin for tools without apt packages |
networking_tools (tunnel sub-set, gated installTunnelTools + tag tunnel_tools) |
ngrok, cloudflared |
macOS: brew cask ngrok + brew formula cloudflared · Debian: ngrok apt repo + cloudflared .deb · RedHat: cloudflared .rpm · noRoot: user-level tarball fallback |
iac_tools (gated installIacTools; per-tool toggles) |
azure-cli, terraform, opentofu |
macOS: brew (hashicorp/tap for terraform) · Linux: vendor apt repos (Microsoft / HashiCorp / Cloudsmith) → user-level zip/uv tool install azure-cli fallback |
llm_tools (gated installLlmTools) |
litellm[proxy] (via uv), llmfit, models, ollama |
mixed — brew formula on macOS / curl-installer on Linux. Ollama: brew formula always (CLI) + cask ollama-app from Brewfile when installBrewApps; Linux uses curl https://ollama.com/install.sh (sudo, skipped in noRoot) |
media_tools (gated installMediaTools) |
ffmpeg, imagemagick, exiftool, vips |
macOS: brew · Debian: apt (libimage-exiftool-perl, libvips-tools) |
3.8 System / GUI / security (docker, gui_apps_linux, auditd, security_tools, bitwarden, input_method, atuin, homebrew)¶
| Role | Owns | Per-OS mechanism |
|---|---|---|
docker |
OrbStack (macOS), Docker rootless (Linux) | macOS: brew cask orbstack (skips if /Applications/Docker.app exists) · Debian/Ubuntu: apt prereqs (uidmap, dbus-user-session, fuse-overlayfs, slirp4netns, iptables) → curl https://get.docker.com \| sh → docker-ce-rootless-extras → dockerd-rootless-setuptool.sh install user systemd unit |
gui_apps_linux (Linux Debian + ubuntu_desktop profile) |
Alacritty, libfuse2, AppImageLauncher, VSCode, Cursor, Google Chrome, Discord, Zen Browser, CopyQ, playerctl/wmctrl/xdotool, wl-clipboard/xclip/xsel | Alacritty: cargo build (apt deps cmake, pkg-config, font/X libs) · AppImageLauncher: PPA → GitHub .deb → Lite AppImage to ~/Applications/ · VSCode: Microsoft apt repo · Cursor: .deb from cursor.com/api/download · Google Chrome: .deb from dl.google.com (x86_64 only) · Discord: flatpak (Flathub user-scope, default) or .deb via discordChannel chooser · Zen Browser: AppImage to ~/Applications/zen.AppImage |
auditd (Linux, gated installAuditd) |
auditd + audispd-plugins (Debian) / audit (RedHat); rule files 00-baseline.rules, 05-privileged.rules, optional 10-execve.rules, 99-finalize.rules |
apt / yum |
security_tools |
pre-commit, gitleaks |
macOS: brew (gitleaks) + uv (pre-commit) · Linux: gitleaks from GitHub release (system → /usr/local/bin; user fallback → ~/.local/bin) + uv pre-commit. Go is no longer here — it moved to mise (go = "latest", gated installExtraRuntimes). |
bitwarden (gated installBitwarden) |
@bitwarden/cli + Bitwarden Desktop (when bitwarden_install_desktop=true) |
CLI: mise exec -- npm install -g @bitwarden/cli (preferred) / system npm fallback · Desktop: macOS brew cask · Linux: snap → .deb fallback |
input_method (gated installInputMethod) |
mcbopomofo, squirrel (macOS) · ibus-rime (Debian) |
macOS: brew cask · Linux: apt. Rime's *.custom.yaml is chezmoi-managed and shared with the Windows repo — see input_methods. |
atuin |
(see § 3.1 base/shared) | — |
homebrew |
(see § 2 Homebrew) | — |
4. mise (runtime versions)¶
Config: dot_config/mise/config.toml.tmpl
[settings]: ruby.compile = false; opted-in modern hosts also set
dotnet.isolated = true so mise exports the runtime root matching the SDK layout.
Modern hosts (default):
| Runtime | Version | Gated on |
|---|---|---|
node |
lts |
always |
bun |
1 |
installExtraRuntimes |
rust |
stable |
installExtraRuntimes |
go |
latest |
installExtraRuntimes |
dotnet |
10 |
installDotnetTools |
ruby |
3 |
installExtraRuntimes (+ not noRoot) |
Version-pinning rationale: node=lts / rust=stable / go=latest use
moving channels because each promises backward compatibility (Go's
go1compat makes latest as safe as
rust's stable). bun=1 and dotnet=10 pin the major instead —
neither guarantees no-break across majors (bun ships behavior changes on
minors; .NET majors bump TFMs / SDK behavior), and dotnet=10 is also the
selected LTS. Bump dotnet deliberately
when the next LTS (12) lands. ruby=3 tracks the 3.x line.
go replaces the old macOS-only brew install from the security_tools
role, so Linux hosts now get a managed Go too (previously a gap).
Ruby is skipped in noRoot mode (profile=ubuntu_server without
sudo) because it needs libffi-dev / libyaml-dev to build, which
require apt.
oldEL (CentOS/RHEL 7, glibc 2.17): only ruby = "3" — other
runtimes have glibc/libstdc++ requirements too new for EL7.
Upgrade: just upgrade-mise → mise self-update --yes + mise upgrade
(honours the version constraints in the config; lts/latest move
forward, pinned versions don't).
Adding a runtime: edit dot_config/mise/config.toml.tmpl directly.
Runtime-version gating across hosts is on the backlog.
Why mise (not nvm/rbenv/pyenv/asdf/n): unified, language-agnostic,
respects .tool-versions, doesn't fight shell init order. Python is
deliberately not mise-managed — that's uv's territory (faster + no
build-from-source for CPython, which the noRoot Linux path can't do).
The backlog tracks
finer-grained per-host gating.
5. uv (Python CLI tools)¶
Config: dot_ansible/roles/python_uv_tools/defaults/main.yml (+
llm_tools/defaults/main.yml for litellm). Role-level dispatch:
dot_ansible/roles/python_uv_tools/tasks/main.yml.
Inspects whether uv is below min_uv_version (0.8.5) and dispatches
the upgrade through the right channel — brew upgrade uv for Homebrew
installs, uv self update for the curl-installer. Mirror logic in
scripts/upgrade_tools.sh::cat_uv(). See
docs/this_repo/uv-bootstrap.md and
pitfalls/uv-self-update-homebrew-noop.md.
Two install-completeness guards repair an existing tool without turning the role into an
upgrade loop: extra_binaries checks entry-point shims in ~/.local/bin, while
required_distributions (paired with env_name) checks packages inside the uv tool venv.
A missing declaration triggers one uv tool install --force; a complete environment remains
unchanged. yt-dlp[default] uses the latter to ensure its matched yt-dlp-ejs solver is
present on hosts that previously installed bare yt-dlp.
Tool inventory (python_uv_tools/defaults/main.yml):
| Tool | --with / --with-executables-from |
needs_modern_gcc |
|---|---|---|
thefuck |
(python 3.11; distutils workaround) | — |
apprise |
— | — |
sqlit-tui[ssh] |
psycopg2-binary, pymysql, mssql-python, pyodbc, duckdb |
✓ (gcc ≥ 9) |
python-dotenv[cli] (dotenv) |
— | — |
git-filter-repo |
— | — |
mlflow |
— | ✓ |
tmuxp |
— | — |
visidata (vd) |
pandas, pyarrow |
✓ |
copyparty |
— | — |
trafilatura |
— | — |
marimo[recommended,mcp] |
httpx[socks] |
✓ |
jupyterlab (jupyter-lab) |
marimo[sandbox], marimo-jupyter-extension, ipykernel, ipywidgets; with_executables_from: notebook, jupyter-core; extra_binaries: jupyter, jupyter-notebook |
✓ |
xonsh |
xontrib-jedi, xontrib-zoxide, xontrib-pipeliner, xontrib-fzf-widgets |
— |
yt-dlp[default] |
required_distributions: yt-dlp-ejs (EJS challenge solver; Node is enabled by yth/ytmv) |
— |
markitdown[docx,xlsx,pptx] |
— (extras in name; pulls pandas 3.x) | ✓ |
From llm_tools/defaults/main.yml:
| Tool | Pin |
|---|---|
litellm[proxy] (litellm) |
--python 3.13, --with httpx[socks] |
Ad-hoc uv tool install outside the defaults YAML:
coding_agents/tasks/main.yml:1153-1184—specify-cli(fromgit+...)security_tools/tasks/main.yml—pre-commit(--python 3.13, avoids the Homebrew Python 3.14 pyexpat ABI mismatch)iac_tools/tasks/main.yml—azure-cliuser fallback (Linux noRoot)
Upgrade: just upgrade-uv → uv self-update (dispatched) + uv tool upgrade --all.
Adding a uv tool: append to python_uv_tools/defaults/main.yml.
If the package writes companion binaries beyond the entry point, declare
both with_executables_from: AND extra_binaries: — otherwise the
install guard short-circuits on hosts where the primary binary already
exists and shims are never written (pitfalls/uv-tool-install-creates-guard-misses-executables-from.md).
If gcc < 9 is in scope, set needs_modern_gcc: true.
6. npm (global Node CLIs)¶
Config: dot_ansible/roles/js_cli_tools/defaults/main.yml for the
JS-tools-specific list. Scattered global-npm installs live in other
roles (coding_agents, bitwarden, lazyvim_deps, devtools).
Install dispatch (universal pattern across roles):
# On Linux:
mise exec -- npm install -g <package>
# Falls back to system npm if mise has no node registered
# On macOS:
community.general.npm: { name: <package>, global: true }
Linux explicitly uses mise exec -- npm to avoid the Ubuntu apt npm
(too old; system Node 12 fails engine checks for modern packages and
/usr/local/lib/node_modules is root-owned, breaking --global for
non-root users). See pitfalls/ansible-js-cli-tools-old-system-node.md.
Pi deliberately prefers mise on both operating systems and adds
--prefix ~/.local: its Node 22.19 requirement is shared with pia, and the
stable prefix survives a mise Node-version replacement. The role installs and
verifies that canonical copy first, then removes exact deprecated/duplicate Pi
packages from another active npm prefix so a failed migration cannot destroy a
working command.
Inventory:
| Package | Binary | Role / line |
|---|---|---|
readability-cli |
readable |
js_cli_tools/defaults/main.yml |
@bitwarden/cli |
bw |
bitwarden/tasks/main.yml |
@earendil-works/pi-coding-agent |
pi |
coding_agents/tasks/main.yml (--ignore-scripts --prefix ~/.local; removes deprecated @mariozechner/pi-coding-agent) |
@githubnext/github-copilot-cli |
github-copilot-cli |
coding_agents/tasks/main.yml:134-168 |
@openai/codex |
codex (Linux only — macOS uses brew cask) |
coding_agents/tasks/main.yml:172-236 |
@google/gemini-cli |
gemini (Linux always; macOS fallback if brew formula fails) |
coding_agents/tasks/main.yml:240-300 |
@openchamber/web |
openchamber |
coding_agents/tasks/main.yml:1188-1222 |
tldr |
tldr (Linux only — macOS uses tlrc via brew) |
devtools/tasks/main.yml |
tree-sitter-cli |
tree-sitter |
lazyvim_deps/tasks/main.yml (preferred over cargo fallback) |
Upgrade: just upgrade-npm → npm -g update with mise exec --
fallback.
Pi uses a fixed ~/.local prefix, so just upgrade-agents tries
pi update --self and falls back to an npm reinstall with that same prefix.
Adding an npm tool: append to js_cli_tools/defaults/main.yml (for
general utilities); for an agent CLI, add the install task to
coding_agents/tasks/main.yml matching the existing dispatch pattern
(normally macOS via system npm and Linux via mise exec -- npm install -g;
use a stable user prefix when the command must survive runtime upgrades).
7. cargo (Rust crates)¶
Config: dot_ansible/roles/rust_cargo_tools/defaults/main.yml
(currently cargo_tools: [] — no generic entries).
Rust install: mise install rust@stable (preferred — the spec MUST match
rust = "stable" in dot_config/mise/config.toml.tmpl, see
pitfalls/mise-warn-missing-rust-stable-every-prompt.md), with two
fallback layers on noRoot/oldEL boxes: direct rustup-init, then
https://static.rust-lang.org when the configured mirror's manifest
is stale.
Inventory (hard-coded in tasks/main.yml):
| Crate | Mechanism | Notes |
|---|---|---|
recon |
cargo install --git https://github.com/gavraz/recon --locked |
git source (no crates.io publish yet) |
pueue |
macOS brew formula; Linux cargo install pueue --locked |
+ pueued.service systemd user unit; version check pueue_min_version=4.0.0 (warn-only) |
tree-sitter-cli |
cargo install tree-sitter-cli (fallback only) |
preferred path is mise exec -- npm install -g tree-sitter-cli; cargo path installs libclang-dev/clang-devel first |
alacritty |
cargo build (gui_apps_linux) |
requires apt build deps (cmake, pkg-config, libfontconfig1-dev, libxcb-xfixes0-dev, libxkbcommon-dev) |
modelsdev |
cargo fallback (llm_tools) |
only when Linuxbrew not present + brew formula models not available |
cargo-update |
cargo install cargo-update (bootstrap for upgrade) |
installed by upgrade_tools.sh::cat_cargo on first run, then cargo install-update -a uses it |
Upgrade: just upgrade-cargo → cargo install-update -a (which
covers everything installed via cargo, including pueue on Linux).
Adding a cargo tool: prefer cargo_tools: [<name>] in the defaults
YAML (covered by cargo install-update -a); hard-code in tasks/main.yml
only when the install needs extra build deps or a non-crates.io source
(see recon, pueue).
8. go (Go CLI tools)¶
dot_ansible/roles/go_tools/defaults/main.yml now contains Linux gopls only;
Homebrew owns macOS gopls. This role remains gated by installExtraRuntimes,
uses mise Go, installs into ~/.local/bin with GOPATH=~/.local/share/go, and
preserves its creates: install-only guard. just upgrade-go explicitly runs
go install <package>@latest for this non-personal manifest.
Personal tools (Homebrew / verified releases / Go source)¶
installPersonalTools selects thirteen CLIs with binary packages. The single registry is
dot_ansible/roles/personal_tools/files/tools.json; the personal_tools role and
just upgrade-personal share its selection and owner-aware installer.
The binary packages use Homebrew formulas on macOS and checksummed release archives on Linux amd64/arm64. Fresh install pins are recorded per tool; apply never upgrades an existing binary. Unknown binaries are preserved. Linux user installs need no sudo or Go SDK. Download/checksum/version failures preserve the previous installation and never silently fall back to compilation.
A missing key retains the old selection and channels: macOS dev/translate via
Brew; Linux dev/translate and macOS/Linux lazyclash via Go only when extra
runtimes are enabled. Those Go installs still use the old fresh-install pins and
XDG output/cache paths, and skip when no Go exists. gopls stays outside this
suite. Backend daemons and credentials are never installed by the suite.
The six new members are lazyansible, lazycrontab, lazyfind, lazypkg,
lazymermaid and lazyset. Their immutable public releases and personal tap
formulas use the same paths and validation as the original suite. No new SDK,
first-use download or backend installation is added. Runtime prerequisites and
per-tool baseline versions are documented in the personal-tools guide.
See Personal tools for migration, ownership receipts,
source-to-Brew backups, explicit upgrade, and platform limits. Register completion
commands in scripts/generate_completions.sh; installer upgrades refresh only the
changed tool's Bash/Zsh output.
9. dotnet (global .NET tools)¶
Config: dot_ansible/roles/dotnet_tools/defaults/main.yml
.NET SDK ships via mise install --yes dotnet@10, matching the managed
mise config and its explicit dotnet.isolated = true setting. The role checks
the resolved SDK executable, not just its parent directory, and never rewrites
the config with mise use. Linux native libraries are install-only apt/dnf
dependencies tagged sudo; noRoot hosts must already provide them.
Once SDK is installed, the role uses mise exec dotnet@10 -- dotnet tool install --global
for each entry — drops binaries into ~/.dotnet/tools/ (already on PATH
via dot_config/shell/00_exports.sh.tmpl).
Inventory:
| Package | Binary |
|---|---|
azure-cost-cli |
azure-cost |
Upgrade: just upgrade-dotnet → re-runs dotnet tool update --global
per tool.
Adding a dotnet tool: append to dotnet_tools/defaults/main.yml
with name (NuGet package id), binary (the actual command name), and optional
smoke_args (an argv list for an offline startup check). azure-cost uses
smoke_args: [--help]; it is checked even when already installed.
10. gem (Ruby gems)¶
Config: dot_ansible/roles/ruby_gem_tools/defaults/main.yml
Ruby install: mise install ruby@3 (skipped in noRoot Linux).
Build deps installed first: libffi-dev, libyaml-dev (Debian) /
libffi-devel, libyaml-devel (RedHat).
Inventory:
| Gem | Binary |
|---|---|
try-cli |
try |
tmuxinator |
tmuxinator |
Plus toolkami.rb fetched as a chezmoi external (see § 13 — runs
standalone, not via gem install).
Upgrade: just upgrade-gem → gem update.
Adding a gem: append to ruby_gem_tools/defaults/main.yml (will
auto-skip on noRoot Linux since ruby isn't installed there).
11. curl-installer (self-managed CLIs)¶
Tools that ship via vendor install.sh instead of any package manager.
Mostly the AI coding agents (each vendor maintains their own installer
+ self-update subcommand). All landing paths are user-writable so
in-binary self-update works without sudo.
Inventory:
| Tool | Install URL | Lands at | Source role |
|---|---|---|---|
| Homebrew | https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh |
/usr/local/bin/brew (macOS) / /home/linuxbrew/.linuxbrew/ (Linux) |
bootstrap |
| uv | https://astral.sh/uv/install.sh |
~/.local/bin/uv (or ~/.cargo/bin/uv) |
bootstrap |
| mise | https://mise.run |
~/.local/bin/mise |
bootstrap |
| just | https://just.systems/install.sh |
/usr/local/bin/just or ~/.local/bin/just |
base |
| starship | https://starship.rs/install.sh |
/usr/local/bin/starship or ~/.local/bin/starship |
starship |
| atuin | https://setup.atuin.sh |
~/.atuin/bin/atuin |
atuin |
| docker | https://get.docker.com |
system docker (rootless variant) | docker (Linux) |
| zoxide | (vendor installer) | ~/.local/bin/zoxide |
devtools (Linux fallback) |
| direnv | (vendor installer) | ~/.local/bin/direnv |
devtools (Linux fallback) |
| Claude Code | https://claude.ai/install.sh |
~/.claude/local/bin/claude |
coding_agents (Linux) |
| OpenCode | https://opencode.ai/install (+ --no-modify-path) |
~/.opencode/bin/opencode |
coding_agents (macOS + Linux) |
| Oh My Pi | https://omp.sh/install (--binary) |
~/.local/bin/omp |
coding_agents (macOS + Linux x64/arm64) |
| Cursor CLI | https://cursor.com/install |
~/.local/bin/cursor-agent |
coding_agents |
| Antigravity CLI | https://antigravity.google/cli/install.sh (patched --skip-path --skip-aliases) |
~/.local/bin/agy + agyc symlink |
coding_agents |
| RTK | https://raw.githubusercontent.com/rtk-ai/rtk/master/install.sh |
~/.local/bin/rtk |
coding_agents (Linux) |
| llmfit | https://llmfit.axjns.dev/install.sh |
~/.local/bin/llmfit (or system) |
llm_tools (Linux) |
| Ollama | https://ollama.com/install.sh |
system path (Linux, sudo) | llm_tools (Linux) |
| Ookla speedtest | https://install.speedtest.net/app/cli/install.<distro>.sh |
system | networking_tools (Linux) |
Why curl-installer over a package manager:
- Fast-moving releases — vendor controls cadence (Claude, OpenCode, Cursor, Antigravity push daily/weekly)
- Self-update subcommand exists and the installer is the canonical
reinstall path →
cat_agentsinscripts/upgrade_tools.shcan call either - Avoids the "brew formula is 3 weeks stale" problem (see commit
history for the OpenCode move from
anomalyco/tapto official installer) - No tap/PPA/repo to maintain
Adding a curl-installer tool:
- Pick a role (or add to
coding_agentsif it's an agent) - Use the canonical pattern:
whichcheck →creates:guard on the binary path →curl ... | bash [-s -- --no-modify-path]shell task - If the installer mutates shell rc files, add a
--no-modify-pathflag (or equivalent — see OpenCode and Antigravity blocks). The chezmoi-managed PATH is the SSOT - Update
scripts/upgrade_tools.sh::cat_agentsif the tool has a self-update subcommand (or just an installer-as-upgrader path) - If the binary lands somewhere other than
~/.local/bin(e.g.~/.opencode/bin/opencode), add an existence-gated PATH prepend todot_config/shell/00_exports.sh.tmpl. When the managed path must override later mise/Bun/package-manager entries, reassert it in a later numbered shared fragment instead (the Pi/OMP/pia pattern lives indot_config/shell/08_pi_agents.sh.tmpl)
12. GitHub-release downloads (user-level fallback)¶
When apt/yum can't satisfy a version requirement (e.g. Apple Silicon
brew bottle missing, RHEL/CentOS package too old, noRoot host with no
sudo), roles fall back to downloading the latest GitHub release tarball
and extracting to ~/.local/bin (user-writable, already on PATH).
Canonical pattern:
- name: Get latest <tool> release
ansible.builtin.uri:
url: https://api.github.com/repos/<owner>/<repo>/releases/latest
return_content: true
register: <tool>_release
- name: Compute <tool> arch
ansible.builtin.set_fact:
<tool>_arch: "{{ 'amd64' if target_architecture in ['x86_64', 'amd64'] else 'arm64' if ... else 'unsupported' }}"
- name: Download / extract / copy into ~/.local/bin
block:
- get_url: { url: "https://github.com/.../{{ tag }}/{{ asset }}", dest: /tmp/..., retries: 3, delay: 5, timeout: 120 }
- unarchive: { src: ..., dest: /tmp/..., remote_src: true }
- find: { paths: /tmp/..., patterns: <binary>, file_type: file }
- copy: { src: ..., dest: "{{ ansible_facts['env']['HOME'] }}/.local/bin/<binary>", mode: '0755' }
rescue:
- debug: "<tool> installation failed (likely network timeout) - skipping"
Tools using this pattern (rough list — see each role's tasks/main.yml):
base:ripgrep,fd,jq,git-lfsdevtools:gh,glab,bat,bats,eza,git-delta,diffnav,git-graph,glow,gum,vhs,freeze,duckdb,yazi,superfile,btop,zellij,sesh,worktrunk,workmux,taplo,tv(television),lnav,tailspin,dasel,yq,jnv,witrnetworking_tools:doggo,gping,trippy,bandwhich,rustscan,cloudflaredneovim: nvim tarball (Linux fallback + macOS Intel/minimum-version fallback)lazyvim_deps:lazygitfallbackcoding_agents:specstory,codexbar,td,sidecar,agy(all also have brew formula / cask as primary)coding_agents/tasks/tasks.yml: Tasks suite; Linuxbrew fallback verifies the official release checksum and installstasks,tasks-tui, andtasks-apitogether. Complete suites are not refreshed; use the explicit Tasks upgrade procedure.security_tools:gitleaksiac_tools:terraform,opentofuuser fallback (zip fromreleases.hashicorp.com/ GitHub)gui_apps_linux:AppImageLauncher Lite, Zen Browser AppImagebitwarden: Desktop.debfallback (Linux)
⚠️ Coverage gap: GitHub-release downloads use a creates: guard
on the binary path, so once installed they're never refreshed by
chezmoi apply — and scripts/upgrade_tools.sh has no category for
them. The only way to refresh: rm the binary, re-apply. See
§ Coverage gaps
below.
Adding a GitHub-release tool: copy the canonical block from
e.g. devtools/tasks/main.yml (look for an existing tool with a
similar arch matrix), substitute owner/repo/asset-name. Always wrap in
block: / rescue: (vendor-side outages happen) and use
retries: 3, delay: 5, timeout: 120 on the download.
13. chezmoi externals (refresh-periodic git/file fetches)¶
Config: .chezmoiexternal.toml.tmpl
chezmoi apply fetches these on a schedule. All set
refreshPeriod = "168h" (weekly), --depth 1 clones, --ff-only
pulls.
| Path | Type | Source | Gating |
|---|---|---|---|
.oh-my-zsh |
git-repo | ohmyzsh/ohmyzsh.git |
always |
.oh-my-zsh/custom/plugins/zsh-autosuggestions |
git-repo | zsh-users/zsh-autosuggestions.git |
always |
.oh-my-zsh/custom/plugins/zsh-syntax-highlighting |
git-repo | zsh-users/zsh-syntax-highlighting.git |
always |
.oh-my-zsh/custom/plugins/zsh-completions |
git-repo | zsh-users/zsh-completions.git |
always |
.oh-my-zsh/custom/plugins/zsh-vi-mode |
git-repo | jeffreytse/zsh-vi-mode.git |
enableVimMode |
.oh-my-bash |
git-repo | ohmybash/oh-my-bash.git |
always |
.local/src/blesh |
git-repo (--recurse-submodules) |
akinomyoga/ble.sh.git |
always; compiled by run_onchange_after_26_install_blesh.sh.tmpl → ~/.local/share/blesh/ble.sh |
.tmux/plugins/tpm |
git-repo | tmux-plugins/tpm.git |
always |
.fzf |
git-repo | junegunn/fzf.git |
Linux only (ansible runs ~/.fzf/install --bin) |
.local/share/pi-agents |
git-repo | daviddwlee84/pi-agents.git |
installCodingAgents; exposes bin/pia, while runtime state stays outside the checkout |
.local/share/toolkami/toolkami.rb |
file (executable) | https://raw.githubusercontent.com/aperoc/toolkami/refs/heads/main/toolkami.rb |
always |
Why externals (not git clone in a role):
- Refresh cadence is built-in (chezmoi handles
git pullon schedule; ansible roles don't have a "re-pull every N hours" primitive) - One place to see "what third-party source code lives on this machine"
chezmoi update --refresh-externalsis the explicit forced refresh
Upgrade: just upgrade-externals → chezmoi apply --refresh-externals.
Adding an external: prepend an entry to .chezmoiexternal.toml.tmpl.
If the cloned repo needs a post-clone compile step (like ble.sh),
attach a run_onchange_after_NN_*.sh.tmpl script that re-runs when
the external's commit changes.
Past pitfalls:
- ble.sh local-changes-overwritten (
pitfalls/chezmoi-update-blesh-local-changes-overwritten.md) — source clone moved out of the chezmoi external path to decouple frommake install
14. apt / yum (system packages)¶
The pragmatic Linux backbone. Roles use apt/yum for tools that are well-packaged by the distro (i.e. recent enough, available, no notable upstream-vs-downstream skew).
Used by these roles (non-exhaustive):
| Role | apt packages | yum packages |
|---|---|---|
base |
git, git-lfs, curl, wget, ripgrep, fd-find, jq, tree, build-essential |
git, git-lfs, curl, wget, jq, tree, gcc, gcc-c++, make |
bash |
(uses brew on Linux only when primary_shell=bash) | — |
zsh |
zsh |
zsh (or ncurses-devel + source build on RHEL 7) |
devtools |
bat, gh, glab, eza, git-delta, htop, pandoc, tldr, etc. (many) |
similar subset |
media_tools |
ffmpeg, imagemagick, libimage-exiftool-perl, libvips-tools |
— |
networking_tools |
nmap, arp-scan, mtr, iperf3, httpie, tcpdump |
same (minus httpie) |
coding_agents |
libnotify-bin (Debian only) |
— |
lazyvim_deps |
(mostly via mise + GitHub releases) | — |
auditd |
auditd, audispd-plugins |
audit |
gui_apps_linux |
cmake, pkg-config, font libs (Alacritty build), libfuse2, copyq, playerctl, wmctrl, xdotool, wl-clipboard, xclip, xsel |
— |
docker |
uidmap, dbus-user-session, fuse-overlayfs, slirp4netns, iptables (rootless prereqs) |
— |
iac_tools |
azure-cli, terraform, opentofu (via vendor apt repos) |
— |
nerdfonts |
fontconfig |
fontconfig |
python_uv_tools |
libffi-dev, libyaml-dev, python3-dev (Ruby+ansible-cryptography compile deps) |
libffi-devel, libyaml-devel, python3-devel |
Vendor apt repos managed (require GPG key + sources.list.d entries):
| Repo | What |
|---|---|
apt.releases.hashicorp.com |
terraform |
packages.microsoft.com |
azure-cli, vscode |
cli.github.com/packages |
gh |
gitlab.com/api/v4/projects/.../packages/debian |
glab |
pkgs.gierens.de |
eza |
ngrok-agent.s3.amazonaws.com |
ngrok |
apt.releases.opentofu.org |
opentofu (Cloudsmith) |
linux-packages.resilio.com |
resilio-sync |
PPA ppa:lazygit-team/release |
lazygit |
PPA ppa:appimagelauncher-team/stable |
AppImageLauncher |
Upgrade: NOT covered by scripts/upgrade_tools.sh. Relies on
sudo apt upgrade / sudo yum update outside the repo's flow. This is
an intentional gap — distro upgrades are a sysadmin decision, not a
dotfiles decision.
Adding an apt/yum package: in the relevant role's tasks/main.yml,
add an ansible.builtin.apt / ansible.builtin.yum task gated on
os_family. Always become: true + tags: [sudo] (so noRoot mode
skips it gracefully) and consider adding a user-level fallback for the
noRoot path if the tool is broadly useful.
15. ya pkg (Yazi plugins)¶
Yazi's built-in plugin manager. ya (the yazi CLI companion) clones a plugin, copies it into ~/.config/yazi/plugins/, and pins its revision in ~/.config/yazi/package.toml.
yaandyaziare a matched pair. Every upstream release asset ships both binaries; installing onlyyaziyields a host that can never acquire a plugin, and yazi then refuses to start at all (Failed to load plugin from …/duckdb.yazi/main.lua). Thedevtoolsrole installs and probes for both. See pitfalls/yazi-lua-runtime-failed-plugin-main-lua.md.
In this repo the lockfile is chezmoi-managed (dot_config/yazi/package.toml) and plugins are materialized by .chezmoiscripts/global/run_after_45_yazi_plugins.sh.tmpl, which runs ya pkg install whenever a declared plugin is missing from ~/.config/yazi/plugins/ or the lockfile hash changed. (It is deliberately run_after_, not run_onchange_: the plugins live outside chezmoi's tree, so a content hash cannot detect them going missing.) ya pkg install rewrites package.toml (normalizes it and strips comments), so the managed source lockfile is kept comment-free to match its canonical output — that's what avoids chezmoi drift (never add comments to it).
| Plugin | Purpose |
|---|---|
yazi-rs/plugins:piper (piper.yazi) |
"pipe any shell command as a previewer" — backs the Office-document previewers in dot_config/yazi/yazi.toml (piper -- view-office --preview …) and the feather/sqlite preview fallbacks. See office-viewers.md. |
wylie102/duckdb (duckdb.yazi) |
DuckDB-powered table previewer for data files (csv/tsv/parquet/xlsx/db/duckdb) — run = "duckdb" in dot_config/yazi/yazi.toml + required (pcall-guarded) require("duckdb"):setup{} in dot_config/yazi/init.lua. Uses the duckdb CLI. See data-viewers.md. |
yazi-rs/plugins:git (git.yazi) |
Git state signs for files and directories. The managed git-guard fetcher delegates on Yazi 26.8.15+ and becomes a noop on older hosts until the explicit package upgrade. See yazi.md. |
Add a plugin: ya pkg add <owner>/<repo>:<plugin>, then copy ~/.config/yazi/package.toml back into the source. Upgrade: ya pkg upgrade → just upgrade-yazi-plugins (install-only by design; apply never bumps revs). Requires a recent yazi (ya pkg replaced the older ya pack).
Multi-mechanism tools + rationale¶
Tools where the install path differs by OS / arch / role-feature-flag, with rationale.
| Tool | Mechanism dispatch | Why |
|---|---|---|
| uv | Bootstrap: curl. Later: dispatch between brew upgrade uv vs uv self update based on binary path |
brew installs pin to versioned Cellar — uv self update no-ops there. Mirror logic in role + upgrade script keeps both paths working. See pitfall |
| OpenCode | curl-installer on both OSes (2026-05) | Was brew tap (anomalyco/tap) on macOS, but formula version lagged + Cellar-pinning broke opencode upgrade. Unified on official installer with --no-modify-path. See coding_agents/tasks/main.yml # === OpenCode block |
| OpenAI Codex CLI | macOS: brew cask; Linux: npm @openai/codex |
macOS has the brewed .app (richer); Linux only ships npm CLI |
| Gemini CLI | macOS: brew formula gemini-cli → npm fallback; Linux: npm |
brew formula availability is inconsistent |
| Specstory | macOS: brew tap → GitHub release fallback; Linux: GitHub release tarball | brew formula availability + version freshness |
| CodexBar | macOS: brew cask (homebrew-cask core, universal); Linux: Linuxbrew formula → GitHub release | macOS ships the menu bar app, which bundles the CLI (a superset of the formula); Linux has CLI only |
| td / sidecar | macOS: brew tap; Linux: Linuxbrew → GitHub release | same maintainer (marcus/tap); Linuxbrew is fast path when present |
| Tasks suite | macOS: marcus/tap/tasks; Linux: Linuxbrew → checksum-verified release |
Same marcus/tap family as td/Sidecar; all three binaries install together, while a complete existing suite stays install-only |
| Ollama | brew formula always (CLI); brew cask ollama-app when installAiDesktopApps + installLlmTools; Linux: curl ollama.com/install.sh |
CLI vs GUI split; vendor installer is the canonical Linux path |
| fzf | macOS: lazyvim_deps brew. Linux: chezmoi external + ~/.fzf/install --bin |
brew is fine on macOS; Linux gets the latest git tip |
| bash | system bash always; homebrew bash 5.x only when primary_shell="bash" on macOS |
macOS ships bash 3.2 (GPL2 vintage); brew bash 5 needed only if user actually wants it as login shell |
| gh | macOS: brew (via devtools); Linux: vendor apt repo → user tarball fallback |
brew is canonical macOS; vendor apt repo is canonical Linux |
| tmux | macOS: brew; Linux: apt → nelsonenzo/tmux-appimage when system < 3.3 |
display-menu popup requires 3.3+ — see AGENTS.md tmux hard invariant |
| Discord | macOS: brew cask; Linux: flatpak (default) or .deb via discordChannel chooser |
apt has no Discord; flatpak is sandboxed-and-current, .deb is for users who hate flatpak |
| Steam | macOS: brew cask gated by installGamingApps; Linux: Valve apt repo + steam-launcher gated by installGamingApps |
Valve's apt repo is the supported Linux path; Flathub exists but is community-packaged |
| just, starship, zoxide, direnv | macOS: brew; Linux: vendor curl-installer | Linux distro packages are too stale for these fast-moving tools |
| Neovim | macOS: healthy install → Apple Silicon brew (state: present) → checksum-verified official release; Intel skips brew when installation is needed. Linux: apt → GitHub release tarball → user fallback → neovim/neovim-releases on oldEL |
Min version 0.11.2 is enforced without turning chezmoi apply into an upgrade; no snap by policy |
| LazyGit | existing >= 0.64.0 → Homebrew-managed stale formula upgrade → checksum-verified official release (/usr/local/bin on Linux, then ~/.local/bin fallback on all supported OSes) |
The managed git.diffRenderers schema is silently ignored before 0.64, so this is a minimum-version repair rather than a general upgrade |
| tree-sitter-cli | npm via mise (preferred, timeout 180) → cargo fallback | npm is faster; cargo needs libclang to compile. Same binary either way |
| bw (Bitwarden CLI) | npm global (preferred) or system npm fallback | always npm, the dispatch is mise-npm vs system-npm |
Adding a tool with multi-mechanism dispatch: prefer ONE primary mechanism per OS, with at most ONE fallback. Multiple fallbacks add test combinations (5 levels × 5 OS variants = 25 cells) that nobody will actually exercise. If you can't decide, write the simplest version and add fallbacks only when an actual host fails.
Fallback chains¶
Notable cascading-fallback patterns:
| Tool | Chain |
|---|---|
| mise binary | curl https://mise.run (preferred) → direct binary download from mise.jdx.dev/mise-latest-linux-${arch} → musl variant (forced when glibc < 2.28) |
| Neovim (macOS) | existing version ≥ minimum → Apple Silicon Homebrew install → checksum-verified official arm64 release fallback; Intel uses the official x86_64 release directly when installation is needed |
| Neovim (Linux) | apt → GitHub release tarball when too old (/opt/nvim + symlink) → user-tarball to ~/.local → neovim-releases fork (oldEL glibc 2.17 rebuild) |
| LazyGit | existing version ≥ 0.64.0 → targeted Homebrew upgrade when formula-managed → checksum-verified official release to /usr/local/bin on Linux → user fallback to ~/.local/bin (also the macOS non-brew/PATH-shadow fallback) |
| Most Linux CLIs in base/devtools/networking_tools | apt/yum (sudo) → user-level GitHub release musl tarball to ~/.local/bin. Several have arch-specific brew-on-aarch64 branch (eza, git-delta) |
| Rust (mise) | configured mirror → official static.rust-lang.org → on oldEL: rustup-init directly |
Cursor .deb (Linux) |
get_url with retries=4 (large file, GFW-prone) — single mechanism but heavily retried |
| Bitwarden Desktop (Linux) | snap → .deb fallback |
| CodexBar / td / sidecar / RTK / specstory / llmfit | Linuxbrew → GitHub release tarball |
| Tasks suite (Linux) | Complete existing install → Linuxbrew marcus/tap/tasks → checksum-verified amd64/arm64 release with all three binaries in ~/.local/bin |
| uv (upgrade-side) | inspect binary path → brew upgrade uv or uv self update |
| Discord (Linux) | flatpak (default) → .deb (selectable via discordChannel) |
| tree-sitter-cli (Linux) | mise exec -- npm install -g tree-sitter-cli (timeout 180) → cargo install tree-sitter-cli (installs libclang-dev first) |
| Antigravity installer | curl → sed patch in-flight to inject --skip-path --skip-aliases → bash (avoids profile mutation) |
| just | macOS brew → Linux: vendor curl install.sh with sudo → user-level --bin-dir ~/.local/bin fallback |
Why fallbacks: noRoot Linux hosts, RHEL/CentOS 7 (glibc 2.17, no modern bottles, ancient zsh), macOS configurations without a suitable Homebrew bottle, Chinese-mirror outages.
Coverage gaps (install-only, no automated upgrade)¶
Tools that chezmoi apply installs but no upgrade_tools.sh category
ever refreshes. These tools stay frozen at install-time version until
manually removed and re-installed.
| Class | Why no upgrade | How to refresh |
|---|---|---|
| GitHub-release user-level fallbacks (~30 tools — see § 12) | creates: guard means ansible never re-downloads |
rm ~/.local/bin/<tool> then chezmoi apply |
| Tasks Linux release suite | Complete-suite guard keeps apply install-only; no automatic release upgrade category | Verify the chosen release checksum and replace all three binaries together; see Tasks upgrades. Brew installs stay with just upgrade-brew |
| Vendor apt-repo packages on Debian (terraform, azure-cli, vscode, opentofu, ngrok, lazygit-from-PPA) | Rely on sudo apt upgrade outside this repo |
sudo apt update && sudo apt upgrade (not chezmoi's job) |
| Docker convenience-script install | No upgrade target | re-run curl get.docker.com \| sh |
| OrbStack | Macos brew cask | Covered by cat_brew --cask --greedy |
Cursor .deb / Discord .deb / VSCode (Microsoft apt) |
apt-side, not chezmoi-side | apt-upgrade |
| Zen Browser AppImage / AppImageLauncher Lite | One-shot download; Zen's guard globs zen*.AppImage (AppImageLauncher renames integrated copies) |
Delete every ~/Applications/zen*.AppImage, re-apply |
| Hack Nerd Font | latest URL, no version tracking |
Delete font dir, re-apply |
| fontconfig / libfuse2 / libnotify-bin / playerctl / wmctrl / xdotool / wl-clipboard / xclip / xsel | System packages, no upgrade automation | apt-upgrade |
| auditd rules | Config files, not a "tool" with versions | Edit rule template, re-apply |
| claude-hud / LazyVim plugins / TPM plugins / pre-commit / tldr cache / gh extensions | Covered by cat_plugins in upgrade_tools.sh |
just upgrade-plugins |
| Mason tools (including Prettier) | LazyVim installs missing tools only; plugin upgrades do not upgrade tool versions | In Neovim, :MasonUpdate, wait for completion, then :MasonInstall prettier (or the selected tool) |
Why this matters: a developer who runs just upgrade-all on a Linux
host gets brew + mise + uv + npm + cargo + dotnet + gem + flatpak +
agents + plugins moved forward. But ~30 GitHub-release-installed CLIs
(many of them widely-used: ripgrep, fd, jq, git-delta, lazygit, …)
stay pinned to install time forever. This is partially mitigated
by:
- These tools on macOS generally go via brew, which
cat_brewcovers; Neovim and LazyGit have narrow minimum-version release fallbacks - On Linux non-noRoot, distro packages get apt-upgrade outside the repo
- The gap is sharpest on noRoot Linux where GitHub releases are the only install path AND there's no upgrade automation
Coverage gap on cat_agents: only claude / opencode / pi / omp /
cursor-agent / ollama / llmfit / rtk / specstory get an explicit guarded
agent-specific upgrade path.
Missing: agy (Antigravity), codexbar, td, sidecar,
specify-cli, openchamber, codex — these rely on brew / uv / npm
categories instead.
Tracked on backlog: no formal item yet, but adding a releases
category to scripts/upgrade_tools.sh (audit installed binaries
against latest GitHub tag, prompt for refresh) would close this. See
upgrades.md for the current category
list.
Tool index (A–Z)¶
Alphabetical lookup. For tools with multiple install paths, the primary path is listed first; secondaries appear in the same row separated by
/.Generated — do not hand-edit the table. The SSOT is
dot_config/docs/tools/tool-catalog.toml(which also carries each tool's category, one-line description and init-prompt gate). Edit an entry there, then runjust gen-tool-index; thetool-catalog-gen-checkpre-commit hook fails on drift. Browse it interactively — with live installed / missing / gated-off status for the current host — viatv inventory.
| Tool | macOS | Linux | Role |
|---|---|---|---|
| actionlint | brew | Linuxbrew (best-effort) | devtools |
| aerospace | brew cask (nikitabobko/tap) |
n/a | Brewfile.darwin |
| agy / agyc (Antigravity CLI) | curl antigravity.google/cli/install.sh |
same | coding_agents |
| alacritty | brew cask | cargo build (apt deps) | Brewfile.darwin / gui_apps_linux |
| alt-tab | brew cask | n/a | Brewfile.darwin |
| anki | brew cask | n/a | Brewfile.darwin |
| ansible-core | uv tool install | uv tool install | bootstrap |
| antigravity (IDE) | brew cask | placeholder | Brewfile.darwin |
| AppImageLauncher | n/a | PPA → GitHub .deb → Lite AppImage |
gui_apps_linux |
| applite | brew cask | n/a | Brewfile.darwin |
| apprise | uv tool | uv tool | python_uv_tools |
| arc | brew cask | n/a | Brewfile.darwin |
| arp-scan | brew | apt/yum | networking_tools |
| atuin | brew | curl setup.atuin.sh |
atuin |
| azure-cli | brew | Microsoft apt repo → uv tool fallback | iac_tools |
azure-cost-cli (azure-cost) |
dotnet tool install --global |
same | dotnet_tools |
| bandwhich | brew | GitHub release musl | networking_tools |
| bash (5.x) | brew (when primary_shell="bash") |
system | bash |
| bat | brew | apt + /usr/bin/bat symlink → GitHub musl |
base/devtools |
| bats-core | brew | apt → GitHub install.sh |
devtools |
Bitwarden CLI (bw) |
mise exec -- npm install -g @bitwarden/cli |
same | bitwarden |
| Bitwarden Desktop | brew cask | snap → .deb |
bitwarden |
| btop | brew | apt → GitHub release | devtools |
| build-essential | n/a | apt | bootstrap (Linux) |
| bun | mise | mise | mise |
calibre (ebook-meta) |
brew cask — opt-in installCalibre |
apt (calibre) — opt-in |
devtools — backs Kindle .mobi/.azw/.azw3 metadata previews in yazi via view-ebook. See yazi-previews.md |
| cargo-update | cargo install (bootstrap for cat_cargo) |
same | rust_cargo_tools |
| chafa | brew | apt (chafa) |
devtools — yazi image-display fallback (unicode-art); backs all image/pdf/video/heic previews. Shadowed by a --probe off shim (dot_dotfiles/bin/executable_chafa) to stop the OSC-query→rename-popup leak. See yazi-previews.md |
| chatgpt | brew cask (Intel + Apple Silicon; includes Codex) | n/a | Brewfile.darwin |
| Claude (desktop) | brew cask claude |
n/a | Brewfile.darwin |
| Claude Code | brew cask claude-code |
curl claude.ai/install.sh → ~/.claude/local/bin/claude |
coding_agents |
| claude-hud | python helper run from role | same | coding_agents |
| cloudflared | brew | GitHub .deb/binary / .rpm |
networking_tools (tunnel) |
| CMux | brew cask | n/a | Brewfile.darwin |
| CodeIsland | brew cask (wxtsky/tap) |
n/a | Brewfile.darwin |
| codex (OpenAI Codex CLI) | brew cask codex |
mise exec -- npm install -g @openai/codex |
coding_agents |
| CodexBar | brew cask codexbar (homebrew-cask core) |
Linuxbrew steipete/tap/codexbar → GitHub release; static-musl asset (and no Linuxbrew) when glibc < 2.38 |
coding_agents |
| copyparty | uv tool | uv tool | python_uv_tools |
| CopyQ | n/a | apt | gui_apps_linux |
| coreutils | brew | (GNU coreutils via apt; macOS gets via brew) | devtools |
| curl | system | apt/yum | base + bootstrap |
| cursor (IDE) | brew cask | .deb from cursor.com/api/download |
Brewfile.darwin / gui_apps_linux |
| cursor-agent (CLI) | curl cursor.com/install |
same | coding_agents |
| dasel | brew | release | devtools |
| dbeaver-community | brew cask | n/a | Brewfile.darwin |
dev-cli (dev) |
brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Repository/task/worktree dashboard; binary dev. |
| dev-projects | chezmoi (dot_dotfiles/bin/executable_dev-projects, Python stdlib) |
same | in-house |
| diffnav | brew | GitHub release | devtools |
| direnv | brew | apt or curl-installer | devtools |
| discord | brew cask | flatpak (default) / .deb |
Brewfile.darwin / gui_apps_linux |
| docker | brew cask orbstack |
curl get.docker.com rootless |
docker |
| doggo | brew | GitHub release | networking_tools |
| dotenv (python-dotenv[cli]) | uv tool | uv tool | python_uv_tools |
| dotnet | mise (10, isolated) |
mise (10, isolated) |
mise |
| doxx | brew (homebrew-core) | GitHub release .tar.xz |
devtools |
| duckdb | brew | GitHub release | devtools |
| duckdb.yazi | ya pkg (Yazi plugin) |
ya pkg |
devtools (yazi) |
| ethtool | n/a | apt / yum (ethtool) |
wake_on_lan |
| exiftool | brew | apt (libimage-exiftool-perl) |
media_tools |
exp-cli (exp) |
brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Research workflow CLI and overview; binary exp. |
| eza | brew | gierens.de apt repo → GitHub musl → brew aarch64 | devtools |
| fastfetch | brew | (Linux release if added) | devtools |
| fd | brew | apt (fd-find + symlink) → GitHub release |
base |
| ffmpeg | brew | apt | media_tools |
| ffmpeg-full | brew (keg-only) | — (Debian ffmpeg already links libass) | media_tools |
| figlet | brew | (apt likely) | devtools |
| font-hack-nerd-font | brew cask | GitHub release Hack.zip → ~/.local/share/fonts |
nerdfonts |
| fontconfig | (system) | apt/yum | nerdfonts |
| freeze | brew | GitHub release | devtools |
| fzf | brew (via lazyvim_deps) |
chezmoi external + ~/.fzf/install --bin |
lazyvim_deps + externals |
| gawk | brew | apt/yum | bash |
| gcc / gcc-c++ / make | (Xcode CLT) | apt (build-essential) / yum |
base + bootstrap |
| gemini (Gemini CLI) | brew formula gemini-cli → npm fallback |
mise exec -- npm install -g @google/gemini-cli |
coding_agents |
| gh | brew | vendor apt (.deb) → GitHub tarball | base/devtools |
| gh-dash | gh extension install dlvhdr/gh-dash |
same | devtools |
| gh-notify | gh extension install meiji163/gh-notify |
same | devtools |
| gh-select | gh extension install remcostoeten/gh-select |
same | devtools |
| ghostty | brew cask (installBrewApps) |
native apt → Ubuntu 24.04 community PPA → classic Snap | GUI profile only; sudo + x64/arm64 on Linux. See Ghostty |
| git | brew | apt/yum | base |
| git-delta | brew | .deb (x86_64) → brew aarch64 → GitHub musl user |
devtools |
| git-filter-repo | uv tool | uv tool | python_uv_tools |
| git-graph | brew | GitHub release (x86_64 only) | devtools |
| git-lfs | brew | apt/yum → GitHub release | base |
| git.yazi | ya pkg (Yazi plugin) |
ya pkg |
devtools (yazi) |
github-copilot-cli (@githubnext/github-copilot-cli) |
npm global | mise exec -- npm install -g |
coding_agents |
| gitleaks | brew | GitHub release | security_tools |
| glab | brew | vendor apt (.deb) → GitHub tarball | devtools |
| glow | brew | GitHub release tarball | devtools |
| go | mise (go = "latest") |
same (mise) | mise (installExtraRuntimes) |
| google-chrome-stable | (Brewfile cask territory) | .deb from dl.google.com; postinst self-registers the apt repo, so apt upgrade maintains it. x86_64 only — no arm64 Linux build exists |
gui_apps_linux — a Chromium-engine backup browser; Ubuntu has no Chromium .deb, only a snap shim |
| google-drive | brew cask | n/a | Brewfile.darwin |
| gopls | brew | go install (go_tools) |
Go language server for Claude Code's official gopls-lsp plugin |
| gping | brew | GitHub release musl | networking_tools |
| grammarly-desktop | brew cask | n/a | Brewfile.darwin |
| grc | brew | apt | devtools |
| gum | brew | GitHub release | devtools |
| herdr | GitHub release single binary → ~/.local/bin/herdr (not brew — see note) |
GitHub release single binary → ~/.local/bin/herdr |
devtools · upgrade: just upgrade-herdr (herdr update --handoff, must run outside herdr) |
| herdr-gpui | penso/tap/herdr-gpui cask; macOS 14.2+ | SHA-256 verified stable .deb; glibc 2.39+, x64/arm64, sudo | GUI bundle; apply installs missing copies only |
| herdr-plus (herdr plugin) | herdr plugin install cloudmanic/herdr-plus (prebuilt binary when no Go; idempotent ansible task) |
same | devtools |
| Homebrew | curl installer | same (Linux, gated) | bootstrap |
| htop | brew | apt | devtools |
| httpie | brew | apt (Debian) | networking_tools |
| imagemagick | brew | apt | media_tools |
input methods (mcbopomofo, squirrel) |
brew cask | (Debian) apt ibus-rime |
input_method |
| iperf3 | brew | apt/yum | networking_tools |
| ipmitool | n/a | apt/yum (gated on BMC detected) | homelab_tools |
| jnv | brew | release | devtools |
| jq | brew | apt/yum → GitHub release | base |
jupyterlab (jupyter-lab) |
uv tool (with notebook, ipykernel, etc.) | uv tool | python_uv_tools |
| just | curl just.systems/install.sh (always) |
same | base |
| lazyansible | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Ansible inventory/playbook workspace; personal fork. |
| lazychezmoi | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Chezmoi state, diff, edit and apply TUI. |
| lazyclash | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Mihomo CLI/TUI and guarded proxy shell integration. |
| lazycrontab | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Local/SSH cron inspection, schedules and reviewed edits. |
| lazyfind | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Local/SSH file and text search with result actions. |
| lazygit | brew → official release fallback (minimum 0.64.0) | stale PPA purge → brew detection → official system/user release (minimum 0.64.0) | lazyvim_deps |
| lazymermaid | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Mermaid repository workbench with optional Neovim and renderers. |
| lazymlflow | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — MLflow experiments, runs and artifact inspection. |
| lazypkg | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Software inventory and reviewed package-manager operations. |
| lazypueue | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Pueue task/queue dashboard. |
| lazyset | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Local/SSH TUI catalog and retained-session workspace. |
| libfuse2 | n/a | apt | gui_apps_linux |
| libnotify-bin | n/a | apt (Debian) | coding_agents |
libreoffice (soffice) |
brew cask | apt (libreoffice-writer/calc/impress, no-recommends) |
devtools |
| liquidctl | n/a | apt/yum (gated on a USB cooler / telemetry PSU detected) | homelab_tools |
litellm[proxy] (litellm) |
uv tool (python 3.13) | uv tool | llm_tools |
| llmfit | brew formula | curl llmfit.axjns.dev/install.sh |
llm_tools |
lm-sensors (sensors) |
n/a | apt/yum (lm-sensors/lm_sensors) |
homelab_tools |
| lnav | brew | apt/release | devtools |
| lolcat | brew | (apt) | devtools |
| maccy | brew cask | n/a | Brewfile.darwin |
| marimo | uv tool ([recommended,mcp]) |
uv tool | python_uv_tools |
markitdown ([docx,xlsx,pptx]) |
uv tool | uv tool | python_uv_tools |
| mas | brew (shared Brewfile, macOS only) | n/a | Brewfile.tmpl |
| micro | brew | apt / verified user-level release (no-root, RedHat) | devtools; editorcfg, just upgrade-micro for release installs |
| mise | curl mise.run |
curl mise.run → direct binary → musl variant |
bootstrap |
| mlflow | uv tool | uv tool | python_uv_tools |
| models | brew formula | Linuxbrew → cargo modelsdev |
llm_tools |
mole (mo) |
brew formula, gated on installMole |
n/a — macOS-only upstream | Brewfile.tmpl |
| mosh | brew | apt | devtools |
| mtr | brew | apt/yum | networking_tools |
| neovim | healthy install → Apple Silicon brew (state: present) → official release; Intel official release when needed |
apt → GitHub release → user → oldEL fork | neovim |
| ngrok | brew cask | vendor apt repo → user tgz / RedHat tgz | networking_tools (tunnel) |
| nmap | brew | apt/yum | networking_tools |
| node | brew (via lazyvim_deps) |
mise | mise + lazyvim_deps |
| nowplaying-cli | brew (gated installMediaControl) |
n/a | media_control |
nvme-cli (nvme) |
n/a | apt/yum (gated on NVMe detected) | homelab_tools |
| obsidian | brew cask | n/a | Brewfile.darwin |
| ollama | brew formula ollama (CLI) + cask ollama-app (GUI) |
curl ollama.com/install.sh |
llm_tools |
| omp (Oh My Pi) | curl omp.sh/install --binary → ~/.local/bin/omp |
same | coding_agents |
openchamber (@openchamber/web) |
npm global | mise exec -- npm install -g |
coding_agents |
| opencode | curl opencode.ai/install (--no-modify-path) → ~/.opencode/bin/opencode |
same | coding_agents |
| opencode-desktop | brew cask | n/a | Brewfile.darwin |
opentofu (tofu) |
brew formula | Cloudsmith apt repo → GitHub release | iac_tools |
| OrbStack | brew cask | n/a | docker |
| pandoc | brew | apt | devtools |
peon-ping (peon) |
brew tap PeonPing/tap (needs brew trust, Homebrew 6 gate) |
install.sh --openpeon --no-rc |
coding_agents — agent completion sounds; never run peon-ping-setup (hooks are declared in dot_claude/modify_settings.json.tmpl). Gated by the agentSounds prompt. See agent-sounds.md |
| pi | mise exec -- npm install -g --ignore-scripts --prefix ~/.local @earendil-works/pi-coding-agent |
same | coding_agents |
| pia | chezmoi external ~/.local/share/pi-agents/bin/pia |
same | .chezmoiexternal.toml.tmpl (installCodingAgents) |
| piper.yazi | ya pkg (Yazi plugin) |
ya pkg |
devtools (yazi) |
| playerctl / wmctrl / xdotool | n/a (playerctl: brew via media_control) | apt | gui_apps_linux; playerctl also via media_control (gated installMediaControl, backs sysplay/sysnow) |
playwright-cli (@playwright/cli) |
mise npm | mise npm (supported Debian/Ubuntu x64/arm64) | devtools, installPlaywrightCli; package skills refreshed on apply / upgrade-npm, Chromium preloading opt-in. See browser-tools |
poppler (pdftoppm) |
brew | apt (poppler-utils) |
devtools — yazi PDF preview. See yazi-previews.md |
| pre-commit | uv tool install --python 3.13 |
same | security_tools |
| prettier | Mason (Neovim-local) | Mason (Neovim-local) | LazyVim ensure_installed; Conform prefers project-local node_modules/.bin/prettier |
| procps | (system) | apt | bootstrap (Linux) |
| pueue | brew formula | cargo install pueue --locked + systemd user unit |
rust_cargo_tools |
| raycast | brew cask | n/a | Brewfile.darwin |
| rclone | brew | official installer or GitHub | devtools — macOS rclone mount also needs macFUSE (5.x on macOS 26+), manual & not repo-managed (reboot + GUI approval); brew rclone ≥1.73 already ships mount so the binary isn't the blocker (pitfall) |
readability-cli (readable) |
npm global | mise exec -- npm install -g |
js_cli_tools |
| recon | cargo install (git source) | same | rust_cargo_tools |
| resilio-sync | brew cask (GUI app) | Resilio apt repo → headless daemon + WebUI (per-user systemd service) | resilio_sync |
| resvg | brew | Linuxbrew → cargo install resvg (no apt pkg; non-fatal if absent) |
devtools — yazi SVG preview. See yazi-previews.md |
| ripgrep | brew | apt/yum → GitHub release | base |
| rsync | brew | apt / yum (usually preinstalled) | base — replaces macOS's built-in openrsync 2.6.9 (no --info etc.); brew rsync 3.x lands in brew's bin (/opt/homebrew/bin Apple Silicon, /usr/local/bin Intel) ahead of /usr/bin on PATH |
| ruby | mise | mise (skipped in noRoot) | mise |
| rust | mise | mise → rustup-init → static.rust-lang.org fallback |
mise + rust_cargo_tools |
| rustscan | brew | GitHub release | networking_tools |
| scroll-reverser | brew cask | n/a | Brewfile.darwin |
| sesh | brew | GitHub release | devtools |
sevenzip (7-Zip; 7zz / 7z) |
brew (sevenzip) |
apt (p7zip-full) |
devtools — yazi archive + dmg listing. See yazi-previews.md |
| shellcheck / shfmt | brew | apt or release | devtools |
| sidecar | brew tap marcus/tap |
Linuxbrew → GitHub release | coding_agents |
| skopeo | brew | apt (universe) |
devtools — client-side registry copy; rung 3 of docker-net pull. See docker-net.md |
smartmontools (smartctl) |
n/a | apt/yum | homelab_tools |
| specify-cli | uv tool install from git |
same | coding_agents |
| specstory | brew tap specstoryai/tap → GitHub release |
GitHub release tarball | coding_agents |
| speedtest (Ookla) | brew (tap teamookla/speedtest) |
install.speedtest.net |
networking_tools |
| spotify | brew cask | n/a | Brewfile.darwin |
| sqlit-tui[ssh] | uv tool | uv tool | python_uv_tools |
| starship | brew | curl starship.rs/install.sh |
starship |
| steam | brew cask (gated by installGamingApps) |
Valve apt repo (steam-launcher, x86_64, gated by installGamingApps) |
Brewfile.darwin / gui_apps_linux |
| storcli | n/a | vendor/mirror download to ~/.local/bin (gated on RAID controller detected + homelab_storcli_url; not in distro repos) |
homelab_tools |
| summarize | brew (gated installSummarize) |
mise exec -- npm install -g @steipete/summarize (Node 24+; skipped on oldEL) |
summarize — see summarize.md |
| super-productivity | brew cask | n/a | Brewfile.darwin |
| superfile | brew | installer/release | devtools |
| superset | brew cask (arm64) | n/a | Brewfile.darwin |
switchaudio-osx (SwitchAudioSource) |
brew (gated installMediaControl) |
n/a | media_control |
| tailscale (CLI) | via the tailscale-app cask wrapper (/usr/local/bin/tailscale → app; no formula) |
apt/yum from pkgs.tailscale.com (networking_tools role, tailscale tag, gated on installTailscale) |
Brewfile.darwin / networking_tools |
| tailscale-app | brew cask (pkg also installs the /usr/local/bin/tailscale CLI wrapper) |
n/a | Brewfile.darwin |
tailspin (tspin) |
brew | release | devtools |
| taplo | brew | apt+release | devtools |
Tasks (tasks / tasks-tui / tasks-api) |
brew (marcus/tap/tasks) |
Linuxbrew → verified GitHub release | coding_agents — all three binaries; user-owned XDG data/config. See tasks.md |
| td | brew tap marcus/tap |
Linuxbrew → GitHub release | coding_agents |
television (tv) |
brew | apt+release | devtools |
| terminal-browser | brew cask | checksum-verified upstream stable release (supported Debian/Ubuntu x64/arm64) | devtools, installTerminalBrowser; package skills, upgrade-brew / upgrade-terminal-browser. See browser-tools |
| terminal-notifier | brew | n/a | coding_agents (macOS) |
| terraform | brew (hashicorp/tap) |
HashiCorp apt repo → user zip | iac_tools |
| the-unarchiver | brew cask | n/a | Brewfile.darwin |
| thefuck | brew | uv tool (python 3.11 distutils) | devtools |
| tldr | (macOS uses tlrc via brew) |
system npm or mise exec -- npm install -g tldr |
devtools |
| tlrc | brew | (Linux uses tldr npm instead) |
devtools |
| tmux | brew | apt → tmux-appimage when < 3.3 | devtools |
| tmuxinator | gem | gem | ruby_gem_tools |
| tmuxp | uv tool | uv tool | python_uv_tools |
| toilet | brew | (apt) | devtools |
| tokens | brew tap owo-network/brew (trusted) |
GitHub release → ~/.local/bin (no systemd unit) |
coding_agents |
| trafilatura | uv tool | uv tool | python_uv_tools |
| translate | brew (daviddwlee84/tap) |
Verified GitHub release; legacy Go preserved | personal_tools — Terminal translation CLI/TUI. |
| tree | brew | apt/yum | base |
| tree-sitter / tree-sitter-cli | brew | mise-npm → cargo fallback | lazyvim_deps |
trippy (trip) |
brew | apt/release | networking_tools |
try-cli (try) |
gem | gem | ruby_gem_tools |
| tsnet | chezmoi (dot_dotfiles/bin/executable_tsnet, uv PEP-723 script) |
same | in-house — tailnet → ssh config, tailnet-HTTPS serve |
| uv | curl astral.sh/uv/install.sh |
same | bootstrap; python_uv_tools self-upgrade dispatch |
| vhs | brew | GitHub release | devtools |
| vips | brew | apt (libvips-tools) |
media_tools |
visidata (vd) |
uv tool (with pandas, pyarrow) | uv tool | python_uv_tools |
| visual-studio-code | brew cask | Microsoft apt repo | Brewfile.darwin / gui_apps_linux |
| wakeonlan | brew | apt (wakeonlan) |
networking_tools |
| warp | brew cask | n/a | Brewfile.darwin |
| wget | brew | apt/yum | base |
| witr | brew | (release) | devtools |
wl-clipboard (wl-copy/wl-paste) |
n/a (pbcopy built-in) |
apt | gui_apps_linux — Wayland clipboard backend for Neovim yank, lazygit Ctrl+O, x copy. See clipboard.md |
| workmux | brew (tap raine/workmux) |
GitHub release .tar.gz |
devtools |
| worktrunk | brew | GitHub release | devtools |
| xclip | n/a (pbcopy built-in) |
apt | gui_apps_linux — X11/XWayland clipboard backend (same consumers as wl-clipboard) |
| xonsh | uv tool (with xontribs) | uv tool | python_uv_tools |
| xsel | n/a (pbcopy built-in) |
apt | gui_apps_linux — X11 clipboard backend; last non-OSC-52 fallback in x, named in lazygit's error text |
| yazi | brew | Linuxbrew/GitHub release | devtools |
| yq | brew | release | devtools |
| yt-dlp | uv tool ([default] + yt-dlp-ejs) |
uv tool ([default] + yt-dlp-ejs) |
python_uv_tools |
| zellij | brew | GitHub release | devtools |
| Zen Browser | n/a | GitHub release AppImage → ~/Applications/zen.AppImage, thereafter matched by glob zen*.AppImage |
gui_apps_linux |
| zoxide | brew | curl official installer | devtools |
| zsh | brew | apt/yum / source build (RHEL 7) | zsh |
Tools not listed:
oh-my-zsh,oh-my-bash,zsh-autosuggestions,zsh-syntax-highlighting,zsh-completions,zsh-vi-mode,ble.sh,TPM,pi-agents,toolkami.rb— these live as chezmoi externals (§ 13), not as "binaries on PATH".
Decision tree for new tools¶
Is the tool needed BEFORE ansible runs?
├── Yes → Bootstrap script (run_once_before_*) — only if absolutely necessary
└── No → continue
Is it a runtime / language interpreter (node, ruby, python, rust, go, dotnet)?
├── Yes → mise (dot_config/mise/config.toml.tmpl) — except python (use uv)
└── No → continue
Is it a formatter / language server used primarily inside Neovim?
├── Yes → Mason via LazyVim ensure_installed; prefer project-local versions when provided
└── No → continue
Is it a Python CLI tool?
├── Yes → uv (append to python_uv_tools/defaults/main.yml)
│ · For LLM-related, llm_tools/defaults/main.yml
│ · Needs gcc ≥ 9? Set needs_modern_gcc: true
│ · Writes companion binaries? Set both with_executables_from and extra_binaries
└── No → continue
Is it a Node/JS CLI tool?
├── Yes → npm via mise (append to js_cli_tools/defaults/main.yml)
│ · Agent CLI? Add to coding_agents/tasks/main.yml instead
└── No → continue
Is it a Rust crate that's well-published on crates.io?
├── Yes → cargo (rust_cargo_tools/defaults/main.yml cargo_tools entry)
│ · Needs build deps or git-source? Hard-code in tasks/main.yml
└── No → continue
Is it a Ruby gem?
├── Yes → gem (ruby_gem_tools/defaults/main.yml)
│ · Skipped on noRoot Linux automatically
└── No → continue
Is it a .NET global tool?
├── Yes → dotnet tool (dotnet_tools/defaults/main.yml)
└── No → continue
Is it a Go CLI tool (installable via `go install`)?
├── Personal CLI suite → personal_tools registry (Homebrew / verified release / explicit Go source)
├── Other Go tool → go_tools/defaults/main.yml: name=<module>@<ver>, binary=<cmd>, platforms=[Linux,Darwin]
│ · Installs to ~/.local/bin via GOBIN; gated on installExtraRuntimes
│ · Upgrade handled by cat_go in scripts/upgrade_tools.sh (just upgrade-go)
└── No → continue
Is the tool a coding agent CLI (vendor-distributed, ships its own installer)?
├── Yes → curl-installer in coding_agents/tasks/main.yml
│ · Match existing pattern (Claude / OpenCode / Cursor)
│ · Pass --no-modify-path or equivalent (don't mutate shell rc)
│ · Add to cat_agents in scripts/upgrade_tools.sh
│ · Update AICAP SSOT (dot_config/shell/04_ai_agents.sh) +
│ four Python AGENT_CONFIG dicts (see AGENTS.md hard invariant)
└── No → continue
Is it a Yazi plugin?
├── Yes → ya pkg (Yazi's plugin manager; § 15). Lockfile
│ dot_config/yazi/package.toml (chezmoi-managed), materialized by
│ run_after_45_yazi_plugins. `ya pkg add owner/repo:plugin`,
│ copy package.toml back; upgrade via `ya pkg upgrade` (just upgrade-yazi-plugins)
└── No → continue
Is it macOS-shipped, GUI app?
├── Yes → brew cask in Brewfile.darwin.tmpl
│ · Gate on installAiDesktopApps for AI apps
└── No → continue
Is it macOS-shipped, CLI?
├── Yes → brew formula in the relevant role's tasks/main.yml
│ · Add Linux fallback (apt or GitHub release) in same role
│ · Consider whether macOS-only is OK (e.g. terminal-notifier)
└── No → continue
Is it Linux-only, distro-packaged?
├── Yes → apt / yum in the relevant role's tasks/main.yml
│ · become: true + tags: [sudo]
│ · Add user-level GitHub-release fallback for noRoot
└── No → continue
Is it Linux-only, vendor-distributed (ships its own install.sh)?
├── Yes → curl-installer in the relevant role
│ · Use creates: guard
│ · If the binary lands outside ~/.local/bin, add a PATH prepend
│ to dot_config/shell/00_exports.sh.tmpl; when it must beat later
│ mise/Bun/package-manager paths, use a later numbered shared
│ fragment (for example 08_pi_agents.sh.tmpl)
└── No → continue
GitHub release tarball (most reliable for everything else):
└── Copy the canonical pattern from devtools/tasks/main.yml
· block: + rescue: (vendor outages happen)
· retries: 3, delay: 5, timeout: 120 on get_url
· Compute arch via target_architecture
· Extract to ~/.local/bin (user) or /usr/local/bin (sudo + tags: [sudo])
· ⚠️ This binary will NOT be upgraded automatically (see § Coverage gaps)
Other things to do when adding a tool:
- If the tool ships shell completions (
<tool> completion zsh|bash), add aregenrow inscripts/generate_completions.sh(per AGENTS.md cross-file rule) - If the tool is a new in-house CLI (one of yours, in
dot_dotfiles/bin/), follow the completion decision flow indocs/zsh/zsh-completions.mdSection F (two hand-written completion files + row in section F + row indocs/shells/aliases.md) - If the tool has a notable upgrade story (self-update subcommand,
etc.), add a category or extend an existing one in
upgrades.md - If the tool replaces another tool, add the deprecated path to
.chezmoiremove - If the tool is a new alias / shell function, add a row to
docs/shells/aliases.md(per AGENTS.md cross-file rule)
Cross-references¶
Other docs that touch this territory:
upgrades.md— Upgrade-side reference (this doc is install-side)architecture.md— Repo layout + install flow phasesbootstrap.md— Bootstrap script deep diveuv-bootstrap.md— Why and how uv is specialansible_customization.md— Per-host ansible variable overridesconfig-conventions.md— Where deployed config files livetesting.md— How to verify a tool installed correctly
AGENTS.md hard invariants relevant here:
- "Install vs upgrade is split on purpose" — never
state: latest - "
with_executables_from:entries MUST also declareextra_binaries:" - "primaryShell choice gates
chshonly — both shells always deploy" - "AI agent autodetect order or AICAP_*_MODEL defaults" — edit only
04_ai_agents.sh - "Tmux ≥ 3.3 required" — see
tmuxentry in § Multi-mechanism tools
Past pitfalls related to install paths:
uv-self-update-homebrew-noop.md— why uv needs the brew-vs-curl dispatchuv-tool-install-creates-guard-misses-executables-from.md—extra_binaries:requirementansible-js-cli-tools-old-system-node.md— why npm goes through mise on Linuxansible-missing-sudo-tag.md—tags: [sudo]conventioncentos7-neovim-apt-register-defined.md— neovim user-fallback gate bugtmux-appimage-terminfo-not-found.md— tmux-appimage caveatopencode-docker-opentui-glibc-loader-missing.md— opencode container variantchezmoi-update-blesh-local-changes-overwritten.md— externals can drift when post-hook mutates the working tree
Backlog items related to install paths:
mise-runtime-gating.md— fine-grained per-host runtime gatingdevtools-role-split.md— split the mega-rolelean-bundle-init-ux.md— bundle presets + decision-tree SSOTopencodebox-wrapper.md— containerized opencode wrapper