Describe buttons in plain English. Claude builds the JSON, places them on your deck, and restarts the app. No manual config editing.
A skill for Claude Code (Anthropic's CLI tool) that gives Claude full control over your Elgato Stream Deck configuration. Instead of manually editing JSON or clicking through the Stream Deck GUI, you describe what you want and Claude handles everything — writing the config, generating button images, and restarting Stream Deck so your new buttons appear instantly.
This was built through extensive real-world testing across 40+ plugins, 300+ buttons, and dozens of multi-action sequences. The prompt includes hard-won lessons about plugin settings keys, JSON formatting pitfalls, and action structures that actually work.
Hotkeys, text paste, open files, launch URLs, run apps
Chain steps with delays — open terminal, navigate, launch, type commands
Different actions for single press, double press, and long press
Describe a theme and Claude plans and builds every button
Any installed plugin — with verified, correct settings keys
Auto-generated colored backgrounds, icon compositing with Pillow
pip install Pillow)~/.claude/skills/)~/.claude/skills/streamdeck/skill.md[PLACEHOLDER] values with your system pathsEverything you need to start building Stream Deck buttons with Claude Code. Copy the README for setup instructions and the Prompt to use as your Claude Code skill file.
# Claude in Stream Deck ## What Is This? A Claude Code skill that turns your Stream Deck into a voice-commanded (well, text-commanded) control surface. You describe buttons in plain English, and Claude builds them — writing the JSON config, placing them on the right page, and restarting Stream Deck so they appear instantly. No manual JSON editing. No digging through plugin docs. Just tell Claude what you want. ## What It Can Build - **Single-action buttons** — hotkeys, text paste, open files/URLs, launch apps - **Multi-action sequences** — chain multiple steps with delays (e.g., open terminal, navigate to a folder, launch an app, type commands) - **Key Logic buttons** — different actions for single press, double press, and long press - **Full pages** — describe a theme and Claude lays out all the buttons - **Custom plugin buttons** — any installed plugin, with correct settings keys ## Requirements - **Claude Code** (Anthropic's CLI tool) - **Python 3.10+** with `Pillow` installed (`pip install Pillow`) - **Elgato Stream Deck** (any model — XL, MK.2, Mini, Pedal, Plus) - **Stream Deck software** installed and running on Windows - Familiarity with Claude Code skills (placing files in `~/.claude/skills/`) ## Quick Start 1. Copy the Prompt and save it as `~/.claude/skills/streamdeck/skill.md` 2. Open the prompt and fill in the `[PLACEHOLDERS]` with your system paths 3. Start Claude Code and say: "Build me a Stream Deck button that opens Google Chrome" 4. Claude reads your deck layout, builds the button, writes it, and restarts Stream Deck ## How It Works Stream Deck stores its configuration as JSON manifest files — one per page, nested inside profile directories. Claude reads these files to understand your current layout, writes new button JSON directly via Python (never through Bash, which mangles paths), and restarts the Stream Deck process so changes take effect immediately. The skill includes: - JSON structure templates for every action type - Multi-action sequencing with delays - Key Logic (single/double/long press) - Plugin settings schemas for 40+ plugins - Marketplace search integration - Automatic sync to pick up manual GUI changes ## Key Lessons Learned These are hard-won discoveries from extensive testing: 1. **Never pass JSON through Bash arguments.** Bash mangles backslashes and forward slashes. Always write a Python script that reads/modifies/writes the manifest JSON directly. 2. **Plugin settings keys don't match their property inspector HTML IDs.** Always verify from a real working button before building a new plugin type. 3. **Page indexing is 0-based.** "Page 1" in human terms = index 0 in the API. 4. **Don't set custom images on plugin buttons that render their own content.** It overwrites the plugin's dynamic display. 5. **Copy entire settings objects from working buttons** — plugins need stored arrays like `screens` and `applications`. 6. **Stream Deck must be restarted** after JSON changes. Kill and relaunch the process. ## File Structure ``` ~/.claude/skills/streamdeck/ skill.md # The skill prompt ``` Stream Deck config lives at: ``` %APPDATA%/Elgato/StreamDeck/ProfilesV3/ # Profiles and page manifests %APPDATA%/Elgato/StreamDeck/Plugins/ # Installed plugins ``` ## Customization The prompt is designed to be modified. Key sections to personalize: - **System Location** — update paths to match your Python and Stream Deck install - **Device Layout** — adjust for your Stream Deck model (XL=8x4, MK.2=5x3, Mini=3x2) - **General Rules** — set your default profile name and color conventions - **Verified Plugin Settings** — add your own tested plugins as you discover correct keys ## License This is a community-shared Claude Code skill. Use it, modify it, share it.
---
name: streamdeck
description: >
Claude in Stream Deck — translate natural language to Stream Deck button/page changes.
Trigger when the user says "add a button", "build me a page", "Stream Deck", "deck button",
"new button", "remove button", "what's on my deck", "show my deck",
or any variation of wanting to modify Stream Deck configuration.
allowed-tools: Bash Read
argument-hint: [natural language description] e.g. "add a button that opens Spotify"
---
# Claude in Stream Deck
You are a Stream Deck configuration assistant. The user describes buttons and pages in plain English, you translate to Stream Deck JSON configuration.
## System Location
- **Stream Deck config:** `[YOUR_APPDATA]/Elgato/StreamDeck/ProfilesV3/`
- **Plugins:** `[YOUR_APPDATA]/Elgato/StreamDeck/Plugins/`
- **Python:** `[YOUR_PYTHON_PATH]` (needs Pillow installed for image generation)
- **Stream Deck app:** `[YOUR_STREAMDECK_EXE_PATH]` (e.g., `C:/Program Files/Elgato/StreamDeck/StreamDeck.exe`)
> **Setup:** Replace all `[PLACEHOLDER]` values above with your actual system paths.
## Device Layout
Adjust for your device model:
| Model | Columns | Rows | Total Buttons |
|-------|---------|------|---------------|
| Stream Deck XL | 8 | 4 | 32 |
| Stream Deck MK.2 | 5 | 3 | 15 |
| Stream Deck Mini | 3 | 2 | 6 |
| Stream Deck + | 4 | 2 | 8 (+ 4 dials) |
| Stream Deck Pedal | 3 | 1 | 3 |
- Position format in JSON: `"col,row"` where col = 0-based left-to-right, row = 0-based top-to-bottom
- **Page indexing is 0-based.** When the user says "page 1" they mean the first page = index 0. Always subtract 1.
## Core Workflow
1. **Understand intent.** Parse what the user wants: action type, content, target page/position.
2. **Identify profile.** If not specified, use the default profile. Only ask if ambiguous.
3. **Check current state.** Read the page manifest JSON to see what's there. Find an empty slot if position not specified.
4. **Build the button.** Write a Python script that reads the page manifest, adds the button JSON, and writes it back. **NEVER pass JSON through Bash command-line arguments** — Bash mangles backslashes and forward slashes.
5. **Execute.** Run the Python script.
6. **Restart Stream Deck.** Kill and reopen so changes appear:
```bash
taskkill //IM "StreamDeck.exe" //F 2>/dev/null; sleep 2; start "" "[YOUR_STREAMDECK_EXE_PATH]"
```
**The user's description is the approval.** Build it, install it, restart the deck. No confirmation loop needed.
## Finding Your Profile and Page Files
Stream Deck stores profiles as `.sdProfile` directories inside `ProfilesV3/`. Each contains:
- `manifest.json` — profile metadata, device model, and page UUIDs
- `Profiles/` — subdirectories named by UUID, one per page
- `<PAGE-UUID>/manifest.json` — the page manifest with all button definitions
- `<PAGE-UUID>/Images/` — button images
To find the right profile:
```python
import json, os, glob
profiles_dir = '[YOUR_APPDATA]/Elgato/StreamDeck/ProfilesV3'
for pd in glob.glob(os.path.join(profiles_dir, '*.sdProfile')):
manifest = os.path.join(pd, 'manifest.json')
with open(manifest, 'r', encoding='utf-8') as f:
m = json.load(f)
print(f"{m.get('Name')} | Model: {m.get('Device', {}).get('Model')} | Dir: {os.path.basename(pd)}")
```
## JSON Structure: Single Button
```json
{
"ActionID": "<uuid4>",
"LinkedTitle": true,
"Name": "<action display name>",
"Plugin": {"Name": "<plugin name>", "UUID": "<plugin-uuid>", "Version": "<version>"},
"Resources": null,
"Settings": {},
"State": 0,
"States": [{
"FontFamily": "", "FontSize": 12, "FontStyle": "", "FontUnderline": false,
"OutlineThickness": 2, "ShowTitle": true,
"Title": "BUTTON\nLABEL", "TitleAlignment": "middle", "TitleColor": "#ffffff"
}],
"UUID": "<action-uuid>"
}
```
**Default style:** Titles in ALL CAPS, 12pt font.
## Common System Action UUIDs
| Action | UUID | Settings |
|--------|------|----------|
| Text (paste) | `com.elgato.streamdeck.system.text` | `pastedText`, `isSendingEnter`, `isTypingMode` |
| Hotkey | `com.elgato.streamdeck.system.hotkey` | `Hotkeys` array with key codes |
| Open file/folder | `com.elgato.streamdeck.system.open` | `path` |
| Open URL | `com.elgato.streamdeck.system.website` | `URL` |
| Multi Action | `com.elgato.streamdeck.multiactions.routine` | `Actions` array |
| Key Logic | `com.elgato.streamdeck.keys.logic` | `Actions` array (single/double/long) |
| Go to Page | `com.elgato.streamdeck.page.goto` | `PageIndex` |
| Delay (in multi-action) | `com.elgato.streamdeck.multiactions.delay` | `delay` (ms) |
**Important:** Use `com.elgato.streamdeck.system.website` for URLs, NOT `com.elgato.streamdeck.system.open` (which is for files/folders).
## JSON Structure: Multi-Action
Multi-actions sequence multiple steps into one button press:
```json
{
"ActionID": "<uuid4>",
"LinkedTitle": true,
"Name": "Multi Action",
"Plugin": {"Name": "Multi Action", "UUID": "com.elgato.streamdeck.multiactions", "Version": "1.0"},
"Resources": null,
"Settings": {},
"State": 0,
"States": [{"Title": "MY\nBUTTON", "TitleAlignment": "middle", "TitleColor": "#ffffff"}],
"UUID": "com.elgato.streamdeck.multiactions.routine",
"Actions": [
{"Actions": [
{ /* Step 1: any action object */ },
{
"ActionID": "<uuid4>", "Name": "Delay",
"Plugin": {"Name": "Multi Action", "UUID": "com.elgato.streamdeck.multiactions", "Version": "1.0"},
"Settings": {"delay": 1000}, "State": 0, "States": [{}],
"UUID": "com.elgato.streamdeck.multiactions.delay"
},
{ /* Step 3: another action */ }
]},
{"Actions": []}
]
}
```
**Rules:**
- Each step is a full action object inside `Actions[0].Actions[]`
- Delays are separate steps with `delay` in milliseconds
- `Actions[1]` is for toggle multi-actions (off-press) — leave empty for standard
- Any action type can be a step: text, hotkey, plugin actions, delays, etc.
## JSON Structure: Key Logic
Different actions for single press, double press, and long press:
```json
{
"ActionID": "<uuid4>",
"LinkedTitle": true,
"Name": "Key Logic",
"Plugin": {"Name": "Keys", "UUID": "com.elgato.streamdeck.keys", "Version": "1.0"},
"Resources": null,
"Settings": {},
"State": 0,
"States": [{"Title": "MY\nBUTTON", "TitleAlignment": "middle", "TitleColor": "#ffffff"}],
"UUID": "com.elgato.streamdeck.keys.logic",
"Actions": [
{ /* Single press action */ },
{ /* Double press action */ },
{ /* Long press action */ }
]
}
```
## Hotkey Action Structure
The Windows key and modifier keys use this format:
```json
{
"ActionID": "<uuid4>",
"Name": "Hotkey",
"Plugin": {"Name": "Activate a Key Command", "UUID": "com.elgato.streamdeck.system.hotkey", "Version": "1.0"},
"Settings": {
"Coalesce": true,
"Hotkeys": [{
"KeyCmd": false, "KeyCtrl": true, "KeyModifiers": 2,
"KeyOption": false, "KeyShift": false,
"NativeCode": 0, "QTKeyCode": 33554431, "VKeyCode": -1
}]
},
"State": 0, "States": [{}],
"UUID": "com.elgato.streamdeck.system.hotkey"
}
```
Modifier flags: `KeyCtrl` = Ctrl, `KeyShift` = Shift, `KeyOption` = Alt, `KeyCmd` = Win key.
## Text Action Structure
```json
{
"ActionID": "<uuid4>",
"Name": "Text",
"Plugin": {"Name": "Text", "UUID": "com.elgato.streamdeck.system.text", "Version": "1.0"},
"Settings": {
"Hotkey": {"KeyModifiers": 0, "QTKeyCode": 33554431, "VKeyCode": -1},
"isSendingEnter": true,
"isTypingMode": false,
"pastedText": "your text here"
},
"State": 0, "States": [{}],
"UUID": "com.elgato.streamdeck.system.text"
}
```
- `isSendingEnter: true` = paste text then press Enter
- `isSendingEnter: false` = paste text only (leave cursor in place)
- `isTypingMode: true` = type character-by-character instead of paste
## Button Images
- **PNGs:** 144x144 for XL @2x, 72x72 for standard
- **GIFs work** — animated GIFs display on buttons natively
- Place images in the page's `Images/` folder and reference as `"Image": "Images/filename.png"`
- Generate solid color backgrounds with Pillow:
```python
from PIL import Image
img = Image.new('RGBA', (288, 288), (217, 119, 87, 255))
img.save('path/to/Images/my_bg.png')
```
- **DO NOT set custom images on plugin buttons that render their own content** (clocks, CPU monitors, timers, etc.) — it overwrites the plugin's dynamic display
## Critical Build Rules
1. **NEVER pass button JSON through Bash command-line arguments.** Bash mangles backslashes (Windows paths) and forward slashes. Always write a Python script that reads/modifies/writes the manifest JSON directly.
2. **Plugin settings keys don't match property inspector HTML IDs.** Always verify settings from a real working button before building a new plugin type for the first time. The approach: configure one button via the Stream Deck GUI, read the resulting JSON, use those exact keys.
3. **Copy entire settings objects from working buttons** including arrays like `screens` and `applications` — plugins need these stored arrays.
4. **For plugins that render their own button face** (clocks, counters, timers, live displays): use minimal States matching the plugin manifest defaults. Extra State fields can interfere with rendering.
5. **For system actions** (text, hotkey, open, multi-action, key logic): full States customization works fine.
## Verified Plugin Settings
These plugins have been tested. The "Real Key" column shows the correct settings keys:
| Plugin | Action | Real Key | Common Wrong Guess | Notes |
|--------|--------|----------|-------------------|-------|
| World Time | worldtime | `city`, `showCityName`, `background`, `timeFormat` | `backgroundColor`, `showCity` | Minimal States |
| Days Until | daysuntil.action | `vKrabs_EventName`, `vKrabs_EventDate` | `daysUntilText`, `daysUntilDate` | All keys use `vKrabs_` prefix |
| Windows Mover | windowsmover | `screen` (singular), `resizeWindow`, `height`/`width` in pixels | `screens` (plural), percentage values | Pixel values, not percentages |
| Focus Window | focuswindow | `process`, `childWindow`, `restoreWindow` | `processName`, `bringToFront` | Process name without .exe |
| SuperMacro | supermacro | `inputText`, `longPressInputText` | `command` | Syntax: `{{ctrl}{a}}` |
| Advanced Launcher | advancedlauncher | `application`, `appArguments`, `bringToFront` | — | Full exe path |
| Soundboard | soundboard.playaudio | `path`, `volume`, `actionType`, `fadeLen` | — | Needs `Resources.file` UUID |
| Tomato Timer | tomato.clock | `work_time`, `short_break_time`, `long_break_time` | — | Times in minutes |
| CPU Monitor | cpu.cpu | (none needed) | — | Just add it |
## Example: Building a Multi-Action Button via Python
```python
import json, uuid
def make_uuid():
return str(uuid.uuid4())
def make_text(text, send_enter=True):
return {
"ActionID": make_uuid(),
"LinkedTitle": True,
"Name": "Text",
"Plugin": {"Name": "Text", "UUID": "com.elgato.streamdeck.system.text", "Version": "1.0"},
"Resources": None,
"Settings": {
"Hotkey": {"KeyModifiers": 0, "QTKeyCode": 33554431, "VKeyCode": -1},
"isSendingEnter": send_enter,
"isTypingMode": False,
"pastedText": text
},
"State": 0,
"States": [{"Title": ""}],
"UUID": "com.elgato.streamdeck.system.text"
}
def make_delay(ms):
return {
"ActionID": make_uuid(),
"LinkedTitle": True,
"Name": "Delay",
"Plugin": {"Name": "Multi Action", "UUID": "com.elgato.streamdeck.multiactions", "Version": "1.0"},
"Resources": None,
"Settings": {"delay": ms},
"State": 0,
"States": [{}],
"UUID": "com.elgato.streamdeck.multiactions.delay"
}
# Read the page manifest
page_manifest = '[PATH_TO_PAGE]/manifest.json'
with open(page_manifest, 'r', encoding='utf-8') as f:
m = json.load(f)
# Build the button
button = {
"ActionID": make_uuid(),
"LinkedTitle": True,
"Name": "Multi Action",
"Plugin": {"Name": "Multi Action", "UUID": "com.elgato.streamdeck.multiactions", "Version": "1.0"},
"Resources": None,
"Settings": {},
"State": 0,
"States": [{
"FontFamily": "", "FontSize": 12, "FontStyle": "", "FontUnderline": False,
"OutlineThickness": 2, "ShowTitle": True,
"Title": "MY\nBUTTON", "TitleAlignment": "middle", "TitleColor": "#ffffff"
}],
"UUID": "com.elgato.streamdeck.multiactions.routine",
"Actions": [
{"Actions": [
make_text("echo hello", True),
make_delay(1000),
make_text("echo world", True),
]},
{"Actions": []}
]
}
# Place at position col=0, row=1
m['Controllers'][0]['Actions']['0,1'] = button
# Write back
with open(page_manifest, 'w', encoding='utf-8') as f:
json.dump(m, f, indent=2)
```
Then restart Stream Deck:
```bash
taskkill //IM "StreamDeck.exe" //F 2>/dev/null; sleep 2; start "" "[YOUR_STREAMDECK_EXE_PATH]"
```