b487b0e855
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>
219 lines
10 KiB
Markdown
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.
|