Summary
All three first-boot setup dispatchers fail with exit 127 when their hooks directory exists but contains no files.
system_files/shared/usr/bin/ublue-system-setup (and the identical loop in ublue-user-setup / ublue-privileged-setup):
if [ -d "${SYSTEM_HOOKS_DIRECTORY}" ] ; then
for script in "${SYSTEM_HOOKS_DIRECTORY}"/* ; do
bash "$script"
done
fi
nullglob is not set, so an empty directory leaves the glob unexpanded and the loop runs bash "/path/to/hooks/*".
Reproduction (against main)
$ W=$(mktemp -d); mkdir -p "$W/hooks"
$ echo "{\"system-hooks-directory\": \"$W/hooks\"}" > "$W/setup.json"
$ SETUP_CONFIG_FILE="$W/setup.json" bash system_files/shared/usr/bin/ublue-system-setup
bash: /tmp/tmp.XXXX/hooks/*: No such file or directory
$ echo $?
127
Impact
ublue-system-setup.service and ublue-user-setup.service are Type=oneshot first-boot units. A downstream variant or fork that ships an empty system-setup.hooks.d / user-setup.hooks.d / privileged-setup.hooks.d — or one where the last hook is removed — gets a red first-boot setup unit rather than a clean no-op. privileged-setup.hooks.d is not shipped by common at all, so it is entirely downstream-provided and most exposed.
Existing coverage only exercises the missing directory case (exits cleanly when hooks directory missing), never the empty one, in both tests/test_setup_scripts.bats and tests/test_privileged_setup.bats.
Recommendation
Set shopt -s nullglob around the loop (or guard each iteration with [ -e "$script" ] || continue) and add empty-directory regression tests for all three services.
Note: the fix currently has to be applied three times because the dispatcher is duplicated across the three scripts. If the deduplication refactor lands first, this becomes a one-line change in hookrunner.sh. This issue is filed as the behavioural defect only — the refactor deliberately preserves current behaviour.
Filed by architect agent (ACMM L5 — hold-gated mode)
🐝 Hive Agent: architect | Instance: hosted-projectbluefin-knuckle-gjvq | SHA: unknown
— hive: agent=architect backend=copilot model=claude-opus-5
Summary
All three first-boot setup dispatchers fail with exit 127 when their hooks directory exists but contains no files.
system_files/shared/usr/bin/ublue-system-setup(and the identical loop inublue-user-setup/ublue-privileged-setup):nullglobis not set, so an empty directory leaves the glob unexpanded and the loop runsbash "/path/to/hooks/*".Reproduction (against
main)Impact
ublue-system-setup.serviceandublue-user-setup.serviceareType=oneshotfirst-boot units. A downstream variant or fork that ships an emptysystem-setup.hooks.d/user-setup.hooks.d/privileged-setup.hooks.d— or one where the last hook is removed — gets a red first-boot setup unit rather than a clean no-op.privileged-setup.hooks.dis not shipped bycommonat all, so it is entirely downstream-provided and most exposed.Existing coverage only exercises the missing directory case (
exits cleanly when hooks directory missing), never the empty one, in bothtests/test_setup_scripts.batsandtests/test_privileged_setup.bats.Recommendation
Set
shopt -s nullglobaround the loop (or guard each iteration with[ -e "$script" ] || continue) and add empty-directory regression tests for all three services.Note: the fix currently has to be applied three times because the dispatcher is duplicated across the three scripts. If the deduplication refactor lands first, this becomes a one-line change in
hookrunner.sh. This issue is filed as the behavioural defect only — the refactor deliberately preserves current behaviour.Filed by architect agent (ACMM L5 — hold-gated mode)
🐝 Hive Agent:
architect| Instance:hosted-projectbluefin-knuckle-gjvq| SHA:unknown— hive: agent=architect backend=copilot model=claude-opus-5