One repository for shared Vim/Neovim, Zsh, Starship, tmux, and SSH
configuration.
Machine- and organisation-specific shell settings stay in
~/.config/zsh/local.zsh, outside Git.
Git follows the same pattern: shared defaults live in git/config, while
credentials and host-specific overrides stay in ~/.config/git/local.gitconfig.
SSH defaults live in ssh/config; private hosts and per-machine overrides stay
in ~/.ssh/config.local.
git clone git@github.com:freeyoung/dotfiles.git ~/dotfiles
bash ~/dotfiles/install
exec zshinstall is idempotent and networked by default. It installs the shared
command-line dependencies, creates symlinks, restores shell/Vim plugins, and
backs up any conflicting target under ~/.dotfiles-backups/<timestamp>/.
Use bash ~/dotfiles/install --links-only when only links and local
configuration should be refreshed, or --skip-plugins when dependencies are
welcome but plugin downloads should wait.
The script links ~/.vim to vim/ for compatibility with existing Vim
tooling. Plugin files are shared under ~/.local/share/vim, while persistent
state is separate under ~/.local/state/vim and ~/.local/state/nvim (or
their XDG equivalents).
On macOS, the installer installs Homebrew when necessary and applies the
shared Brewfile. A Linux host that already has Linuxbrew takes
the same path.
Without Homebrew, a Linux host is bootstrapped from its own package manager.
Arch (pacman) and Debian with its derivatives (apt) are mapped; other
distributions still ask for Linuxbrew. Either native path also installs zsh,
which macOS provides as the login shell and neither distribution installs by
default, and clones antidote into
~/.antidote, which neither packages. Node is deliberately not installed:
mise provides the versions projects pin.
On Arch, nothing comes from the AUR, so no AUR helper becomes a bootstrap
dependency. Where mise is packaged under another name — Omarchy and the AUR
ship mise-bin, which conflicts with the official mise — the installer
detects it through pacman -T and leaves the existing package alone.
On Debian the package names differ: GNU sed is sed rather than gnu-sed, Go
is golang-go, and bat installs its binary as batcat, because another
package already claims the name. The installer links ~/.local/bin/bat to it
— an alias would not do, since MANPAGER and the ff preview name bat in
commands that man and fzf run outside an interactive shell. A host that has
bat under its own name gets no link.
Ubuntu takes the same path: the dispatch turns on apt-get existing, not on
which distribution it is. What differs is only what the archive carries, and
that is measured rather than assumed — Debian 13 has the whole list, Ubuntu
24.04 is missing starship and kubectl, and Ubuntu 26.04 is missing only
kubectl. Both of those are guarded in the shell configuration, so an Ubuntu
host falls back to Zsh's own prompt and to a k alias with nothing behind it
rather than failing to start.
That is the general rule: what the archive does not have is named rather than
silently dropped, and the rest still installs. apt-get update runs only when
something is actually missing, since dpkg answers the already-installed
question offline — which is also why the three below are left out of the list
asked of apt rather than listed and reported missing every time. On a host
that is already set up, the whole apt step is offline and silent.
Three are not packaged at all. mise and uv are load-bearing here — every
runtime version comes from mise, and mise builds project virtualenvs through
uv — so they are installed from their own upstream installers, which write a
single binary into ~/.local/bin, need no root, and leave no apt source on
the host to maintain. ~/.zshrc puts that directory on PATH before the
modules load, ahead of the guards that ask whether mise and bat exist.
ouch is not load-bearing: only the x helper reaches for it, and that falls
back to tar, unzip, and the single-file decompressors without it. The
installer names it once and carries on.
Either way the installer verifies the GNU userland this configuration relies
on and stops if it is incomplete. macOS needs Homebrew's gls, ggrep,
gsed, and gawk because its own userland is BSD; on Linux the unprefixed
tools are accepted once their --version output confirms they are GNU builds
rather than BSD or busybox ones. The installer never changes the login shell.
Copy zsh/local.zsh.example to ~/.config/zsh/local.zsh to add private
machine settings. The installer does this once automatically and preserves it
on later runs.
Shared Zsh settings are loaded in a deliberate order from zsh/modules/:
environment, runtimes, prompt, commands, completion, Node/tool integrations,
plugins, then interactive bindings. zsh/zshrc is only the interactive guard
and loader; keep machine-specific settings in ~/.config/zsh/local.zsh.
mise resolves every pinned runtime version, replacing
the per-language managers this configuration used before (pyenv,
pyenv-virtualenv, and fnm). mise/config.toml is linked to
~/.config/mise/config.toml and lists the tools every host gets; a
repository's own mise.toml takes precedence over it. One mise activate in
zsh/modules/runtimes.zsh covers all of them
through a single precmd hook, so the deferred-initialisation machinery those
tools needed — pyenv's own hook cost roughly 80 ms per shell — is gone.
mise reads its own mise.toml by default and ignores .python-version,
.nvmrc, and similar files unless the owning tools are named in
MISE_IDIOMATIC_VERSION_FILE_ENABLE_TOOLS; runtimes.zsh sets it to
python,node.
A project virtualenv replaces pyenv-virtualenv and is declared in the
project's mise.toml:
[tools]
python = "3.13"
[env]
_.python.venv = { path = ".venv", create = true }mise creates the virtualenv on first entry and exports VIRTUAL_ENV and
PATH, leaving PROMPT untouched. With uv installed, setting
python.uv_venv_auto routes creation through it; without uv, mise falls back
to the standard library's venv silently.
Note that a .python-version naming a pyenv virtualenv rather than a
version — pyenv-virtualenv accepts both — has no mise equivalent. Replace those
files with a mise.toml; mise otherwise treats the name as a version string
and reports it as uninstalled.
On first run, an existing ~/.gitconfig is moved to
~/.config/git/local.gitconfig, then replaced with a link to the shared Git
defaults. This keeps credential helpers and private URL rewrites out of Git.
git/ignore is linked to ~/.config/git/ignore, which Git reads
with no core.excludesFile pointing at it. It holds only what is never worth
committing anywhere — Claude Code's .claude/, and the .DS_Store files that
arrive in repositories synced from a Mac. A pattern specific to one project
belongs in that project's .gitignore, where its other contributors can see
it.
Tools that write to ~/.gitconfig directly do not know it is a link into this
repository, so their changes land in the shared file. gh auth login is the
one to watch: it writes a credential.helper naming the absolute path of the
gh binary it was run from, which is both machine-specific and wrong on any
other host. Check git -C ~/dotfiles status after running it, and move
anything it added to ~/.config/git/local.gitconfig. Authenticating through
GH_TOKEN instead avoids the problem entirely.
The installer links ssh/config to ~/.ssh/config and keeps
machine-specific settings in ~/.ssh/config.local. On first installation, an
existing SSH config is copied there before the original is backed up; otherwise
it is created from ssh/config.local.example. The local file is loaded before
the shared Host * defaults and preserved on later runs. Installer-managed
regular files are kept at mode 600; an existing symlink remains under the
user's control. Missing local include paths are ignored by OpenSSH.
Run the non-interactive Vim smoke test after installation:
bash ~/dotfiles/scripts/check-vim-config.shVim plugin revisions are recorded in vim/plugins.lock.vim,
which the default installer restores after downloading vim-plug. After
updating plugins intentionally, regenerate the snapshot from the repository
directory with:
:PlugSnapshot! ~/.vim/plugins.lock.vimCommit the resulting change. Restoring the snapshot accesses the network and checks out the pinned revisions.
Both editors share one configuration: ~/.vimrc and ~/.config/nvim/init.vim
(the latter symlinked by install to vim/nvim_init.vim) both
source vim/vimrc. nvim_init.vim extends runtimepath to the
Vim configuration directory first,
since Neovim's default runtimepath doesn't include ~/.vim. A handful of
has('nvim') branches account for real differences between the two (Neovim
dropped the pastetoggle option, so paste toggling is an explicit
<F12> mapping instead; some vim-airline extensions auto-enable under
Neovim regardless of whether their backing library is present and are
explicitly disabled in vim/config/plugins.vim). Neovim is optional — install
links its config the same way regardless of whether nvim is installed.
The configuration is split into vim/config/ by responsibility: options, file
types, plugin settings, LSP, mappings, autocmds, and terminal settings.
freeyoung/vim-tomorrow-theme
(Tomorrow-Night-Bright) is the active default, a fork of
chriskempson/vim-tomorrow-theme
with fixes upstream never made:
- The color-setting logic was gated only on the legacy
&t_Co, which Vim and Neovim can report differently, so Vim silently keptNormalunset while Neovim applied the theme correctly. The fork also checks&termguicolors, so both editors render identically (verified by diffing:highlightoutput directly) without a globalset t_Co=256workaround. - The original shipped no airline theme at all, falling back to
vim-airline-themes' generictomorrow.vim, which — like most community-contributed airline themes — has no dedicated commandline-mode palette and silently reuses Normal's colors. The fork addsautoload/airline/themes/tomorrow_bright.vimwith full mode coverage, including commandline (orange, not Replace's red — less alarming for routine:commands), using the same hex values as the colorscheme itself. - The colorscheme's own
StatusLine/StatusLineNCused a"reverse"attribute (a 2013-era convention predating airline-style per-segment statuslines). Neovim's TUI leaks that into the entire statusline row as unconditional reverse video — including airline's own segment colors and the separator colors airline computes by reading back already-applied group colors — which Vim does not replicate. sonokai's colorscheme, by contrast, never puts"reverse"onStatusLineat all, which is why sonokai-based setups never hit this. Fixed at the source (dropped"reverse", colors swapped to preserve the exact original look without depending on it) and defended invim/config/plugins.vim(has('nvim')only: clearsStatusLine/StatusLineNC's reverse attribute on everyColorSchemeevent, in case some other theme has the same issue). Verified via raw PTY capture, not just:highlightintrospection — the highlight definitions looked correct in isolation even when the actual rendering wasn't.
:colorscheme Tomorrow-Night-Bright | :AirlineTheme tomorrow_brightSeveral other colorschemes (sonokai, onedark.vim, edge, everforest,
gruvbox-material, catppuccin, papercolor-theme, ayu-vim, base16-tomorrow-night
via tinted-vim) were evaluated and rejected along the way — mainly for
missing airline commandline coverage, being Neovim/Lua-only (breaks the
shared-config setup), or, for base16, having Identifier equal Normal
(no color distinction for YAML/Ansible keys). See git history on
vim/config/plugins.vim and vim/plugs.vim for the full trail.
Beyond the aliases in zsh/modules/commands.zsh,
a few helpers are worth calling out. Several were adapted from
Omarchy, whose Bash configuration is where the ideas
came from; they are reimplemented here in Zsh and without its machine-specific
dependencies.
ssh is wrapped. A remote tmux, pager, or editor arms terminal modes over the
connection — mouse tracking, focus reporting, the alternate screen — that only
it can disarm. When the connection dies instead of exiting cleanly those modes
stay armed locally, and every mouse move then floods the prompt with escape
junk. The wrapper always disarms them, and reconnects when an interactive
session drops: exit status 255, a session that lasted at least 30 seconds, a
terminal on stdin, and no remote command. That last condition is checked
against ssh -G, so a RemoteCommand from ssh_config cannot have its side
effects replayed either. Ctrl-C stops the retry loop.
fip host port... forwards ports over SSH in the background, dip port...
stops them, and lip lists what is currently forwarded.
gwa branch creates a Git worktree as a sibling directory named
<repo>--<branch> and moves into it; gwr removes the worktree and its branch
from inside one, recovering the branch name from that directory name and
refusing to run anywhere else. They are not called ga and gd — gd is
already the abbreviation for git diff.
man renders through bat when it is installed, which colours the synopsis
and options.
x archive... extracts. It hands the work to ouch, which knows every format
from one command; where ouch is absent — Debian packages it nowhere — it
falls back to tar, unzip, and the single-file decompressors, chosen by
extension. GNU tar sniffs the compression itself, so the whole .tar.*
family is one branch. Anything neither can place is named and the rest of the
arguments still run.
ff picks a file with fzf and previews it with bat on the way — inline images
too, under Kitty. eff opens what it picked in $EDITOR, and sff dest:/path
copies it to a remote host, newest files first.
ls is rendered by eza, which the installer installs on every platform —
colour-coded permission bits, Git status, human sizes — but it keeps GNU
ls's flags, because the same hands type ls on servers that will never have
eza, and a half-learned habit is worse than none. Where the two disagree the
GNU meaning wins: -t and -S sort (eza reads -t as which timestamp to
show, and sorts ascending where ls puts newest and largest first), -F
appends type indicators, and -a lists . and .. as eza's -aa does. -r
reverses whatever sort is in effect, so it composes with the rest — -lt,
-ltr, and -lrt all order exactly as GNU ls does. The guard stays
regardless: on a host bootstrapped with --links-only, or a release that does
not package eza, ls is GNU ls and the e aliases below are not defined.
One difference remains: eza collates byte-wise, so dotfiles sort ahead of everything instead of under their letter. eza has no collation setting, and no habit depends on it.
e, ea, et, and eta reach for eza's own flags directly, in long and
tree form, each with and without dotfiles.
tdl <command> [second] builds a tmux dev layout — editor left, the command
on the right, a shell along the bottom — tdlm opens one such window per
subdirectory, and tsl <n> <command> tiles the same command across n panes.
hdl, hdlm, and hsl are the same three layouts under
herdr. All take the command as an argument
rather than hard-coding a particular editor or agent.
Every command in this group carries the guard that makes it safe on both
platforms. open and iso2sd are defined only on Linux: macOS has its own
open(1), a full utility this one-line xdg-open wrapper would shadow, and
writing an image to a block device has no macOS counterpart.
fontconfig/fonts.conf is linked to
~/.config/fontconfig/fonts.conf on Linux. macOS renders through Core Text,
so even where a Homebrew package pulls fontconfig in these rules would name
faces that host does not have; linking there would create the directory and
stand ready to displace a fonts.conf written for something else, for no
effect. Noto Sans CJK ships SC, TC, JP, KR and HK
under one family name, and left to itself fontconfig picks between them by
whatever the ordering leaves first — which is how Chinese ends up rendered in
Japanese glyph forms. These rules put the Simplified Chinese faces first for
Chinese, and each region's own face first for Japanese, Korean and Traditional
Chinese.
Chromium and Electron resolve a missing glyph one character at a time, on a pattern carrying neither a family nor a language, so language rules never fire for them. A weakly bound last-resort family covers that path; all three SC faces are named, because a monospace pattern carries a spacing requirement the proportional face cannot satisfy.
Everything here adds to the generic families rather than replacing them, so a
distribution that assigns its own faces to sans-serif and the rest keeps
them.
Fonts that exist only to cover the CJK extension blocks — BabelStone Han, HanaMin — claim enough of Unicode besides that fontconfig will otherwise hand them ordinary text and even emoji. The language they declare is reassigned so they stay what they are for: a last resort for a codepoint nothing else has.
No font is installed on any platform: these rules and kitty.conf select
faces, they do not provide them. Omarchy already ships the Noto CJK families
and a Nerd Font, which is why nothing here has had to. A Debian desktop does
not, and a rule that names a face the host lacks simply never fires — install
fonts-noto-cjk and a JetBrains Mono Nerd Font build there to give the
selection something to choose from.
tmux.conf is linked to ~/.tmux.conf. It enables true color,
mouse support, large scrollback, and system clipboard integration. Splits
preserve the current directory; h/j/k/l navigate panes and prefix + r
reloads the configuration. Ctrl-B stays the prefix, because that is what an
unconfigured tmux on a server answers to; Ctrl-Space is a secondary prefix.
tmux applies every configuration file it finds rather than stopping at the
first, in the order /etc/tmux.conf, ~/.tmux.conf,
$XDG_CONFIG_HOME/tmux/tmux.conf. A file at the XDG path therefore does not
supplement this one -- it overrides every setting the two have in common.
Omarchy installs one, so the installer moves it into the backup directory.
What was worth keeping from it is here instead: the no-prefix Alt layer
(Alt+Enter to split, Alt+1..Alt+9 for windows, Ctrl+Alt+arrows for
panes), the uppercase session controls, prefix + ? for the searchable
keybinding popup, and a note on every binding so list-keys -N reads as
documentation.
The status line names colors instead of giving hex values, so it follows whatever palette the terminal is themed with -- Omarchy's theme switcher repaints it for free, and a 16-color terminal on a server still renders it. It shows the session on the left, and copy/prefix/zoom flags, host, date, and time on the right, refreshed every second.
- Files over 5MB (
g:vim_large_file_bytesinvim/config/autocmds.vim) skip undo history and syntax highlighting on open. synmaxcol=500caps syntax scanning on very long lines (minified JS, data dumps).NERDTreeand the filetype-specific syntax plugins (vim-markdown,vim-javascript,html5.vim,vim-yaml,ansible-vim) are lazy-loaded via vim-plug'son/for, not sourced at startup unless actually used.- LSP diagnostics keep their signs and highlights quiet during Insert mode, then refresh when you return to Normal mode.
%-matching comes from Vim's bundledmatchitpackage (packadd!invim/config/filetypes.vim) instead of a vim-plug-managed checkout.
The leader is explicitly set to \.
| Keys | Mode | Action |
|---|---|---|
F1, \w |
Normal / Insert | Save the current file |
F2, Ctrl-Q |
Normal / Insert | Quit the current window |
F3 |
Normal / Insert | Save and quit |
F5 |
Normal | Toggle NERDTree |
F12 |
Normal / Insert | Toggle paste mode |
Ctrl-H/J/K/L |
Normal | Move between split windows |
Shift-H/L |
Normal | Previous / next tab |
\+, \-, \_, \=, \[, \] |
Normal | Resize / equalize splits |
gd, gD, gr, gi, K |
Normal | LSP definition, declaration, references, implementation, hover |
\rn, \ca, \f |
Normal | LSP rename / code action / manual format |
Tab, Shift-Tab, Enter, Ctrl-Space |
Insert | Select, accept, or manually trigger LSP completion |
:w!! |
Command-line | Write the current buffer through sudo |
j and k move by screen line when wrapping is enabled. vim/config/keymaps.vim
contains general mappings; vim/config/lsp.vim contains LSP and formatting mappings.
\f formats Python with Ruff and formats other supported buffers through LSP. It
never formats automatically on save.
NERDTree remains available through F5 as a directory-oriented view.
termguicolors is enabled when supported.
vim-lsp supplies diagnostics, navigation, completion, code actions, and
formatting. The configured servers are Pyright and Ruff (Python), gopls (Go),
TypeScript Language Server (JavaScript/TypeScript), YAML Language Server,
vscode-json-language-server (JSON/JSONC), and
ansible-language-server. Language servers are deliberately not bootstrap
dependencies: when a catalogued server is missing, opening its file type
suggests :LspInstallServer; run that command to download it. Ruff is used for
both Python diagnostics and formatting (uv tool install ruff@latest).
ansible-language-server is not in vim-lsp-settings' catalog. Opening an
Ansible buffer without it shows the corresponding on-demand command:
npm install -g @ansible/ansible-language-server.
ansible-vim detects Ansible YAML by path (tasks/, roles/, handlers/,
group_vars/, host_vars/) and by filename (playbook.yml, site.yml,
main.yml, etc.); a supplemental rule in vim/config/filetypes.vim also covers a
generic playbooks/ directory with arbitrary filenames. It sets
filetype=yaml.ansible under Vim and plain filetype=ansible under Neovim
(ansible-vim's own upstream difference); both are handled everywhere they
matter (LSP allowlist, plugin lazy-loading).
Completion functions live in zsh/completions/, which
completion.zsh puts on fpath. zsh autoloads each only when something asks
to complete the command it names, so an entry costs nothing at startup and
needs no guard for a command the host may not have.
_omarchy completes Omarchy's dispatcher, which
Omarchy itself ships only for Bash. omarchy a b runs the omarchy-a-b
executable, so the command tree is read out of the names in its bin directory,
and the values after the last subcommand come from the # omarchy:args=
header each executable carries.
Completion lists take their filename colours from LS_COLORS, which
completion.zsh fills in through dircolors — nothing on Arch sets it
otherwise, and eza reads the same variable.
Three things here only make sense on a Linux desktop, so the installer guards each on what actually reads it rather than on the platform alone.
This is the part of the repository that stays Omarchy-shaped. Hyprland's Lua
configuration and the override files named below are Omarchy's arrangement,
not upstream Hyprland's, and the tray plugin is a clone of one of its widgets;
the guards link them wherever a Hyprland binary exists, but only Omarchy's
loader reads them. kitty.conf is the exception that had to be portable —
it pulls the current Omarchy theme in with globinclude rather than
include, because a glob that matches nothing is silently skipped where a
missing named include is a startup error in every kitty window. Off Omarchy it
keeps kitty's own colours. Nothing here has a Debian equivalent to write:
install on a Debian host that is not running Hyprland links none of it.
These raise the cost of a branch switch inside this repository. Everything is
linked rather than copied, so a checkout that does not contain one of these
files leaves a dangling link where the live configuration used to be -- and
unlike a shell or editor file, that takes the keyboard layout, the touchpad,
and the input method down with it, on the desktop being used at the time.
Hyprland reports it as hypr.input not found. Keep the working tree on a
branch that has these files, or checkout them back afterwards:
git -C ~/dotfiles checkout master -- hypr fcitx5 mise xcomposehypr/input.lua and
hypr/looknfeel.lua are linked into ~/.config/hypr/
where a Hyprland binary exists. Omarchy loads these two after its own defaults
and after the theme, and says so in the hyprland.lua it ships -- they are the
files it sets aside for personal overrides, so it can keep improving its
defaults without this repository having to track them. input.lua maps
CapsLock to Escape, selects the mac variant so right Alt is Option and the
German umlauts sit where macOS puts them, moves Compose to right Ctrl (right
Alt being spoken for), and turns on natural scrolling and three-finger drag.
looknfeel.lua carries the border and gap settings ported from the old
hyprland.conf.
fcitx5/wubi-large.conf is linked to
~/.config/fcitx5/table/wubi-large.conf where fcitx5 exists -- table/, not
inputmethod/, which registers the input method rather than configures it. It
sets four values and leaves the rest to fcitx5's defaults: a four-code
character that is the only match still waits for the space bar, a fifth
keystroke commits what is pending, and a phrase is learned after three uses
instead of ten. Changing any setting through fcitx5's configuration tool
rewrites the file in full -- the symlink survives, but every default is written
out explicitly and the comments are lost, so trim it back afterwards.
xcompose is linked to ~/.XCompose on Linux. It includes the
locale's own table, adds identification sequences, and vendors Omarchy's emoji
shortcuts so they work on a host without Omarchy.
Several of the commands above came from
Omarchy's own Bash configuration, which keeps growing.
omarchy/ledger.tsv records the verdict on every alias,
function and export it defines — ported, renamed, or skipped with the reason —
and scripts/omarchy-review.sh subtracts that
ledger from what Omarchy currently ships:
bash ~/dotfiles/scripts/omarchy-review.shRun it after omarchy update. The output is only ever what is new, with the
line that defines it; add a row for each and the next run is quiet. The script
exits cleanly on a host without Omarchy.
It checks the other direction too. Omarchy ships user configuration files, and
four of them this repository either owns or has to keep out of the way:
hypr/input.lua, hypr/looknfeel.lua, starship.toml, and tmux/tmux.conf,
which must not exist at all since tmux reads it after ~/.tmux.conf.
omarchy-refresh-config copies Omarchy's version over ~/.config/<path> with
cp -f, and the Omarchy menu reaches it through omarchy-refresh-hyprland and
omarchy-refresh-tmux, which name exactly those files -- so this is one click
away rather than hypothetical.
cp -f writes through a symlink rather than replacing it, so a refresh
overwrites the contents of a file in this repository, where git status will
show it, instead of quietly detaching the link. The review reports any of the
four that reappeared, stopped being a link, or came back byte-identical to
Omarchy's default, and names the .bak.<epoch> file Omarchy left behind. That
is a regression rather than news, so it exits 1; new shell definitions on their
own exit 0.
.github/workflows/checks.yml runs
scripts/check-bootstrap.sh on every push and
pull request, on both Ubuntu and macOS, since much of what the installer does
turns on which of the two it is running on. Every check inside the bootstrap is
guarded on its tool existing, so the workflow installs zsh, vim, tmux, and Lua
first -- an absent tool is not a passing check, it is a check that never ran.
The Ubuntu job tests the linking half only. It runs install --links-only,
which skips
scripts/install-dependencies.sh
entirely. What it proves is the linking half on a Linux that is not this one:
the platform guards, the SSH sanitiser, and the tmux, Lua, Zsh, and Vim
configuration parsing under older tools and no installed plugins. Both of
those last two caught a real bug the first time it ran.
A second job, bootstrap-apt, runs the mapping itself. It cannot run on the
runner directly -- that image carries Linuxbrew, which the installer prefers
over apt -- so it runs in a container instead, on debian:trixie and
ubuntu:24.04, and installs real packages with install --skip-plugins. Two
archives rather than one because they answer differently: Debian carries the
whole list, while Ubuntu 24.04 is missing starship and kubectl and has to
report them and carry on, which is the branch that would otherwise never be
executed anywhere. The job then checks what is unique to this path -- mise
landing in ~/.local/bin, bat linked to batcat, eza present -- and starts
an interactive Zsh, which is where a missing guard for a tool the archive did
not supply would surface.
Those images have no sudo, which is why install-dependencies.sh escalates
through a helper that runs the command directly when it is already root. That
is not only a CI convenience: a root shell on a minimal install or a rescue
boot has no sudo either.
The pacman mapping has no such job. Arch publishes no image the runners can
use as readily, and it is the one this repository's own host exercises on
every update.
Two guards are invisible to it either way: Hyprland and fcitx5 exist on
neither runner, so nothing links or checks those files except the host that
has them.