Summary
Profile pre_install / post_install scripts run before the guest user is created, so a profile that writes into the guest user's home directory (e.g. installing dotfiles) either fails or writes into the wrong place. The files land in the base CI rootfs's leftover /home/ubuntu, which is then orphaned when guest-config.sh replaces the uid-1000 user.
Root cause
compose_recipe in src/setup.rs assembles the chroot provisioning script in this order:
- preamble +
export GUEST_USER=...
- base packages
- profile pre-install scripts (
src/setup.rs:664)
- third-party repos + packages
- profile post-install scripts (
src/setup.rs:704)
guest-config.sh — this is where the guest user is created (src/setup.rs:720, script at scripts/guest/guest-config.sh:61-95)
- claude-code / codex
So at steps 3 and 5 the guest user and its /home/<user> do not exist yet. The comment at src/setup.rs:719 shows the placement was only chosen to be "before claude-code" — profile scripts that depend on the user were never accounted for.
Symptoms
Two users / home directories are visible in the guest:
<guest_user> / /home/<guest_user> — the configured guest user, created by guest-config.sh at step 6.
ubuntu / /home/ubuntu — ships with the base CI rootfs image (not created by coop at launch).
When a custom guest user is configured, guest-config.sh evicts the pre-existing uid-1000 user:
# scripts/guest/guest-config.sh:69-75
EXISTING=$(getent passwd 1000 | cut -d: -f1) || true
if [[ -n "$EXISTING" ]]; then
userdel "$EXISTING" # no -r → /home/ubuntu is left on disk
fi
useradd -m -s /bin/bash --uid 1000 -G sudo,docker "${GUEST_USER}"
Two leaks:
- Only the uid-1000 occupant is removed. If the base image's
ubuntu is not at uid 1000, it survives as a full account alongside <guest_user>.
userdel is called without -r, so /home/ubuntu is left behind as an orphan even when the account is removed.
A profile installing dotfiles at step 5 finds no /home/<guest_user>. If it falls back to (or hardcodes) /home/ubuntu, the files land there — and are then stranded when step 6 deletes/replaces the uid-1000 user, leaving the real guest home empty.
Fix
Create the guest user before profile scripts run. guest-config.sh cannot simply be moved to the top, because it also symlinks the claude binary that is not installed until step 7. Split it instead:
- Extract the user + home creation block (
scripts/guest/guest-config.sh:61-95: useradd/usermod, sudoers, /home/<user> ownership, .local tree, .ssh keys) into its own script segment.
- Run that segment in
compose_recipe right after base packages and before pre_installs (src/setup.rs:663).
- Leave the rest of
guest-config.sh (networking, docker daemon, claude symlink, services, sshd) where it is at src/setup.rs:720.
- Add
-r to the userdel so the orphan /home/ubuntu does not linger.
- Add a unit test asserting the
useradd line precedes profile pre/post-install in the composed recipe (guards against regressing the ordering).
Acceptance criteria
- A profile
post_install (or pre_install) script can write to /home/<guest_user> and have it persist.
- No orphaned
/home/ubuntu remains after provisioning with a custom guest user.
- A test pins the ordering: guest-user creation before profile scripts.
Summary
Profile
pre_install/post_installscripts run before the guest user is created, so a profile that writes into the guest user's home directory (e.g. installing dotfiles) either fails or writes into the wrong place. The files land in the base CI rootfs's leftover/home/ubuntu, which is then orphaned whenguest-config.shreplaces the uid-1000 user.Root cause
compose_recipeinsrc/setup.rsassembles the chroot provisioning script in this order:export GUEST_USER=...src/setup.rs:664)src/setup.rs:704)guest-config.sh— this is where the guest user is created (src/setup.rs:720, script atscripts/guest/guest-config.sh:61-95)So at steps 3 and 5 the guest user and its
/home/<user>do not exist yet. The comment atsrc/setup.rs:719shows the placement was only chosen to be "before claude-code" — profile scripts that depend on the user were never accounted for.Symptoms
Two users / home directories are visible in the guest:
<guest_user>//home/<guest_user>— the configured guest user, created byguest-config.shat step 6.ubuntu//home/ubuntu— ships with the base CI rootfs image (not created by coop at launch).When a custom guest user is configured,
guest-config.shevicts the pre-existing uid-1000 user:Two leaks:
ubuntuis not at uid 1000, it survives as a full account alongside<guest_user>.userdelis called without-r, so/home/ubuntuis left behind as an orphan even when the account is removed.A profile installing dotfiles at step 5 finds no
/home/<guest_user>. If it falls back to (or hardcodes)/home/ubuntu, the files land there — and are then stranded when step 6 deletes/replaces the uid-1000 user, leaving the real guest home empty.Fix
Create the guest user before profile scripts run.
guest-config.shcannot simply be moved to the top, because it also symlinks theclaudebinary that is not installed until step 7. Split it instead:scripts/guest/guest-config.sh:61-95:useradd/usermod, sudoers,/home/<user>ownership,.localtree,.sshkeys) into its own script segment.compose_reciperight after base packages and beforepre_installs(src/setup.rs:663).guest-config.sh(networking, docker daemon, claude symlink, services, sshd) where it is atsrc/setup.rs:720.-rto theuserdelso the orphan/home/ubuntudoes not linger.useraddline precedes profile pre/post-install in the composed recipe (guards against regressing the ordering).Acceptance criteria
post_install(orpre_install) script can write to/home/<guest_user>and have it persist./home/ubunturemains after provisioning with a custom guest user.