Files
dotfiles-public/README.md
T
bcherb2 b487b0e855 feat: public dotfiles tier — no credential, no identity, one installer
Fresh history. This is the repo a throwaway VM clones anonymously: it brings a
machine to a working baseline and carries nothing that makes it mine.

56 files. 50 land in $HOME, 3 are chezmoi metadata, 2 are repo documentation,
1 is the manifest, and a 15-file test harness stays behind in .tests/.

What did not travel, and why:

  encrypted_private_bws-token.age   a real credential; age is dropped entirely
  .chezmoidata/bws.toml             env-var -> secret-id map; belongs with the
                                    tier that can use it
  SECRETS.md                        documentation of the rules, not config
  finish-setup.sh.tmpl              superseded by dotup
  nvim/init.lua.backup              dead file
  dot_claude/**, dot_codex/**,      120 files of agent config, private tier
  dot_pi/**

De-identified rather than dropped:

  .gitconfig   [user], the GitHub ssh rewrite and both Gitea host rewrites are
               identity, not configuration. They move behind an [include] of
               ~/.config/git/config.local, which the private tier writes. Git
               treats a missing include as a no-op, so a public-only machine
               reads the file and stops.
  .zshrc       the two gitea aliases carried a personal domain and a LAN IP.
               They move behind a guarded source of ~/.config/zsh/local.zsh,
               the sibling of the secrets.zsh seam phase 2 established.
  nvim         a commented-out LM Studio endpoint naming a LAN address.
  ghostty      a stale auto-generated header naming an absolute home directory.

Newly captured, never tracked before: ~/.zshenv, ~/.config/gh/config.yml. The
former sourced ~/.cargo/env unguarded, so every zsh on a machine without rustup
printed an error -- the same shape as the unguarded oh-my-zsh source phase 2
fixed. It is guarded now.

.chezmoiexternal.toml grows from one entry to six. oh-my-zsh, powerlevel10k,
zsh-autosuggestions, zsh-ai and tpm were hand-installed and declared nowhere,
which is why `chezmoi init --apply` on a clean box produced a .zshrc that broke
the shell it configures. The theme and both plugins nest under
.oh-my-zsh/custom/, which is what $ZSH_CUSTOM resolves to.

dotup gains an install engine. It resolves each selected package to a channel
(apt, brew, npm, uv, snap, deb, flatpak, tarball, script, builtin) through one
function every consumer reads, probes apt-cache before batching so a name apt
does not know moves to brew instead of failing all thirty, and retries
individually if a batch still fails -- which earned its keep on the first real
container run, where mermaid-cli's puppeteer dependency failed and the other
twelve npm packages installed anyway. --unattended computes safe defaults fresh
from the manifest rather than inheriting a state file, and refuses private and
invasive rows outright even when a stale state file ticks them.

The manifest gains @spec, a second directive kind alongside @needs, carrying the
argument a channel needs but a package name cannot supply -- the scoped npm
name, the flatpak app id, the .deb source. The TSV stays five columns wide.

Three bugs the container runs found, all fixed here:

  * `apt install nodejs` gives you node WITHOUT npm on Ubuntu, so all thirteen
    npm packages failed on a fresh box. The manifest asks apt for both names.
  * A tool installed a moment ago is not on this process's PATH -- uv lands in
    ~/.local/bin, npm -g honours the ~/.npmrc prefix, linuxbrew is outside a
    non-login PATH. Resolved by looking in the places we just wrote to, never by
    exporting a modified PATH.
  * `A || { B && C; }` is one || list, so when `command -v sudo` failed the list
    failed and `set -e` killed dotup at load. On a non-root machine with no
    sudo it died before printing anything. There is a regression test.

.zshenv and .p10k.zsh are marked private_. Both are shell code the login shell
executes and both applied at 664, group-writable. Third occurrence of the class
of bug phase 1 found on .pi/agent/auth.json and phase 2 found on .zshrc; the
first one found on purpose rather than by accident.

Verification: 81 assertions, 81/81 on this box and in ubuntu:24.04, ubuntu:22.04
and debian:12. 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 package landing on the test machine.
`gitleaks detect` over the full history and the working tree: no leaks found,
with no allowlist and no .gitleaks.toml.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-17 00:24:41 -04:00

219 lines
10 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.
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 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`, `mermaid-cli` and then `neovim`. The closure is
shown, not described — the counts move on the same keystroke.
### 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 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. The installer goes further and probes `apt-cache` before it batches: a
name apt does not know moves to the brew column rather than failing the whole
batch.
`@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.
`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
```
81 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.
## 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.