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:
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 swait after closing the editor is tunable (insd-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.shgenerates 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):
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.
