From d29bc23bb7fe62604a021438b2ea7acf7ae4a42b Mon Sep 17 00:00:00 2001
From: Riccardo Manfrin <3090891+riccardomanfrin@users.noreply.github.com>
Date: Mon, 3 Aug 2026 16:28:10 +0200
Subject: [PATCH] [client] launch macOS GUI as the logged-in user after
install/update (#6962)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Describe your changes
In unattended installs/updates there may be no logged-in user, so
there's no context to start the GUI (nor anyone to see it).
The bug is the GUI being started in the wrong user context / inheriting
the wrong `$HOME` (OS mechanics aside).
Today the GUI is started by a per-user LaunchAgent, i.e. on behalf of
the user who logs in — no login ⇒ no GUI.
The patch aligns to this: it launches the GUI on behalf of the logged-in
console user if one exists, otherwise it delegates the launch to the
per-user LaunchAgent at next login.
Additionally it logs when default UI settings are applied.
Note (small caveat): the LaunchAgent auto-starts the GUI at login only
once it's been registered — which happens on the first GUI launch in the
user's context. On an MDM/unattended fresh install done with no user
logged in (where the user has never run the GUI before), they may need
to start it manually once; it self-registers from then on.
## Issue ticket number and link
No public issue — reported internally (community report on Slack: macOS
advanced-view + onboarding reset on every update, esp. via MDM/Munki).
Buggy line on main:
https://github.com/netbirdio/netbird/blob/dd2bdc0de3aa14dd14dc90611c69c420bb3d3eb2/release_files/darwin_pkg/postinstall#L33
## Stack
### Checklist
- [x] Is it a bug fix
- [ ] Is a typo/documentation fix
- [ ] Is a feature enhancement
- [ ] It is a refactor
- [ ] Created tests that fail without the change (if possible)
> By submitting this pull request, you confirm that you have read and
agree to the terms of the [Contributor License
Agreement](https://github.com/netbirdio/netbird/blob/main/CONTRIBUTOR_LICENSE_AGREEMENT.md).
## Documentation
Select exactly one:
- [ ] I added/updated documentation for this change
- [x] Documentation is **not needed** for this change (explain why)
Internal macOS installer / GUI-launch behavior. No public API, CLI, or
configuration change: the fix only changes the user context the desktop
GUI is launched in after a pkg install/update.
### Docs PR URL (required if "docs added" is checked)
Paste the PR link from https://github.com/netbirdio/docs here:
N/A
---
Need help on this PR? Tag @codesmith-bot with what you
need. Autofix is disabled.
## Summary by CodeRabbit
- **Bug Fixes**
- Improved macOS installation and updater UI launching to occur only
when an active, valid GUI console session is detected.
- Prevented UI launches during unattended/system, root, or
login-window-related installs.
- Ensured the app is launched in the correct console-user context, and
skips cleanly when username/UID resolution fails.
- **Improvements**
- Added clearer informational logging when the UI preferences file is
not found and default preferences are used.
---
.../updater/installer/installer_run_darwin.go | 49 +++++++++----------
client/ui/preferences/store.go | 1 +
release_files/darwin_pkg/postinstall | 18 ++++++-
3 files changed, 41 insertions(+), 27 deletions(-)
diff --git a/client/internal/updater/installer/installer_run_darwin.go b/client/internal/updater/installer/installer_run_darwin.go
index 248a404aa..5650bc769 100644
--- a/client/internal/updater/installer/installer_run_darwin.go
+++ b/client/internal/updater/installer/installer_run_darwin.go
@@ -98,47 +98,44 @@ func (u *Installer) startDaemon(daemonFolder string) error {
func (u *Installer) startUIAsUser() error {
log.Infof("starting netbird-ui: %s", uiBinary)
- // Get the current console user
- cmd := exec.Command("stat", "-f", "%Su", "/dev/console")
- output, err := cmd.Output()
+ username, err := consoleUser()
if err != nil {
- return fmt.Errorf("failed to get console user: %w", err)
+ return err
}
- username := strings.TrimSpace(string(output))
- if username == "" || username == "root" {
- return fmt.Errorf("no active user session found")
- }
-
- log.Infof("starting UI for user: %s", username)
-
- // Get user's UID
userInfo, err := user.Lookup(username)
if err != nil {
- return fmt.Errorf("failed to lookup user %s: %w", username, err)
+ return fmt.Errorf("lookup user %s: %w", username, err)
}
- // Start the UI process as the console user using launchctl
- // This ensures the app runs in the user's context with proper GUI access
- launchCmd := exec.Command("launchctl", "asuser", userInfo.Uid, "open", "-a", uiBinary)
+ log.Infof("starting UI for user: %s (uid %s)", username, userInfo.Uid)
+
+ launchCmd := exec.Command("launchctl", "asuser", userInfo.Uid, "sudo", "-u", username, "-H", "open", "-a", uiBinary)
log.Infof("launchCmd: %s", launchCmd.String())
- // Set the user's home directory for proper macOS app behavior
- launchCmd.Env = append(os.Environ(), "HOME="+userInfo.HomeDir)
- log.Infof("set HOME environment variable: %s", userInfo.HomeDir)
- if err := launchCmd.Start(); err != nil {
- return fmt.Errorf("failed to start UI process: %w", err)
- }
-
- // Release the process so it can run independently
- if err := launchCmd.Process.Release(); err != nil {
- log.Warnf("failed to release UI process: %v", err)
+ if err := launchCmd.Run(); err != nil {
+ return fmt.Errorf("run UI launch: %w", err)
}
log.Infof("netbird-ui started successfully for user %s", username)
return nil
}
+func consoleUser() (string, error) {
+ output, err := exec.Command("stat", "-f", "%Su", "/dev/console").Output()
+ if err != nil {
+ return "", fmt.Errorf("get console user: %w", err)
+ }
+
+ username := strings.TrimSpace(string(output))
+ switch username {
+ case "", "root", "loginwindow", "_mbsetupuser":
+ return "", fmt.Errorf("no active GUI user session, console user: %q", username)
+ }
+
+ return username, nil
+}
+
func (u *Installer) installPkgFile(ctx context.Context, path string) error {
log.Infof("installing pkg file: %s", path)
diff --git a/client/ui/preferences/store.go b/client/ui/preferences/store.go
index df6fbbb16..49acb7917 100644
--- a/client/ui/preferences/store.go
+++ b/client/ui/preferences/store.go
@@ -246,6 +246,7 @@ func (s *Store) ExistedAtLoad() bool {
func (s *Store) load() error {
if _, err := os.Stat(s.path); err != nil {
if errors.Is(err, os.ErrNotExist) {
+ log.Infof("no ui preferences file at %s; using defaults", s.path)
return nil
}
return fmt.Errorf("stat preferences: %w", err)
diff --git a/release_files/darwin_pkg/postinstall b/release_files/darwin_pkg/postinstall
index 33fa4bfee..2c96a80cd 100755
--- a/release_files/darwin_pkg/postinstall
+++ b/release_files/darwin_pkg/postinstall
@@ -30,7 +30,23 @@ mkdir -p /usr/local/bin/
$AGENT service install || true
$AGENT service start || true
- open $APP
+ console_user=$(stat -f%Su /dev/console 2>/dev/null)
+ case "$console_user" in
+ ""|root|loginwindow|_mbsetupuser)
+ echo "No active GUI user session (console user: '${console_user:-none}'); skipping UI launch."
+ ;;
+ *)
+ uid=$(id -u "$console_user" 2>/dev/null)
+ if [ -z "$uid" ]; then
+ echo "Could not resolve uid for console user '$console_user'; skipping UI launch."
+ else
+ echo "Launching NetBird UI as console user $console_user (uid $uid)."
+ if ! launchctl asuser "$uid" sudo -u "$console_user" -H open "$APP"; then
+ echo "Failed to launch NetBird UI; if autostart is enabled it will start at next login."
+ fi
+ fi
+ ;;
+ esac
echo "Finished Netbird installation successfully"
exit 0 # all good