bcherb2 e1332c67d8 feat: carry CLAUDE.md and AGENTS.md in the public tier
Phase 3 excluded all of dot_claude/, but the plan always intended CLAUDE.md
to be the one agent file that ships publicly. Adds it plus the codex and pi
AGENTS.md, so a throwaway VM gets sane agent behaviour with no credential.

Specialized agents, skills, hooks and the language rule sets are deliberately
absent -- they live in a separate repository now. The Active Rule Sets section
is rewritten to say so rather than dangle a reference to a rules/ tree this
repo does not carry.

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

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 -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:

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:

@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 .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.
S
Description
No description provided
Readme 902 KiB
Languages
Shell 73.3%
Lua 15.5%
Go Template 8.1%
Python 2.7%
Dockerfile 0.4%