Full Claude control on your Elgato Stream Deck. One key per project, glowing with that
project's live Claude Code status — working, needs-you, done — plus an attention doorbell,
usage gauges, and deck-answerable permission prompts. Add app / URL / command shortcut keys,
give any key a colour, emoji, or the app's real logo, and build your whole board by talking
to it (jetstream chat) — live, no re-import. See SPEC.md.
Requires macOS 12+ and the Stream Deck app 7.1+. Windows isn't supported yet (the plugin manifest is macOS-only).
-
Claude Code, logged in with your subscription (
claude→/login). LeaveANTHROPIC_API_KEYunset: Jetstream strips it from anything it spawns so a keypress can never silently bill the metered API. -
Jetstream:
npm i -g @pimmesz/jetstream, thenjetstream install— it hands the packed plugin to the Stream Deck app; approve the install prompt there. (Updating?jetstream update— it fetches the latest published package from npmjs.org and reinstalls the plugin, so a barenpm i -gbehind a stale mirror can't strand you on an old version; override the registry withJETSTREAM_REGISTRY. If thejetstreamyou ran is not the copy npm updates, it prints a note saying where each one is.) That's it: on first launch (recorded in a marker next toprojects.json) the plugin wires its hooks into~/.claude/settings.json, backing the file up first: the per-project status hook (lights the board), the permission hook (lets Approve/Deny keys answer Claude's permission prompts from the deck; while the plugin runs, an unanswered prompt falls back to Claude's own dialog after ~90s) and the stop gate, aPreToolUsehook that lets the deck's stop keys end the current turn without ending the session (it reads one local file per tool call). No terminal step. Restart any runningclaudesessions to pick the hooks up. The plugin runs under the Stream Deck app, which does not see your shell profile, so it uses theCLAUDE_CONFIG_DIRandCODEX_HOMEyour shell had the last time you ranjetstream install,update,setup,hooks install,initorchat(from the plugin's next start; a value set for apps withlaunchctl setenvwins). If you set or changeCLAUDE_CONFIG_DIRafter the first launch, runjetstream hooks installfrom that shell (add--tool-detailfor per-tool detail) so the hooks land where Claude reads them. The hooks are wired once per hook-set version: hooks you remove fromsettings.jsonstay removed until an update adds a new hook, which re-wires the set once. Re-wire any time withjetstream setup. The usage/statusline hook is installed automatically on first launch if you have no statusline yet; if another tool already owns your statusline, hand the slot over withjetstream hooks install --replace-statusline(otherwise the usage gauge stays blank).With the plugin installed (step 2 above), set up your fleet — two ways, both via the
jetstreamCLI:jetstream chat # conversational — "3 repos in ~/dev: …", "add a Telegram key at a8" jetstream init # guided wizard — repos, theme/timings, a ready-made layout
chatlets you describe repos AND arrange keys in plain English (add app/URL/run shortcuts, recolour, rename, set emoji/logo icons, ask what is on a key). It shows each key it will change before you apply, and stays open for the next request. Pressing an empty Jetstream key opens chat for that key ("What should key c3 do?"), so a one-word answer like "Telegram" fills it. Edits to Jetstream keys apply live. A key from another plugin (a Philips Hue toggle you already placed somewhere, say) is copied by chat, and anything that cannot go live is written into your current board while Stream Deck restarts for a few seconds, after a backup to~/.config/jetstream/profile-backups/. When chat finds your Jetstream board it creates no extra profile; without one it opens aJetstream-Customprofile from~/Downloadsfor you to import once.initasks for your repos (or scans a folder), your theme and timings, writesprojects.json(see below), wires the hooks — and can prebuild a ready-made key layout for your deck (Mini / MK.2 / XL) as aJetstream.streamDeckProfilein~/Downloadsyou double-click to import. The import installs it as a new profile on the device you pick in the dialog; your existing layout is never touched. (The layout file mirrors the profile format Elgato's own plugins ship, but treat it as experimental — dragging keys by hand always works.)The smaller pieces still exist:
setup(hooks + a starterprojects.jsontemplate),hooks install(only the hooks; the oldbin/hooks-install.jsstill works;--tool-detailadds the active-tool hooks,--replace-statuslinetakes the statusline slot from another tool so the usage gauge works),update(bump + reinstall in one step), anddoctor(read-only health check for when the board isn't lighting up).
Drag keys onto your deck: Project status (set a name + project path per key;
short-press opens the project folder in your editor (VS Code, then Cursor, else Finder),
long-press interrupts the session; done keys show the
change size, done 4m · +120/-40), Fleet roll-up (one always-visible key counting the whole
fleet — 3w 1! 2✓ — coloured by the worst state present, so "is anything waiting on me?" is
answerable even when projects outnumber keys), Attention (flashes if a request goes
unanswered), Usage gauge (5h/7d used + the sooner reset, used·resets 3h33m, or the reading's age once it is over 15 minutes old, used·20m old, since use on claude.ai or another machine is not in it until Claude Code's next turn; a second gauge can show
your Codex limits, jetstream chat → "put a Codex usage gauge at b3"),
Approve / Deny (place one of each — they answer
the oldest pending Claude permission request straight from the deck; no press within ~90s → Claude
falls back to its normal dialog), and Jetstream settings (a press opens jetstream doctor;
its inspector toggles colour-blind mode and sets escalation/long-press/refresh). A waiting Project key reads
approve on deck when you can answer it from the deck, or answer in Claude when the question needs a typed reply.
The Approve and Deny keys show the project the request came from. A corner * means the command is
longer than the key can show (about 18 characters, tool name included), so read it in Claude first.
Hold Approve for 1.5 seconds (the key turns red and asks auto-allow <tool>?) to approve and
also auto-approve that session's later requests with no press. A Bash rule covers only the exact
same command run the same way, so a rule armed inside the sandbox never covers that command with
the sandbox disabled; for any other tool a rule covers every call of that tool. A chained command,
one with ;, |, ||, &&, a lone &, a backtick, a newline, or any (, ), {, } or [[, is
never armed, and when it does not fit the key or spans more than one line it is left to Claude's
own dialog. Rules end with the session or a plugin restart, and the idle Approve key shows how many
are on.
The board reads without relying on colour: each key's sub-line names its state in words (done 4m,
approve on deck), and a corner glyph marks the exceptions — a stall, a failure, or a working key showing
its tool (Bash · 12m, where the glyph carries "working"). The settings key's high-contrast theme also
swaps the done green for blue, so working (orange) and done never rely on a red/green pair.
Keys you drag by hand work on any device, as many as it has (Mini 6, MK.2 15, XL 32, Neo). No fixed
layout is required: drag keys wherever you like. The generated layouts know the Mini, MK.2 and XL:
jetstream init can prebuild a starting layout for them, and jetstream chat and jetstream board
work on their boards. Each Project key holds its own name+path, so everyone's board is their
own. (On the Stream Deck +, drag the Fleet dial onto an encoder: rotate scrubs the
fleet, the touchscreen shows the current project, a tap or short-press opens it, a long-touch or long-press interrupts.)
Define your whole fleet in one place instead of a placed key per repo — jetstream init
builds this file for you, or write it by hand. On macOS the plugin reads
~/.config/jetstream/projects.json (%APPDATA%\jetstream\projects.json on Windows) at startup;
init, setup and chat create it there. The plugin runs under the Stream Deck app, which does not
see an XDG_CONFIG_HOME set in your shell profile, so a file under $XDG_CONFIG_HOME/jetstream/ is
found by the CLI and doctor but not by the board:
{
"projects": [{ "id": "falcon", "name": "Falcon", "path": "/Users/you/falcon" }]
}The Fleet roll-up and Attention doorbell then cover every repo in the file, so placed
Project keys become optional focused jump-to buttons rather than the only way the plugin learns
your repos. An optional "settings" block presets the plugin config: theme, longPressMs,
usageRefreshSec, escalateAfterSec, plus the two destructive-key opt-ins allowRunKeys and
allowStopKeys. Run jetstream doctor to check the file is parseable.
Select a Slot key in Stream Deck to configure an app or folder, URL, or project, or to change its label and colour. Choose Save to apply, or Cancel to reload the saved settings. Other existing slot kinds support label and colour edits; Run and Stop-all slots are read-only in this form. If chat changes the key while the form is open, Save refuses the stale edit: choose Cancel to load the current settings before editing again. No Claude login is needed.
Slot-based Stop-all and Run keys are OFF by default and do nothing until you opt in. These are
the kinds jetstream chat (or a /slot request) can place; the standalone Stop all action you
drag onto the deck yourself works as placed. The plugin's loopback endpoint needs its token for key
edits and permission answers, so the opt-in is defence in depth: a planted key cannot stop every
running turn or run a command until you turn it on. (A stop ends the current turn; the session stays
open.) An app key whose target would run rather than open counts as a Run key too and shows
run off: a script or launcher file (.command, .sh, .py, .jar, .webloc and the like), an
executable file, a Finder alias, or a URL. Turn them on deliberately:
{
"projects": [{ "id": "falcon", "name": "Falcon", "path": "/Users/you/falcon" }],
"settings": { "allowStopKeys": true, "allowRunKeys": true }
}Working keys can show the current tool (Bash · 12m) instead of just working 12m. It adds the
status hook to PreToolUse, PostToolUse and PostToolUseFailure, on top of the default stop gate,
so each tool call starts a few more hook processes. That's why it's opt-in:
node "<plugin folder>/bin/jetstream.js" hooks install --tool-detail- The usage gauge shows your interactive 5h/7d subscription windows.
- The Codex usage gauge asks Codex for its limits every 5 minutes and on a press
(
codex app-server, using Codex's own login; Jetstream never reads its credentials), and also reads the limits Codex writes into its session logs (~/.codex/sessions), whichever is newer. Without acodexthe plugin can find, it reads only the logs, so runcodexonce for data. ACODEX_HOMEset in your shell profile is recorded each time you runjetstream install,update,setup,hooks install,initorchatfrom that shell, and the plugin uses it from its next start (quit and reopen Stream Deck to apply it now). A Marketplace-only install never runs the CLI, so there set it for apps instead (launchctl setenv CODEX_HOME <dir>, then restart Stream Deck; this lasts until you log out), or link the folder once:mkdir -p ~/.codex && ln -s <dir>/sessions ~/.codex/sessions(only while~/.codex/sessionsdoes not exist). The long window is labelled by its length:7d, or30don a plan with a monthly limit. - Watching status costs nothing; the hooks only talk to the plugin locally
(
127.0.0.1, never the network). The status hook reports lifecycle events; the permission hook additionally holds a pending prompt briefly so a deck key can answer it (falling back to Claude's own dialog after ~90s when unanswered).
Elgato ships an official MCP server (npm install -g @elgato/mcp-server, Stream Deck app
7.4+, enable MCP Deck in Preferences → General) that lets an AI assistant trigger
actions — but only ones you've placed on the dedicated MCP Actions profile; your other
profiles stay private. Jetstream keys work there like any action, but never place Approve or
Deny on the MCP Actions profile: an agent could approve its own prompt. Two caveats: the MCP
server can trigger keys, never build layouts (that's what jetstream init is for), and its
actions fire without per-call confirmation, so use the stdio transport, skip the ngrok/HTTP modes,
and only place keys you'd let an AI press.
pnpm install # install workspace deps
pnpm check # build the shared cores, then typecheck + test every package, then lint (prettier --check)
(cd packages/jetstream && npm pack --dry-run) # pre-release: runs prepack and lists the npm tarball, as CI does
pnpm build # build:cores + bundle the plugin into the .sdPlugin
pnpm --filter '@pimmesz/jetstream' run validate # Elgato manifest validation
pnpm --filter '@pimmesz/jetstream' run repack # rebuild, validate, and produce the .streamDeckPlugin (pack --force)