default the username to whoever ran sudo, and look up their real home

Three changes to the question at the top of the run.

USER_HOME is looked up rather than assumed. The original built "/home/$USERNAME",
which is only the usual answer — an account created with a different home, or one
whose home was moved, had every later step writing to a directory that was not
theirs. getent passwd knows; the /home guess remains only as the fallback for an
account that does not exist yet, where there is nothing to look up.

The default is now whoever invoked sudo. On a re-run, or on a machine that is
already somebody's, that is the answer every time, and retyping it is a chance to
typo it into creating a second account. root invoking the script directly offers
no default, since root is never the account being set up — and is refused if
typed.

The name is validated against the portable shape of a Linux account name before
anything else happens. Letting adduser refuse it later means several questions
have already been answered against a name that was never going to work.

It also no longer goes through prompt_value, which obeys any environment variable
matching the name it is filling in. USERNAME is set by some login environments,
and a variable this script silently takes as an answer should not be one that
might already be set for unrelated reasons. SETUP_USERNAME is the explicit
override.

The tmux config write moved out of the user section entirely. It was there only
because the original copied it right after adduser, where $USER_HOME first
exists. It is a dotfile and belongs with .zshrc and the starship config in the
shell section.

Verified: defaults to the sudo invoker, resolves daemon's home to /usr/sbin
rather than /home/daemon, falls back to /home for an account that does not exist,
and rejects a name with a space, a leading digit, one over 32 characters, and
root.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-12 18:27:46 +00:00
co-authored by Claude Opus 5
parent 3fb0e5c887
commit fbb90c917d
2 changed files with 62 additions and 19 deletions
+56
View File
@@ -163,6 +163,62 @@ confirm() {
done done
} }
# Which account this machine is being set up for.
#
# Asked at the top because two later questions default off it — where Officer is
# installed, and where the disk ballast goes — so it has to be settled before
# either is put to the user.
#
# Defaults to whoever invoked sudo. On a re-run, or on a machine that is already
# somebody's, that is the answer every time, and typing it again is a chance to
# typo it into creating a second account.
#
# SETUP_USERNAME in the environment answers it ahead of time. Deliberately not
# USERNAME: that name is set by some login environments, and a variable this
# script silently obeys should not be one that might already be in the
# environment for unrelated reasons.
ask_username() {
local default="${SUDO_USER:-}" answer
# root invoked the script directly rather than through sudo. It is never the
# account being set up, so there is nothing to suggest.
[[ "$default" == "root" ]] && default=""
if [[ -n "${SETUP_USERNAME:-}" ]]; then
answer="$SETUP_USERNAME"
else
echo ""
info "Which account is this machine for?"
echo " The account you log in and work as — not root. It will be created"
echo " if it does not exist."
echo ""
while [[ -z "${answer:-}" ]]; do
if ! read -rp " Username${default:+ [$default]}: " answer; then
echo ""
fail "No answer. Set SETUP_USERNAME=<name> to answer this ahead of time."
fi
answer="${answer:-$default}"
[[ -z "$answer" ]] && warn "There is no default here — type a username."
done
fi
# The portable shape of a Linux account name. Worth checking rather than
# letting adduser refuse it later, because by then several questions have been
# answered against a name that was never going to work.
[[ "$answer" =~ ^[a-z_][a-z0-9_-]*\$?$ && ${#answer} -le 32 ]] ||
fail "'${answer}' is not a usable Linux username — lower case, starting with a letter or underscore."
[[ "$answer" == "root" ]] && fail "root is not the account to set up here."
USERNAME="$answer"
# Looked up, not assumed. The original built "/home/$USERNAME", which is merely
# the usual answer — an account created with a different home, or one whose home
# was moved, would have every later step writing to a directory that is not
# theirs.
USER_HOME="$(getent passwd "$USERNAME" 2>/dev/null | cut -d: -f6)"
[[ -n "$USER_HOME" ]] || USER_HOME="/home/${USERNAME}"
}
# Where Officer will live. # Where Officer will live.
# #
# Asked in pre-flight with the rest of the questions rather than at the point it # Asked in pre-flight with the rest of the questions rather than at the point it
+6 -19
View File
@@ -73,13 +73,13 @@ if [[ "$EUID" -ne 0 ]]; then
fail "Please run as root: sudo ./machine-setup.sh" fail "Please run as root: sudo ./machine-setup.sh"
fi fi
prompt_value USERNAME "New admin username (or existing)" "" ask_username
if [[ -z "$USERNAME" ]]; then if id "$USERNAME" &>/dev/null; then
fail "Username cannot be empty" info "Account: ${USERNAME} (exists, home ${USER_HOME})"
else
info "Account: ${USERNAME} (will be created, home ${USER_HOME})"
fi fi
USER_HOME="/home/$USERNAME"
ask_officer_root ask_officer_root
if [[ -d "$OFFICER_ROOT" ]]; then if [[ -d "$OFFICER_ROOT" ]]; then
info "Officer: ${OFFICER_ROOT} (exists already)" info "Officer: ${OFFICER_ROOT} (exists already)"
@@ -813,19 +813,6 @@ if ! skip; then
SUMMARY+=("Sudo: password required (Officer's account provisioning will not work)") SUMMARY+=("Sudo: password required (Officer's account provisioning will not work)")
fi fi
fi fi
# ── tmux config ──
#
# install_config rather than cp: the original overwrote whatever was there on
# every single run.
if [[ -f "$SCRIPT_DIR/.tmux.conf" ]] && id "$USERNAME" &>/dev/null; then
install_config "$SCRIPT_DIR/.tmux.conf" "${USER_HOME}/.tmux.conf" "$USERNAME"
case $? in
0) ok "tmux config installed" ;;
1) : ;;
2) : ;;
esac
fi
step_ok step_ok
fi fi
@@ -836,7 +823,7 @@ fi
# Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order: # Sections still to move across from scripts/setup-old/setup-ubuntu.sh, in order:
# #
# ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades · # ssh keys · ssh hardening · dns · static ip · fail2ban · unattended-upgrades ·
# git config · docker · zsh + prompt · tailscale · neovim · js runtimes · # git config · docker · zsh + prompt (incl. .tmux.conf) · tailscale · neovim · js runtimes ·
# dev tools · ufw · zshrc # dev tools · ufw · zshrc
# #
# Each arrives as its own commit. Delete this block when the list is empty. # Each arrives as its own commit. Delete this block when the list is empty.