streamdeck-aerospace-rewire
Skill0 starsChanged 5 months agoMITView on GitHub
Rewire Stream Deck buttons that use the "System → Hotkey" action onto silent `.app` wrappers so they trigger AeroSpace bindings. Use when the user says "stream deck buttons not working with aerospace", "fix stream deck workspace buttons", "stream deck hotkey not triggering aerospace", "rewire stream deck buttons", "stream deck button does nothing", "stream deck firing wrong app", or describes a Stream Deck key whose chord leaks through to iTerm / the frontmost app instead of switching workspaces. Patches the Stream Deck profile manifest in place, builds `osacompile` wrappers in `~/.config/aerospace/apps/`, and preserves existing button titles and icons.
In this repository18 other files it gives its agents.
- app-icon-gridSkill
- archive-manualSkill
- commitSkill
- component-consistencySkill
- component-locatorSkill
- debug-triageSkill
- gemini-iconSkill
- parity-auditorSkill
- prd-writingSkill
- refactor-and-safety-passSkill
- release-helperSkill
- smellSkill
- streamdeck-workspace-buttonSkill
- svg-icon-renderSkill
- test-conventionsSkill
- verification-gateSkill
- work-breakdownSkill
- yt-audioSkill
The file
We split this skill into the rules it gives an agent
3 rules. About 0 are common, 1 is distinctive, and 2 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
2
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 95
If a chord has no AeroSpace binding, surface it and ask the user what should run.
- Very specific: line 121
If AppleScript-quit returns User canceled (-128) (a dirty-state dialog catches it), fall back to:
- Very specific: line 143
If a button needs more than a Hotkey→Open swap (different title, new icon, swap to a different .app), use set-streamdeck-button.sh — same …
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Stream Deck → AeroSpace rewire (Hotkey → Open)
Stream Deck's "System → Hotkey" action synthesizes keystrokes via a CGEvent path that does not trigger Carbon's RegisterEventHotKey — the API AeroSpace uses for global hotkey capture. The chord leaks through to the frontmost app (usually iTerm, which interprets it as a split-pane or menu shortcut). The fix is to replace Hotkey actions with Open actions pointed at a silent .app wrapper that shells out to the same command AeroSpace would have run.
Related memory:
- [[reference_aerospace_no_autoreload]] — AeroSpace doesn't auto-reload
~/.config/aerospace/aerospace.toml;aerospace reload-configafter edits. - [[reference_streamdeck_aerospace_hotkey_bypass]] — the bypass itself.
Helper scripts on Tarik's machine
Under ~/.config/aerospace/scripts/. Each script has --help and is self-contained — read the help cold.
diagnose-streamdeck-aerospace.sh— health check (daemon, config freshness, permissions, manifest discovery). Run this first.inventory-streamdeck-buttons.sh— TSV of every button in the active profile.--only-hotkeyfilters to the rewire candidates and decodes their chords (e.g.cmd-ctrl-alt-h).build-aerospace-wrapper.sh—osacompile+ PlistBuddy in one call; produces~/.config/aerospace/apps/<Name>.appwithLSUIElement=true.rewire-streamdeck-button.sh— Hotkey → Open at one coord, preserving title + image. Backs the manifest up first.set-streamdeck-button.sh— general per-button editor (title / icon / action). Used when a button needs more than the Hotkey-to-Open swap. Pass--createto add a new button at a coord that doesn't exist yet (greenfield profile or empty slot) — the script seedsActions = {}and a fresh ActionID stub before applying edits.
rewire-streamdeck-button.sh and set-streamdeck-button.sh refuse to run if Stream Deck is up and the target manifest is the live profile (otherwise Stream Deck rewrites the file from in-memory state on save and clobbers the patch). The scripts exit 2 with the pkill recovery command in the message.
Conventions on Tarik's machine
- AeroSpace config:
~/.config/aerospace/aerospace.toml. Modifier isalt; some chords usecmd-ctrl-alt-<key>. - Silent app wrappers:
~/.config/aerospace/apps/<CamelCaseName>.app. - Underlying scripts (when one exists):
~/.config/aerospace/scripts/<slug>.sh. - Stream Deck profile manifests:
~/Library/Application Support/com.elgato.StreamDeck/ProfilesV3/*/Profiles/*/manifest.json. Actions live at.Controllers[0].Actions. - Hotkey action UUID:
com.elgato.streamdeck.system.hotkey. Open action UUID:com.elgato.streamdeck.system.open. - Stream Deck button face (title + image) lives in the action's
Statesarray — preserve it across rewrites. jqandosacompileare available system-wide.
Diagnostic flow — run before fixing anything
1. Confirm AeroSpace daemon is running with the current config
~/.config/aerospace/scripts/diagnose-streamdeck-aerospace.shaerospace-config-fresh flips to WARN when ~/.config/aerospace/aerospace.toml is newer than the daemon's start time — run aerospace reload-config before assuming the Hotkey bypass is the cause. TCC accessibility checks come back WARN ("cannot read TCC.db") when the calling Terminal lacks Full Disk Access; that's a check-tool limitation, not a missing permission.
2. Prove the bypass
Synthesize the chord via osascript and watch the workspace switch:
osascript -e 'tell application "System Events" to key code <N> using {control down, option down, command down}'RegisterEventHotKey sees osascript-synthesized events but not Stream Deck's. So:
- osascript triggers the AeroSpace binding and Stream Deck does → not the bypass, look elsewhere (chord mismatch, stale config, app focus).
- osascript triggers it but Stream Deck doesn't → confirmed bypass; proceed with the fix.
Key code lookup is in /System/Library/Frameworks/Carbon.framework/.../Events.h (e.g. h = 4, j = 38).
3. Permissions sanity check
Both apps need Accessibility. diagnose-streamdeck-aerospace.sh covers this via TCC.db query; the WARN ("cannot read TCC.db") path means the script can't verify, not that the permission is missing. To verify directly, give Terminal Full Disk Access or query TCC.db manually:
sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" \
"SELECT client, auth_value FROM access WHERE service='kTCCServiceAccessibility' AND (client LIKE '%elgato%' OR client='bobko.aerospace');"auth_value=2 is granted. If Stream Deck is missing, add it in System Settings → Privacy & Security → Accessibility. AeroSpace missing is rarer but possible after upgrades.
Fix steps
1. Inventory the rewires
~/.config/aerospace/scripts/inventory-streamdeck-buttons.sh --only-hotkeyOutput: COORD<TAB>Hotkey<TAB>TITLE<TAB>IMAGE<TAB>CHORD. The chord is already decoded (KeyModifiers bitmask + VKeyCode Carbon code → cmd-ctrl-alt-h). Unknown VKeyCodes fall through as vk<N>.
Present a table to the user: coord · current title · chord · proposed wrapper .app name. Confirm before editing anything.
2. Map chord → command
For each chord, find the matching AeroSpace binding in ~/.config/aerospace/aerospace.toml:
grep -E "cmd-ctrl-alt-h\s*=" ~/.config/aerospace/aerospace.tomlPull the right-hand side. Bindings usually look like exec-and-forget <script> <args> or workspace <N>. Build the wrapped command from that:
exec-and-forget /path/to/script.sh foo→ wrapper runs/path/to/script.sh foo.workspace 3→ wrapper runs/Applications/AeroSpace.app/Contents/MacOS/aerospace workspace 3.
If a chord has no AeroSpace binding, surface it and ask the user what should run.
3. Build silent .app wrappers
~/.config/aerospace/scripts/build-aerospace-wrapper.sh \
--name <CamelCaseName> \
--command "<full command with args>"Prints the resulting .app path. LSUIElement=true and CFBundleIdentifier=digital.tiki.<kebab> are set automatically. Test before patching the manifest:
open "$HOME/.config/aerospace/apps/<CamelCaseName>.app"
aerospace list-workspaces --focused # for workspace-switch wrappers
aerospace list-windows --workspace <N> # for layout/launch wrappers4. Quit Stream Deck before patching the manifest
Stream Deck rewrites manifest.json from in-memory state on quit/save, so a live edit gets clobbered. The rewire/set scripts will refuse to run against the live manifest while SD is up — quit first:
osascript -e 'tell application "Elgato Stream Deck" to quit'If AppleScript-quit returns User canceled (-128) (a dirty-state dialog catches it), fall back to:
pkill -f "/Applications/Elgato Stream Deck.app/Contents/MacOS/Stream Deck"5. Patch the manifest
One coord at a time:
~/.config/aerospace/scripts/rewire-streamdeck-button.sh \
--coord <row,col> \
--wrapper "$HOME/.config/aerospace/apps/<CamelCaseName>.app"The script:
- Backs up the manifest to
<manifest>.bak.before-rewire-<unix-timestamp>. - Replaces the Hotkey action with an Open action pointed at
--wrapper. - Preserves
ActionID,LinkedTitle, andStates(title + image survive). - Verifies the post-patch UUID and States count.
If a button needs more than a Hotkey→Open swap (different title, new icon, swap to a different .app), use set-streamdeck-button.sh — same backup + safety contract.
6. Relaunch Stream Deck
open -a "Elgato Stream Deck"Press each rewired key. The button face should be unchanged; the action should now fire reliably.
Failure modes to flag
- Manifest clobbered by Stream Deck — happens if Stream Deck wasn't fully quit before the edit. The rewire/set scripts guard against this with the exit-2 SD-running gate; if Stream Deck was launched mid-edit, the
.bak.before-rewire-*backup is your recovery: quit Stream Deck,cp <backup> <manifest>, retry. - Open action
pathdoesn't expand~— must be absolute (/Users/<your-username>/...). The Stream Deck app will silently no-op on a tilde path.build-aerospace-wrapper.shalways returns an absolute path; pass that through verbatim. .appwrapper quarantined — locally-built.apps usually aren't, but if Stream Deck refuses to launch one:xattr -d com.apple.quarantine <path>.- Automation permission prompt on first press — macOS asks once for the wrapper to control whichever app
do shell scriptinvokes (or Shortcuts). Allow once; sticks. - Daemon still running stale config after toml edits — AeroSpace doesn't auto-reload. Run
aerospace reload-configafter every.tomlchange before retesting. ([[reference_aerospace_no_autoreload]]) - Chord matches no AeroSpace binding — the Hotkey button was firing into the frontmost app the whole time; nothing to rewire to until the user decides what should run.
- Unknown
VKeyCodein inventory — falls through asvk<N>. Look up the code inCarbon/Events.hand add it to the lookup table ininventory-streamdeck-buttons.sh'sVK_TABLEheredoc.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

