Switching profiles even while the Stream Deck EDITOR is open (extends the ghost-app trick)

Hi all,

Some time ago I asked here how to switch Stream Deck profiles from a MIDI script, and @jordikt’s ghost-app trick solved it beautifully: {launch:"SD_switch - X.app"} and the deck jumps to the linked profile.
(orig. threads: “Switch profiles from scripting dials and buttons” and “How to run applescript from midi script action for a switch profile action?”)

There’s one well-known catch, called out in both threads:

profile switching only works while the Stream Deck configuration window is CLOSED.

With the editor open, the deck live-previews the profile you’re editing and freezes the active profile, so every switch is suppressed (the app-linked trick and the WebSocket API).

I found a reliable way to make it work even with the editor open. Full recipe below, and all the code + generators are on GitHub:
:backhand_index_pointing_right: GitHub - Beennnn/streamdeck-profile-switch: Switch Elgato Stream Deck profiles by name from the CLI — reliably, even with the config window open (via ghost apps). · GitHub


Why it’s hard

To switch while the editor is open you must first close the editor window (Cmd-W — Stream Deck stays alive in the menu bar; that lifts the lock). Closing a window programmatically needs macOS Accessibility permission — and here’s the trap I hit:

the little apps a Stream Deck button can launch are ad-hoc-signed, and ad-hoc AppleScript applets never get a working Accessibility grant on recent macOS. I verified it: count of windows returns -25211 even after enabling the app in System Settings, and a properly self-signed helper still failed to post the keystroke (1002). So the button itself can never close the editor.

The idea — decouple the trigger from the privileged action

  • The button only SIGNALS: it launches a tiny app that writes the profile name to a FIFO (/tmp/sd-switch). Writing a file needs no permission, so anything can do it.
  • A small daemon that does hold Accessibility — because it runs inside a terminal you granted Accessibility — reads the FIFO and does the privileged part: close the editor, wait a beat, then launch the ghost app (which switches the profile the usual way).

Bonus finding: a physical deck press fires its action even while the editor is open (the editor only intercepts software clicks on keys, not hardware presses). That’s why a signalling button works while you edit.

No server, no WebSocket, no network — the “daemon” is literally a shell loop reading a named pipe.


How to set it up

1) Grant your terminal Accessibility (this is the ONE manual step — a macOS security boundary, can’t be automated):
System Settings → Privacy & Security → Accessibility → enable Terminal (or iTerm).

2) Start the daemon (keep it running; a login LaunchAgent makes it permanent):

bin/sd-switch-daemon.sh          # watches /tmp/sd-switch

3) For each target profile, build two tiny apps:

bin/make-ghost-app.sh   "Live Set"   # jordikt's ghost app — bind it to the profile as usual
bin/make-signal-app.sh  "Live Set"   # the signal app the button launches

4) Wire a button to LAUNCH the signal app. Two ways, BOTH tested working with the editor open:

(a) From the MIDI plugin (a script) — “from scripts”:

[(press){launch:"/Users/you/Applications/SD-sig - Live Set.app"}]

(b) Without any plugin — “live”, base Stream Deck:
use the built-in “Open Application” action → pick SD-sig - Live Set.app.

Press the button (a), (b), or fire the MidiScript → the editor window closes and the deck switches, even though the editor was open. Same result from the command line too: echo "Live Set" > /tmp/sd-switch.


Notes

  • Response time: the ~0.8 s wait after closing the editor is tunable (in sd-profile.sh). Stream Deck releases the lock a beat after the window closes — 0.2 s was too short and dropped the switch; ~0.8–1.3 s is reliable. It only applies when a window was actually closed, so with the editor closed (a live gig) it’s near-instant.
  • Why one signal app per profile? The built-in “Open Application” action does not forward its Arguments field to the launched app, and {launch}'s parameter is passed as a document (not an argument) — so the profile name is baked into each signal app (make-signal-app.sh generates them; no permission, no binding).
  • MIDI variant: there’s also a MIDI-triggered daemon (a note/CC → profile) if you’d rather fire it from a controller, a pedal, Bome, or your DAW instead of the deck.

Full code, generators and a longer write-up (the investigation, all the dead ends):
:backhand_index_pointing_right: GitHub - Beennnn/streamdeck-profile-switch: Switch Elgato Stream Deck profiles by name from the CLI — reliably, even with the config window open (via ghost apps). · GitHub

Huge thanks to @jordikt for the ghost-app foundation — this just removes the editor-open limitation on top of it.

1 Like