The host-side updater has been silently broken in two ways: 1. Empty staging directories would cause rm -rf of live routes/src with no replacement, leaving the host tree gutted while the container kept serving from its own image. Now deploy_tree() refuses to delete unless the staging source has actual files. 2. chattr +i on critical files (used to protect security-hotfixed routes from being clobbered by upstream tarballs) caused rm -rf to partially execute then fail under set -e, leaving the host in a half-deleted state. Now deploy_tree() scans for immutable files, unlocks them before replace, and re-locks them after — so security-locked files survive every update. Also adds: - Channel gate: trigger.json channel=prerelease/beta/rc/alpha is rejected unless ALLOW_PRERELEASE=true is set in /opt/dashcaddy/updates/channel.conf. Default is 'stable only', safe for production. Staging hosts opt in. - channel.conf.example documenting the new opt-in mechanism. Verified end-to-end: manual trigger.json → path unit fired → routes (53 files) + src (62 files) deployed → container rebuilt → health check passed. totp.js remained locked with security edits intact.