On macOS (Tahoe), plugin scripts spawned by SketchyBar fail when they invoke commands that internally use waitpid() - most notably brew outdated which crashes with:
Error: undefined method 'success?' for nil
/opt/homebrew/Library/Homebrew/hardware.rb:106:in 'Hardware::CPU.cores'
The root cause appears to be that fork_exec() in src/misc/helpers.h uses vfork() and execvp() without calling setsid() in the child process. The spawned plugin processes end up with "Session ID 0" and no controlling terminal (TTY: ??). On macOS 26, this causes Ruby's IO.popen("-") (fork mode) to fail to properly waitpid() for its child, leaving $? ($CHILD_STATUS) as nil. Homebrew's Hardware::CPU.cores calls Utils.popen_read("getconf", "_NPROCESSORS_ONLN") and then checks $CHILD_STATUS.success?, which raises NoMethodError on nil.
This likely worked on earlier macOS versions (14, 15) where the kernel allowed waitpid() to succeed even with Session ID 0.
Reproduction
- Install SketchyBar v2.23.0 via Homebrew on macOS 26
- Create a plugin script that runs
brew outdated
- Observe the error in
/opt/homebrew/var/log/sketchybar/sketchybar.err.log when running sketchybar using brew services
Root cause analysis
The relevant code in src/misc/helpers.h (https://github.com/FelixKratz/SketchyBar/blob/master/src/misc/helpers.h):
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
static inline bool fork_exec(char *command, struct env_vars* env_vars) {
int pid = vfork();
if (pid == -1) return false;
if (pid != 0) return true;
alarm(FORK_TIMEOUT);
exit(sync_exec(command, env_vars));
}
#pragma clang diagnostic pop
Key observations:
vfork() is used (and the deprecation warning is explicitly suppressed). Apple has deprecated vfork() and recommends posix_spawn() instead.
- No
setsid() call is made in the child before exec, so spawned processes inherit Session ID 0 (no session) and have no controlling terminal.
- On macOS 26, this breaks
waitpid() behavior in grandchild processes (e.g., Ruby calling IO.popen("-") which forks internally).
Evidence:
- Running
ps -o pid,ppid,sess,tty,command on a plugin process shows SESS=0 and TTY=??
- Wrapping the plugin script with
setsid (e.g., perl -e 'use POSIX; POSIX::setsid(); exec @ARGV') before execution completely fixes the issue
- The same Ruby binary and
brew outdated command works perfectly from a normal terminal session
- Both Ruby 3.4.8 and 4.0.1 are affected equally — this is not a Ruby version issue
SIGCHLD signal disposition is DEFAULT in both contexts — not a signal handler issue
Suggested fix
Option A (minimal): Add setsid() in the child before exec:
static inline bool fork_exec(char *command, struct env_vars* env_vars) {
int pid = fork();
if (pid == -1) return false;
if (pid != 0) return true;
setsid();
alarm(FORK_TIMEOUT);
exit(sync_exec(command, env_vars));
}
Option B (recommended by Apple): Replace vfork() + execvp() with posix_spawn(), which is the modern API Apple recommends for spawning child processes and handles session/process group setup correctly.
Current workaround
Users can work around this in their plugin scripts by re-execing in a new session:
#!/bin/bash
if [ -z "$_SETSID_DONE" ]; then
export _SETSID_DONE=1
exec /usr/bin/perl -e 'use POSIX; POSIX::setsid(); exec @ARGV' -- "$0" "$@"
fi
# ... rest of plugin script
Environment
- macOS: 26.3.1 (Build 25D2128)
- Kernel: Darwin 25.3.0 (xnu-12377.91.3~2/RELEASE_ARM64_T6020)
- Hardware: Mac14,10 (Apple M2 Pro)
- SketchyBar: v2.23.0
- Ruby: 3.4.8 and 4.0.1 (both tested, both affected)
- Homebrew: installed via /opt/homebrew
On macOS (Tahoe), plugin scripts spawned by SketchyBar fail when they invoke commands that internally use
waitpid()- most notablybrew outdatedwhich crashes with:The root cause appears to be that
fork_exec()insrc/misc/helpers.husesvfork()andexecvp()without callingsetsid()in the child process. The spawned plugin processes end up with "Session ID 0" and no controlling terminal (TTY: ??). On macOS 26, this causes Ruby'sIO.popen("-")(fork mode) to fail to properlywaitpid()for its child, leaving$?($CHILD_STATUS) asnil. Homebrew'sHardware::CPU.corescallsUtils.popen_read("getconf", "_NPROCESSORS_ONLN")and then checks$CHILD_STATUS.success?, which raisesNoMethodErroronnil.This likely worked on earlier macOS versions (14, 15) where the kernel allowed
waitpid()to succeed even with Session ID 0.Reproduction
brew outdated/opt/homebrew/var/log/sketchybar/sketchybar.err.logwhen running sketchybar usingbrew servicesRoot cause analysis
The relevant code in src/misc/helpers.h (https://github.com/FelixKratz/SketchyBar/blob/master/src/misc/helpers.h):
Key observations:
vfork()is used (and the deprecation warning is explicitly suppressed). Apple has deprecatedvfork()and recommendsposix_spawn()instead.setsid()call is made in the child before exec, so spawned processes inherit Session ID 0 (no session) and have no controlling terminal.waitpid()behavior in grandchild processes (e.g., Ruby callingIO.popen("-")which forks internally).Evidence:
ps -o pid,ppid,sess,tty,commandon a plugin process showsSESS=0andTTY=??setsid(e.g.,perl -e 'use POSIX; POSIX::setsid(); exec @ARGV') before execution completely fixes the issuebrew outdatedcommand works perfectly from a normal terminal sessionSIGCHLDsignal disposition isDEFAULTin both contexts — not a signal handler issueSuggested fix
Option A (minimal): Add
setsid()in the child beforeexec:Option B (recommended by Apple): Replace
vfork()+execvp()withposix_spawn(), which is the modern API Apple recommends for spawning child processes and handles session/process group setup correctly.Current workaround
Users can work around this in their plugin scripts by re-execing in a new session:
Environment