On an Omarchy laptop bootstrap left two of almost everything: VS Code from both pacman and the tarball (two menu entries, one shared ~/.config/Code profile), and kubectl/helm/k9s/kubectx/yq/jq/node/ansible in ~/.local/bin behind mise and pacman copies that always won on PATH. The zsh helpers (op*, grmb, secrets.env) also never reached the bash login shell. - lib/fetch.sh: add `shadowed`, true when another copy of a tool comes earlier on PATH than ~/.local/bin. Always false where zshrc puts ~/.local/bin first, so VM installs are unchanged. - bin/, node/, ansible/: skip tools that are shadowed. - vscode/: defer to any system `code`, and remove a tarball copy this module installed earlier. - bash/: new module; bash port of the zshrc helpers, sourced from ~/.bashrc via a marked block. No-ops without ~/.bashrc. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
dotfiles
Configures fresh VMs (RHEL-family or Debian-family) with a consistent
environment. Run ./bootstrap.sh (or curl-pipe it) to apply everything;
each module also runs standalone and is safe to re-run.
Quick start
On a fresh box, apply everything with one command:
curl -fsSL https://git.nic-oconnor.com/public/dotfiles/raw/branch/main/bootstrap.sh | sh
When piped like this (no local checkout), bootstrap.sh clones the repo to
~/.dotfiles and runs from there, so git must be installed first. Re-running
the command updates the checkout (git pull) and re-applies every module.
Override the destination or source with DOTFILES_DIR / DOTFILES_REPO.
Already have a clone? Just run it directly:
./bootstrap.sh
Modules
-
zsh/— installs zsh + oh-my-zsh, drops a managed~/.zshrc(robbyrussell theme, git plugin), and sets zsh as the login shell. The managed rc sources~/.zshrc.locallast for per-machine customizations (see below). -
bash/— for hosts that keep bash as the login shell (Omarchy), copies a bash port of thezsh/zshrchelpers (opr/opc/opd/opl/ops,grmb, andsecrets.envloading) to~/.config/dotfiles/dotfiles.bashand adds one marked source line to~/.bashrc. Never owns~/.bashrc; no-ops when there is none. No sudo. -
git/— sets gituser.name/user.emailand configurescredential.helper store(plaintext~/.git-credentials, 0600) for HTTPS auth. -
bin/— installs userland devops CLIs as self-contained, statically linked release binaries into~/.local/bin: jq, kubectl, kubelogin (thekubectl oidc-loginplugin, for OIDC auth to Omni clusters), helm, k9s, kubectx/kubens, stern, yq, OpenTofu, terragrunt, task (go-task, the Taskfile runner), the 1Password CLIopfor secrets storage, the opencode coding agent, zarf (DevSecOps for Offline and Airgapped Kubernetes), packer (HashiCorp's infrastructure build tool), and the coder CLI for Coder workspaces. kind is installed only when full (non-rootless) Docker is available — it doesn't work against rootless Docker, so it is skipped on Kasm. Dotfiles does not install Docker itself; install it on the host (or bake it into the image) where you want it. No sudo, nothing outside$HOME, so tools persist across Kasm redeploys with no reinstall. Idempotent: each tool records its version under~/.local/bin/.versionsand is skipped when the pinned version is already present.zsh/zshrcputs~/.local/binonPATH. A tool is also skipped when another copy comes earlier onPATHthan~/.local/bin(e.g. mise or pacman on Omarchy, where~/.local/binis last), since a copy there would never run.node/andansible/follow the same rule. On boxes where zshrc puts~/.local/binfirst, nothing changes. -
secrets/— reads your 1Password vault once and writes every field to~/.config/dotfiles/secrets.env(0600), which the managed~/.zshrcsources, so opening a shell never callsop. One vault per user and one item in it, both named after your login by default; each field's label is the variable name (GITEA_KEY,LITELLM_KEY,PROXMOX_TOKEN_SECRET, …). The first run prompts for the service-account token and checks it with 1Password straight away, re-prompting on a bad paste. It then picks the vault and item: the only one the token can see, or a prompt that lists them. It keeps all three in~/.config/dotfiles/op.env(0600, not loaded into shells). If that fails, bootstrap skips mcp-config/ and hermes/ with an error rather than asking for the LiteLLM key by hand. Runs afterbin/(needsopandjq); bootstrap loads the result so later modules get the keys without prompting. Day to day:opr NAMEreads a field,opc NAME [VALUE]creates or updates one (prompts, hidden, without VALUE),opd NAMEdeletes one; both of those refresh the file and this shell.opl [VAULT [ITEM]]shows every field with its current value, andopsrefreshes after a change made elsewhere. No sudo.opencode is configured by
mcp-config/: it points at the covert-gpt LiteLLM gateway (https://llm.covert-gpt.com/v1) for models. MCP tools do NOT come from the gateway — it hosts no MCP servers, deliberately; they run client-side under your own credentials (seemcp-config/README.md). Runmcp-config/install.shonce to store your LiteLLM key and enable config sync — without it, opencode installs but can't reach the gateway. -
ansible/— installs Ansible into a virtualenv at~/.local/opt/ansible(it's Python, not a single binary) and symlinks its CLIs into~/.local/bin. Persists across redeploys; only rebuilt when the venv is missing or broken. Building it may installpython3/python3-venvvia the system package manager (the one place this module uses sudo). -
vscode/— installs Visual Studio Code from upstream's portable tarball into~/.local/opt/vscode, symlinkscodeinto~/.local/bin, and writes a user-level.desktopentry so it shows up in the application menu. No sudo, nothing outside$HOME— the same contract asbin/, so it survives a Kasm redeploy. Pinned viaVSCODE_VERSIONin the module; bump it to upgrade. Also setspassword-store: basicin~/.vscode/argv.jsonso KDE's kwallet does not prompt for an unlock on every launch (see below). No-ops on headless hosts — it's a ~400MB GUI app — override withDOTFILES_VSCODE. Defers to a system VS Code (anycodeonPATHoutside~/.local/bin, e.g. Arch'svisual-studio-code-bin): it skips the install and removes a tarball copy it put there earlier, since two builds would show two menu entries and share one~/.config/Codeprofile. -
docker-rootless-cgroup/— on systemd-less hosts (e.g. Kasm), hand-delegates cgroup v2 controllers to the rootless Docker daemon so per-container cgroups work (rootless Docker otherwise comes up withCgroupDriver=none). Uses sudo for host cgroup writes and restarts the daemon; no-ops on systemd or non-rootless hosts. Re-run at each bootstrap (the cgroup layout resets on redeploy); its PATH shim lives in$HOME. -
mcp-config/— shared MCP gateway config sync for laptops (LiteLLM / opencode). Run bybootstrap.sh, and standalone too. The config and skills are read from the local checkout, not re-downloaded from Gitea, so this repo does not have to be public for a shell to work; see its own README.
Per-machine customizations and secrets
~/.zshrc is managed — bootstrap overwrites it, so never hand-edit it.
Per-machine settings and secrets go in ~/.zshrc.local, which the managed
rc sources last (so it can override anything) and bootstrap never touches:
cp zsh/zshrc.local.example ~/.zshrc.local && chmod 600 ~/.zshrc.local
Variables modules look for. With secrets/ set up, put these in your vault
item instead of ~/.zshrc.local:
-
GITEA_KEY— personal access token forgit.nic-oconnor.com. Used for API/CLI work, and bybootstrap.sh/mcp-config/to read this repo when it is private and there is no checkout yet. With a checkout on disk nothing here needs it; HTTPS git auth otherwise goes throughgit/'s credential helper. -
LITELLM_KEY— covert-gpt gateway key used by opencode. Normally you don't set this by hand: runmcp-config/install.shonce and its shell hook exports it from~/.config/mcp-sync/key. Set it in~/.zshrc.localonly on a box without mcp-sync. -
DOTFILES_VSCODE— controls thevscode/module. Unset (default) installs VS Code only when a graphical session is detected;1forces the install on a headless host (e.g. forcode tunnel);0skips it everywhere. A plain shell/env var, not a vault secret.On the keyring: VS Code's secret storage reaches for the OS keyring through libsecret, which on KDE makes kwalletd prompt for an unlock before the editor is usable — every session, since
kwallet-pamis not installed. The module sets"password-store": "basic"in~/.vscode/argv.jsonto stop that, which means VS Code's stored tokens are obfuscated on disk rather than sealed behind a wallet password. To use the wallet instead, installkwallet-pamso it unlocks at login, then change that value to"kwallet5". -
OP_SERVICE_ACCOUNT_TOKEN— 1Password service-account token. Not a shell variable any more:secrets/install.shasks for it once and stores it in~/.config/dotfiles/op.env. Pass it in the env only for an unattended run. -
GITEA_KEY,PROXMOX_TOKEN_ID,PROXMOX_TOKEN_SECRET— your own credentials for the client-side MCP servers inmcp-config/. opencode reads them from your environment ({env:VAR}) and passes them to the MCP server it spawns, so the tools act as you. Keep them in~/.zshrc.local; they are never written into any config file.These are NOT used by
ai-k8s-iac/task up: the LiteLLM gateway hosts no MCP servers and stores no MCP credentials. Seemcp-config/README.md. -
hermes/— installs Hermes Agent (Nous Research's self-improving AI agent) via npm. Hermes uses npx to spawn and downloads the Python hermes-agent from PyPI. Everything under $HOME, so it survives Kasm redeploys.It then merges
hermes/gateway.yamlinto~/.hermes/config.yaml, wiring Hermes to the covert-gpt LiteLLM gateway as a named custom provider. The merge is idempotent and touches only that provider entry, so the rest of your personal config is left alone;model.default/model.providerare seeded only on a fresh box. Unlikemcp-config/, this deploys at BOOTSTRAP time, not per-shell — re-runhermes/install.shto pick up a gateway.yaml change.Depends on
mcp-config/install.shhaving run: gateway.yaml names the key (key_env: LITELLM_KEY) but never contains it, andmcp-config/sync.shis what exports LITELLM_KEY into your shell. Without it Hermes 401s.
Secrets never go in this repo or in zsh/zshrc — that file is shared
config applied to every box.
lib/os-detect.sh provides shared package-manager detection (dnf/yum
vs apt); lib/fetch.sh provides arch detection and binary-download
helpers. Both are used by module install scripts.