805c407764
Documents what the code now does: git is installed by the bootstrap rather than assumed; the full path to dotup is what to type in the session that installed it, and why; brew is a script row that lazygit, omp and herdr @needs, so channel order is script-before-brew; apt resolution probes for an installation candidate, not mere existence; ^o toggles and ^t clears the query; and the header line carries ^o.
294 lines
14 KiB
Markdown
294 lines
14 KiB
Markdown
# dotfiles — public tier
|
|
|
|
Everything a machine needs to become a working machine, and nothing that makes
|
|
it *mine*. Clone it anonymously, apply it to a throwaway VM, and you get the
|
|
editor, the shell, the terminal and the tools. You do not get my name, my email,
|
|
my hosts, my agent config, or any credential — because none of that is in here.
|
|
|
|
The other half lives in a private repo you cannot clone without a password. That
|
|
split is the whole design, and it is structural rather than a policy: the gate
|
|
is *which repo a machine can obtain*, not a `.chezmoiignore` rule that could be
|
|
misconfigured once and leak.
|
|
|
|
Why the repo is shaped this way: **`DESIGN.md`**. How it got here: **`PLAN.md`**.
|
|
Both live in the working repo, not this one.
|
|
|
|
---
|
|
|
|
## Install
|
|
|
|
```sh
|
|
sh -c "$(curl -fsLS get.chezmoi.io)" -- init --apply <this repo url>
|
|
~/.local/bin/dotup
|
|
```
|
|
|
|
Two commands. The first lays down the files and clones the six externals; the
|
|
second picks and installs the software. There is no third step, and nothing
|
|
here prompts for anything.
|
|
|
|
**Type the second path in full.** Bare `dotup` will not be found in the shell
|
|
that ran the install, and that is not a bug in either command: Ubuntu's own
|
|
`~/.profile` adds `~/.local/bin` to `PATH` only if the directory already exists
|
|
*when you log in*. On a fresh box it does not, the install then creates it, and
|
|
the shell you are sitting in never re-reads `.profile`. A new login shell finds
|
|
it. Nothing a child process can do changes the `PATH` of its parent, so the
|
|
install prints the working path rather than pretending otherwise.
|
|
|
|
**git does not have to be installed first.** It used to: chezmoi is a static
|
|
download and installs fine, but all six `git-repo` externals need git, and
|
|
Ubuntu cloud images, WSL and the docker image all ship curl and no git — so the
|
|
documented first command failed on the most ordinary kind of fresh box there
|
|
is. `.chezmoiscripts/run_before_00-require-git.sh` installs it across
|
|
apt/dnf/yum/pacman/zypper/apk/brew, and explains itself where it cannot (no
|
|
sudo, or macOS, where `git` is a stub that opens a GUI installer).
|
|
|
|
For a machine with nobody at the keyboard:
|
|
|
|
```sh
|
|
dotup --unattended # safe defaults, no UI, no prompt, no private tier
|
|
dotup --print # resolve everything and print the commands, install nothing
|
|
```
|
|
|
|
## What lands where
|
|
|
|
| Path | What |
|
|
|---|---|
|
|
| `~/.zshrc` | Plain file, no template, no secrets. One guarded `source` is the entire secret surface. |
|
|
| `~/.zshenv`, `~/.p10k.zsh` | Shell environment and prompt |
|
|
| `~/.config/nvim/**` | LazyVim, 36 files. `lua/plugins/image.lua` is the fragile one — see DESIGN §3 before touching it. |
|
|
| `~/.config/{gh,ghostty,git,btop,herdr}` | Terminal and tooling config |
|
|
| `~/.tmux.conf` → `~/.tmux/.tmux.conf` | Symlink into the gpakosz external |
|
|
| `~/.gitconfig` | No `[user]`. See "the seams" below. |
|
|
| `~/.local/bin/{dotup,sysjournal}` | The installer and the journal CLI |
|
|
| `~/.local/share/dotup/packages.tsv` | The manifest `dotup` reads |
|
|
|
|
## The seams
|
|
|
|
Three files this tier *reads* and never writes. Each is absent on a public-only
|
|
machine, and each absence is a silent no-op rather than an error.
|
|
|
|
| File | Written by | Carries |
|
|
|---|---|---|
|
|
| `~/.config/git/config.local` | private tier | `[user]`, signing key, Gitea host rewrites |
|
|
| `~/.config/zsh/local.zsh` | private tier | aliases naming hosts you own |
|
|
| `~/.config/zsh/secrets.zsh` | `dotsecrets`, from bws | the seven API keys, mode 600, in no repo |
|
|
|
|
If `git commit` says it does not know who you are, the private tier has not been
|
|
applied. That is the correct complaint, not a bug.
|
|
|
|
## Externals
|
|
|
|
Six `git-repo` externals, none of which were declared anywhere before this repo
|
|
existed. That omission is why a clean bootstrap used to produce a broken login
|
|
shell: `.zshrc` sourced `$ZSH/oh-my-zsh.sh` with nothing having installed it.
|
|
|
|
`.tmux`, oh-my-zsh, powerlevel10k, zsh-autosuggestions, zsh-ai, tpm. The theme
|
|
and both plugins are nested under `.oh-my-zsh/custom/`, which is what
|
|
`$ZSH_CUSTOM` resolves to; chezmoi processes externals in path order, so the
|
|
parent is cloned first.
|
|
|
|
## dotup
|
|
|
|
One screen. Groups and packages are the same kind of row, both carry a tick box,
|
|
and every group opens into the packages underneath it. Group boxes are a
|
|
tri-state summary of their children, never an independent switch.
|
|
|
|
```
|
|
▸ [x] core 31/31 safe
|
|
▾ [~] media 2/3 safe
|
|
[x] ffmpeg apt ffmpeg
|
|
[ ] sox apt sox
|
|
[x] p7zip apt p7zip-full
|
|
▸ [ ] gpu 0/3 invasive kernel modules; a bad driver can bre…
|
|
▸ [ ] private 0/2 private one password, typed after the install…
|
|
|
|
space tick tab open ^t tick all shown ^a defaults ^x none ^o open all enter install
|
|
```
|
|
|
|
**One toggle rule, everywhere.** Expand the row to the packages it covers; if
|
|
every one is on, turn them all off, otherwise turn them all on. That single rule
|
|
covers a group row, a package row, and a bulk toggle over a filtered set — type
|
|
`nvidia`, press `^t`, and exactly the three rows you can see flip.
|
|
|
|
**The risk model.** `safe` is pre-ticked; `gui` is pre-ticked only where there
|
|
is a display; `invasive` is never ticked for you, and its reason is on the row;
|
|
`private` needs a password you type after the install finishes. A group's flag
|
|
is derived as the worst flag among its children, so a group can never look
|
|
safer than something inside it.
|
|
|
|
**Dependencies tick themselves.** `@needs` closure runs in both directions:
|
|
ticking `xrdp` ticks the `desktop` group it is useless without, and unticking
|
|
`core/node` drops `codex`, `pi`, `pi-plugins`, `mermaid-cli` and then `neovim`
|
|
— six rows including `node` itself. The closure is shown, not described: the
|
|
counts move on the same keystroke, and `^t` clears the filter so the rows it
|
|
just pulled in are on screen rather than hidden behind your query.
|
|
|
|
`^o` is a toggle, not "open all": press it again and everything closes. And on
|
|
the default collapsed screen every group row counts as "shown", so a single
|
|
`^t` there reaches all 69 packages — including the invasive and private ones.
|
|
That is the documented rule working as specified, but it is worth knowing
|
|
before you press it.
|
|
|
|
### The manifest
|
|
|
|
Five tab-separated columns, greppable by hand. Anything that would have been a
|
|
sixth column is an `@` directive instead:
|
|
|
|
```tsv
|
|
@needs core/neovim core/imagemagick core/mermaid-cli
|
|
@spec agents/codex @openai/codex
|
|
core neovim safe -tarball neovim apt ships 0.9.5 — tarball to /opt/nvim
|
|
core node safe -tarball node apt ships 18.19 and the npm rows need 20+
|
|
core fd safe fd-find fd binary is fdfind on ubuntu
|
|
```
|
|
|
|
A leading `-` in the apt or brew column means "not from this package manager":
|
|
`-tarball -npm -script -snap -deb -flatpak -uv -builtin -xcode`. A bare `-`
|
|
means unavailable there — and **Linux resolves apt-then-brew, not
|
|
apt-instead-of-brew**, because `omp`, `herdr` and `lazygit` have no apt package
|
|
at all — and `core/brew` is itself a `script` row that those three `@needs`, so
|
|
ticking `lazygit` brings Homebrew with it. On Linux that means linuxbrew, under
|
|
`/home/linuxbrew/.linuxbrew`; `/usr/local` is not touched. Channel order is a
|
|
dependency graph and not a preference: `script` runs **before** `brew`, because
|
|
otherwise brew would still be missing when the rows needing it are attempted.
|
|
|
|
The installer probes `apt-cache policy` before it batches, and asks for the
|
|
installation *candidate* rather than merely whether the name is known: on stock
|
|
Ubuntu `apt-cache show docker-ce` prints nothing and **exits 0**, so an
|
|
existence test kept it in the batch, apt refused all thirty packages at once,
|
|
and the brew fallback was never reached. One unavailable name took thirty
|
|
installable ones down with it. A name with no candidate now moves to the brew
|
|
column instead.
|
|
|
|
`@spec` carries the argument a channel needs but the package name cannot supply
|
|
— the scoped npm name, the flatpak app id, the `.deb` source. It defaults to the
|
|
package name.
|
|
|
|
### Where fzf comes from
|
|
|
|
The picker cannot get fzf from the manifest: it needs fzf to *draw* the
|
|
manifest. So it brings its own copy into `~/.cache/dotup/` and invokes it by
|
|
absolute path. **`PATH` is never modified and `~/.local/bin` is never written**
|
|
— a tool that refuses to tick `invasive` for you has no business quietly
|
|
shadowing your distro's fzf for Ctrl-R and the oh-my-zsh plugin. Your own fzf
|
|
wins whenever it clears the floor; nothing is replaced merely for being old.
|
|
|
|
Floor is 0.44.0, which is exactly where verification stops rather than where
|
|
compatibility breaks: every release from 0.29 up parses every binding used here,
|
|
but cursor-on-reload can only be *measured* from 0.44.1, where fzf's `--listen`
|
|
API began reporting state.
|
|
|
|
### The private tier
|
|
|
|
Ticking a `private` row schedules work, it does not do it — `git`, `chezmoi` and
|
|
`bws` have to exist first, and a password typed at picker time would sit in
|
|
memory through ten minutes of package downloads. So it happens at the end, and
|
|
never without a human:
|
|
|
|
```
|
|
prompt: URL · username · password the address is in no repo
|
|
│
|
|
├── blank ─▶ done. public-only machine. nothing was asked for.
|
|
├── 401 ─▶ say so, leave the machine public-only
|
|
└── 200 ─▶ two-line blob ─┬─ private repo → chezmoi init --apply
|
|
└─ bws token → ~/.config/bitwarden/bws-token (600)
|
|
```
|
|
|
|
The endpoint returns **data, never a script**: two `KEY=VALUE` lines,
|
|
`PRIVATE_REPO_URL=` and `BWS_ACCESS_TOKEN=`. Compromising it discloses two
|
|
revocable read-only credentials rather than executing arbitrary code as you on
|
|
every machine you ever build. The password is spent, never stored, and never
|
|
reaches `argv` — `curl -K -` reads it from stdin, so it never reaches `ps`
|
|
either.
|
|
|
|
The clone URL that comes back carries a read-only token inline, and it gets the
|
|
same treatment. The token is split out into
|
|
`~/.config/dotfiles/private-credentials` (mode 600, git-credential-store
|
|
format); `chezmoi init` receives a clean URL, and the helper is written into the
|
|
clone's own config so a later `chezmoi update` still authenticates. Not printing
|
|
it was never sufficient: `/proc/<pid>/cmdline` is world-readable, so passing the
|
|
credential-bearing URL to `chezmoi init` disclosed the token to every account on
|
|
the box for the length of the clone, and then persisted it in `.git/config`
|
|
until the tree was deleted. The redaction that predates this only ever covered
|
|
the trace line.
|
|
|
|
`dotup --unattended` cannot reach any of this. It has nobody to type a password,
|
|
and it refuses `private` and `invasive` rows outright even if a stale state file
|
|
ticks them.
|
|
|
|
## Tests
|
|
|
|
```sh
|
|
sh .tests/test.sh # here
|
|
sh .tests/test.sh --docker # clean ubuntu:24.04
|
|
DOTUP_TEST_IMAGE=ubuntu:22.04 sh .tests/test.sh --docker
|
|
DOTUP_TEST_IMAGE=debian:12 sh .tests/test.sh --docker
|
|
```
|
|
|
|
202 assertions covering the toggle rule, the `@needs` closure both ways, the risk
|
|
model as invariants rather than prose, match confinement (two-sided: `--exact`
|
|
confines `nvidia` to three rows **and** fuzzy still over-matches, so removing
|
|
`--exact` fails loudly), the fzf preflight, channel resolution on both
|
|
platforms, the installer, and that dotup runs at all as a non-root user with no
|
|
`sudo` on `PATH`.
|
|
|
|
The installer is driven against a directory of fake package managers that record
|
|
what they were asked to do and install nothing, so the engine is exercised end
|
|
to end without a single package landing on the machine running the suite.
|
|
|
|
The version spread is the point of the containers — this box runs fzf 0.72,
|
|
24.04 ships 0.44.1, 22.04 ships 0.29 and bookworm 0.38.
|
|
|
|
The macOS half of the manifest has no CI anywhere, so a fake `uname` stands in
|
|
for it. That is not theatre: it caught the apt/brew fallback running in both
|
|
directions, which resolved Linux-only packages to `apt install davfs2` on a
|
|
machine that has never had apt. The fallback is one-directional now.
|
|
|
|
### End to end
|
|
|
|
The suite above installs nothing. `.tests/e2e.sh` does the opposite — a stock
|
|
`ubuntu:24.04` container, the public tier fetched from the anonymous URL, then
|
|
the private tier through a real bootstrap endpoint, then `bws`, then a rendered
|
|
`secrets.zsh`.
|
|
|
|
```sh
|
|
bash .tests/e2e.sh # prompts for endpoint URL, username, password
|
|
```
|
|
|
|
The password is never written to disk and never placed on a command line: it
|
|
reaches the container as `docker exec -e BOOT_PW` with a bare name, which
|
|
inherits from the calling shell. `-e BOOT_PW=…` would put it in docker's own
|
|
argv, and `/proc/<pid>/cmdline` is world-readable — the same bug this repo was
|
|
fixed to stop committing itself.
|
|
|
|
It asserts what the unit suite structurally cannot: that a machine which has
|
|
only ever seen the public repo can authenticate, clone the private tier, and end
|
|
up with **no credential in its logs and none in `.git/config`**, with the token
|
|
in a 600-mode credential file that a later `fetch` still authenticates from.
|
|
|
|
**Without an endpoint password**, `.tests/e2e-mock-endpoint.py` stands in for
|
|
Caddy — 200 with the blob, 401 on bad credentials, and a closed connection on
|
|
any other path, which is what `handle { abort }` does and why a retired route
|
|
reports `000` rather than `404`. Everything downstream stays real. That is how
|
|
the argv fix was verified, and running it that way immediately caught a rotation
|
|
script whose route regex had stopped matching.
|
|
|
|
## Rules this repo lives by
|
|
|
|
- **Zero credentials, forever.** `gitleaks detect` runs over the full history
|
|
with no allowlist. The private repo gets exactly one allowlisted path; this one
|
|
gets none, and that asymmetry is the point.
|
|
- **Grep the files this repo owns, not `$HOME`.** A wholesale scan of a finished
|
|
machine is not a test of this repo: `~/.npm-global` ends up holding an AWS SDK
|
|
whose type definitions contain a literal `AKIA…` and an Anthropic SDK whose
|
|
docs contain `ghp_…`. Scope the scan to `chezmoi managed`, and tighten `sk-` to
|
|
`sk-[A-Za-z0-9_-]{20,}` — the bare substring matches `ask-`, `task-`, `risk-`
|
|
and `disk-`, and one of this manifest's own npm packages is called
|
|
`rpiv-ask-user-question`.
|
|
- **No identity.** No name, email, hostname, private IP or absolute home path.
|
|
If something needs one, it belongs behind a seam.
|
|
- **`.chezmoiignore` is gitignore syntax.** A `#` mid-line becomes part of the
|
|
pattern and silently matches nothing. Every comment goes on its own line.
|
|
- **Nothing invasive without an action of yours.** Not "never", since a `@needs`
|
|
edge can pull one in — but never silently, and never unattended.
|