Enumerates the forty-seven prompts the two scripts actually ask and sorts them into branches, consents and values — the distinction that decides what a generated leaf can remove. Four real branches (OS, role, tailnet state, which half), fourteen consents that let a leaf omit a section entirely, and a set of values that must stay prompts because baking them in would mean publishing somebody's hostname. Names the two things that need deciding rather than deciding them: Whether a leaf strips dead code or sets constants and calls the base. They are different artifacts and the plan rests on which one is meant — the first is what makes it auditable by being short, the second is what keeps it maintainable. And the combinatorics: 4 OS x 3 roles x 3 tailnet states is 36 leaves before consents, so the tree cannot be the full product. Publishing a few opinionated leaves keeps the static-file-anyone-can-diff property; generating on demand does not, which is the property per-leaf scripts existed for. Also notes that --unattended and a generated leaf are the same mechanism seen twice, and that install_config's existing behaviour — keep the user's file when there is no tty — is the conservatism every unattended answer needs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
4.7 KiB
The install page, and the scripts behind it
Status: for discussion, 2026-08-13. Nothing here is built. It exists so tomorrow's conversation is about real branches rather than sketched ones — every question below is one the scripts already ask today.
The shape agreed
- One source — the interactive scripts as they are.
- A build script that compiles them into single files, because
curl | bashcannot fetch libs. - The build emits one script per leaf of the question tree, not one script with pre-seeded answers. A person auditing before running reads only their own path.
- Verification is of the generator, once: anyone regenerates the leaves from source and diffs them against what is published. One thing to trust rather than N.
The questions that actually exist
Forty-seven prompts across the two scripts. Almost none of them should become a branch — the distinction that matters is:
A branch changes which code runs. Removing it makes a script genuinely shorter.
A value changes a string. Removing it makes a script no shorter — it just moves the answer from a prompt to a constant.
A consent is a yes/no about doing a step at all. These are the interesting middle: pre-answering one lets the build delete the section entirely.
Branches — these change what code exists
| question | answers | what it eliminates |
|---|---|---|
| operating system | macOS · Debian/Ubuntu · Arch · Fedora | 17 of 26 machine-setup sections on macOS; the whole case $PM ladder collapses to one arm |
| machine role | homelab · vps · dev | swap, ballast, earlyoom, sleep/suspend, boot-hang, static addressing — each is role-gated today |
| tailnet | already connected · set one up · none | the entire Tailscale section, its four sub-options and the offscale explanation |
| which half | machine + officer · officer only · machine only | one of the two scripts disappears |
Consents — pre-answering deletes a section
Docker · fail2ban · unattended-upgrades · Neovim · agent CLIs · shell config · firewall · SSH hardening · DNS · swap · ballast · earlyoom · inotify · boot-on-start.
Fourteen sections that a leaf script can simply not contain.
Values — never a branch
Username · install path · git name and email · port · public URL · Postgres connection · timezone · locale · LAN CIDR · swap size · swappiness.
These stay as prompts even in a generated script, or arrive as environment variables. Baking them into a published file would mean publishing somebody's hostname.
Where this collides with --unattended
--unattended and a generated leaf are the same mechanism seen twice: both are "answer these in
advance". The difference is only whether the answer is baked in at build time or supplied at run
time.
Worth deciding tomorrow whether a leaf script is literally base.sh --unattended with a header of
constants, or whether the build truly strips the dead branches. The second is what makes it
auditable-by-being-short; the first is what makes it maintainable. They are not the same artifact,
and the whole plan rests on which one we mean.
One thing that already exists and should be preserved either way: with no tty, install_config
keeps the user's file rather than replacing it. Every unattended answer needs to be conservative in
that same way, and that is a property of each prompt, not of the flag.
The combinatorics
4 OS × 3 roles × 3 tailnet states = 36 leaves before any consent is considered, and consents multiply it past anything anyone would publish.
So the tree the install page walks cannot be the full product. Two ways out, to choose between:
- Publish a few opinionated leaves — "Ubuntu VPS, new tailnet", "macOS dev machine", "Ubuntu homelab, existing tailnet" — and send everything else to the full interactive script.
- Generate on demand — the page composes the leaf when the questions are answered. Stronger, but the artifact is no longer a static file anyone can diff against the repo, which costs the verification property the whole design was for.
My inclination is (1), because (2) quietly trades away the thing that made per-leaf scripts worth building. But it is a real trade and it is yours.
Open, for tomorrow
- Does a leaf strip dead code, or set constants and call the base?
- How many leaves get published, and what happens to the rest?
- Does the install page show the script before running it? It should — that is the moment auditing is cheap and nobody will do it afterwards.
- The report from
install-report.mdnames a script commit. A generated leaf needs to name the source commit it was generated from, or the report cannot be checked against anything.