Files
dotfiles/README.md
T
noconnorandClaude Opus 5 c6e5f22acb secrets: read the 1Password vault once into a 0600 env file
Secrets were pulled in each box's hand-written ~/.zshrc.local with one
`op read` per field on EVERY new shell: every terminal waited on
1Password, every shell broke when it was unreachable, and the loop lived
outside the repo.

New secrets/ module (after bin/, which provides op and jq): one vault per
user and one item, both defaulting to the login name, whose field labels
are the variable names. It asks for the service-account token once
(hidden), fetches the item with a single `op item get`, and writes every
valid-named field to ~/.config/dotfiles/secrets.env (0600), replaced
atomically so a failed fetch keeps the previous copy. Values go through
jq @sh, so quotes, $() and newlines in a secret can't break or inject.
The token is kept in op.env (0600), which shells never source.

bootstrap loads secrets.env right after the module, so mcp-config takes
LITELLM_KEY from the env instead of prompting. A LITELLM_KEY rotated in
the vault also updates ~/.config/mcp-sync/key, since sync.sh re-exports
from that file after secrets.env loads and would otherwise win.

The managed zshrc sources secrets.env (no op call at startup) and adds
envr / envc / envd / envl / envs to read, upsert, delete, list and
refresh; they load the token only in a subshell.

Existing boxes still need the old op loop removed from ~/.zshrc.local.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:30:24 +00:00

147 lines
7.8 KiB
Markdown

# 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:
```sh
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:
```sh
./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.local` last for per-machine customizations
(see below).
- `git/` — sets git `user.name`/`user.email` and configures
`credential.helper store` (plaintext `~/.git-credentials`, 0600) for
HTTPS auth.
- `docker/` — installs full (rootful) Docker Engine from Docker's upstream
package repo (get.docker.com on Debian-family; the docker-ce repo wired up
directly on RHEL-family, since the convenience script rejects EL rebuilds
like Oracle Linux by name), enables the service (systemd hosts), and adds
the invoking user to the
`docker` group (takes effect at next login). No-ops where a rootless
Docker daemon is already running (e.g. Kasm) so it never fights that
setup. Uses sudo.
- `bin/` — installs userland devops CLIs as self-contained, statically
linked release binaries into `~/.local/bin`: jq, kubectl, helm, k9s,
kubectx/kubens, stern, yq, OpenTofu, terragrunt, task (go-task, the
Taskfile runner), the 1Password CLI `op` for 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.
No sudo, nothing outside `$HOME`, so tools persist across Kasm
redeploys with no reinstall. Idempotent: each tool records its version
under `~/.local/bin/.versions` and is skipped when the pinned version
is already present. `zsh/zshrc` puts `~/.local/bin` on `PATH`.
- `secrets/` — reads your 1Password vault once and writes every field to
`~/.config/dotfiles/secrets.env` (0600), which the managed `~/.zshrc`
sources, so opening a shell never calls `op`. 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 vault/item), and
keeps them in `~/.config/dotfiles/op.env` (0600, not loaded into shells).
Runs after `bin/` (needs `op` and `jq`); bootstrap loads the result so later
modules get the keys without prompting. Day to day: `envr NAME` reads a
field, `envc NAME [VALUE]` creates or updates one (prompts, hidden, without
VALUE), `envd NAME` deletes one; both of those refresh the file and this
shell. `envl [VAULT [ITEM]]` shows every field with its current value, and
`envs` refreshes 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 (see `mcp-config/README.md`). Run
`mcp-config/install.sh` once 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 install `python3`/`python3-venv` via
the system package manager (the one place this module uses sudo).
- `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 with
`CgroupDriver=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 by `bootstrap.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:
```sh
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 for `git.nic-oconnor.com`. Used for
API/CLI work, and by `bootstrap.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 through `git/`'s credential
helper.
- `LITELLM_KEY` — covert-gpt gateway key used by opencode. Normally you
don't set this by hand: run `mcp-config/install.sh` once and its shell
hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local`
only on a box without mcp-sync.
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token. Not a shell
variable any more: `secrets/install.sh` asks 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 in `mcp-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. See `mcp-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.yaml` into `~/.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.provider` are seeded
only on a fresh box. Unlike `mcp-config/`, this deploys at BOOTSTRAP time, not
per-shell — re-run `hermes/install.sh` to pick up a gateway.yaml change.
Depends on `mcp-config/install.sh` having run: gateway.yaml names the key
(`key_env: LITELLM_KEY`) but never contains it, and `mcp-config/sync.sh` is
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.