Summary
On macOS, every desktop-notification banner leaves empty ffiXXXXXX files in $HOME. The overlay is rendered by osascript -l JavaScript scripts/mac-overlay.js, launched once per display. JavaScript-for-Automation calls AppKit through the Objective-C bridge, which uses libffi. Under osascript's hardened runtime (no allow-jit / allow-unsigned-executable-memory entitlement), libffi can't get anonymous executable memory and falls back to mkstemp("$HOME/ffiXXXXXX") + mmap(PROT_EXEC), then never unlinks it. With notification_all_screens: true and N displays, each notification leaks ~N empty files.
Environment
- peon-ping 2.29.0
- macOS 15.7.7 (x86_64), 2 displays
- System
/usr/bin/osascript (hardened runtime)
Reproduction
- Defaults:
desktop_notifications: true, notification_all_screens: true.
- Trigger any notification (e.g. a
Stop/Notification hook event).
- ~1–2s later (the overlay is backgrounded via
nohup … &), new 0-byte ~/ffiXXXXXX files appear — one per display.
Evidence
- Files match libffi's
open_temp_exec_file template (ffi + 6 mkstemp chars), mode 0600, size 0.
- Caught live: one notification spawned two
osascript … mac-overlay.js processes (slots 0 and 1 for 2 screens) and created ~/ffiXXXXXX ~1.9s later.
- Setting
desktop_notifications: false stops all new files; sounds (afplay/peon-play, no libffi) are unaffected.
- The system libffi used by
python3 ctypes and Ruby Fiddle uses static trampolines and does not leak — the issue is specific to the JXA/AppKit overlay under osascript.
- Real-world accumulation: 48 files over ~3 days on one machine.
Impact
Cosmetic but unbounded — $HOME steadily fills with empty files, and the ffi prefix gives users no hint of the origin (mistaken for compiler/AWS/Homebrew leftovers).
Suggested fixes (any one)
- Reap after render: snapshot
ffiXXXXXX before/after launching the overlay and remove the new ones once osascript exits.
- Redirect the leak, then sweep: launch the overlay subprocess with
HOME pointed at a peon-owned scratch dir (libffi's last-resort dir is $HOME), and clean that dir. Note: TMPDIR/LIBFFI_TMPDIR alone won't help — those dirs fail osascript's PROT_EXEC test on hardened macOS, which is why it lands in $HOME.
- Avoid the libffi bridge: render via a compiled Swift helper (like
peon-play) or UNUserNotification/terminal-notifier instead of JXA→AppKit.
Debugged, drafted, and raised by Claude (Anthropic's Claude Code) on behalf of a peon-ping user.
Summary
On macOS, every desktop-notification banner leaves empty
ffiXXXXXXfiles in$HOME. The overlay is rendered byosascript -l JavaScript scripts/mac-overlay.js, launched once per display. JavaScript-for-Automation calls AppKit through the Objective-C bridge, which uses libffi. Underosascript's hardened runtime (noallow-jit/allow-unsigned-executable-memoryentitlement), libffi can't get anonymous executable memory and falls back tomkstemp("$HOME/ffiXXXXXX")+mmap(PROT_EXEC), then never unlinks it. Withnotification_all_screens: trueand N displays, each notification leaks ~N empty files.Environment
/usr/bin/osascript(hardened runtime)Reproduction
desktop_notifications: true,notification_all_screens: true.Stop/Notificationhook event).nohup … &), new 0-byte~/ffiXXXXXXfiles appear — one per display.Evidence
open_temp_exec_filetemplate (ffi+ 6mkstempchars), mode0600, size 0.osascript … mac-overlay.jsprocesses (slots 0 and 1 for 2 screens) and created~/ffiXXXXXX~1.9s later.desktop_notifications: falsestops all new files; sounds (afplay/peon-play, no libffi) are unaffected.python3ctypes and RubyFiddleuses static trampolines and does not leak — the issue is specific to the JXA/AppKit overlay underosascript.Impact
Cosmetic but unbounded —
$HOMEsteadily fills with empty files, and theffiprefix gives users no hint of the origin (mistaken for compiler/AWS/Homebrew leftovers).Suggested fixes (any one)
ffiXXXXXXbefore/after launching the overlay and remove the new ones onceosascriptexits.HOMEpointed at a peon-owned scratch dir (libffi's last-resort dir is$HOME), and clean that dir. Note:TMPDIR/LIBFFI_TMPDIRalone won't help — those dirs fail osascript'sPROT_EXECtest on hardened macOS, which is why it lands in$HOME.peon-play) orUNUserNotification/terminal-notifierinstead of JXA→AppKit.Debugged, drafted, and raised by Claude (Anthropic's Claude Code) on behalf of a peon-ping user.