agentleFS
tarikhamiltontarikhamilton/claude-skills/skills/streamdeck-aerospace-rewire/SKILL.md

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.

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
  1. Distinctive:

    If a chord has no AeroSpace binding, surface it and ask the user what should run.

    line 95
  2. Very specific:

    If AppleScript-quit returns User canceled (-128) (a dirty-state dialog catches it), fall back to:

    line 121
  3. Very specific:

    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 …

    line 143

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

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-config after 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-hotkey filters 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>.app with LSUIElement=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 --create to add a new button at a coord that doesn't exist yet (greenfield profile or empty slot) — the script seeds Actions = {} 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 is alt; some chords use cmd-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 States array — preserve it across rewrites.
  • jq and osacompile are 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.sh

aerospace-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-hotkey

Output: 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.toml

Pull 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 wrappers

4. 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, and States (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 path doesn't expand ~ — must be absolute (/Users/<your-username>/...). The Stream Deck app will silently no-op on a tilde path. build-aerospace-wrapper.sh always returns an absolute path; pass that through verbatim.
  • .app wrapper 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 script invokes (or Shortcuts). Allow once; sticks.
  • Daemon still running stale config after toml edits — AeroSpace doesn't auto-reload. Run aerospace reload-config after every .toml change 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 VKeyCode in inventory — falls through as vk<N>. Look up the code in Carbon/Events.h and add it to the lookup table in inventory-streamdeck-buttons.sh's VK_TABLE heredoc.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.