# 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 ~/.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. 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//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 ``` 101 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//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.