Hardware controls for Claude Code, Codex, Pi, JCode, and any other terminal agent.
Standalone, local, and evidence-backed.
Landing page · Install · SKILL.md · Changelog
English · 한국어
A single portable skill and reference runtime for turning Stream Deck into a local cockpit for Claude Code, Codex, Pi, JCode, and other terminal-based agents.
No MCP server is required. The default implementation uses a loopback-only Python daemon, predeclared local commands, and either generated launchers or a Stream Deck SDK plugin.
claude plugin marketplace add cskwork/streamdeck-agent-cockpit
claude plugin install streamdeck-agent-cockpit@streamdeck-agent-cockpitCodex, Gemini CLI, Cursor, OpenCode, Amp, Antigravity, and manual installs are all in INSTALL.md.
┌──────────────────────────────────────────────────────────────┐
│ Stream Deck │
│ A. built-in Open action → generated launcher │
│ B. local Agent Cockpit plugin → dynamic key/dial UI │
└───────────────────────┬──────────────────────────────────────┘
│ authenticated localhost API
┌───────────────────────▼──────────────────────────────────────┐
│ cockpitd │
│ config · command allowlist · state TTL · adapter registry │
└───────────────────────┬──────────────────────────────────────┘
│ argv execution, never remote MCP
┌───────────────────────▼──────────────────────────────────────┐
│ tmux / terminal / agent CLI │
│ Claude Code · Codex · Pi · JCode · custom commands │
└──────────────────────────────────────────────────────────────┘
streamdeck-mcp and AgentDeck are not installed, imported, called, or expected by the runtime. They may be studied as prior art, but the skill's operation does not depend on them.
| Mode | Requirements | Best for | Limitation |
|---|---|---|---|
| Launcher-only | Stream Deck app, Python 3.9+, configured terminal tools | Static tap-to-launch/focus actions | No live label/icon updates or dial events |
| Native plugin | Above plus current official Stream Deck SDK toolchain | Live state, dynamic visuals, hold, dials, Property Inspector | Requires building/installing a local plugin |
The daemon and CLI use only Python's standard library.
streamdeck-agent-cockpit/
├── README.md · INSTALL.md · CHANGELOG.md · LICENSE · VERSION
├── .claude-plugin/ # Claude Code plugin + marketplace manifests
├── .codex-plugin/ # Codex plugin manifest
├── .agents/plugins/ # agents marketplace manifest
├── .cursor/skills/… # Cursor mirror of SKILL.md
├── gemini-extension.json # Gemini CLI extension (context: GEMINI.md)
├── docs/index.html # landing page (GitHub Pages)
└── skills/streamdeck-agent-cockpit/
├── SKILL.md
├── assets/
│ ├── cockpit.example.json
│ ├── cockpit.live-sessions.example.json
│ └── cockpit.schema.json
├── bin/
│ ├── cockpitd.py
│ ├── cockpitctl.py
│ ├── focus_tmux.py
│ ├── report_state.py
│ ├── slotclaims.py # slot bookkeeping for attached sessions
│ ├── claim_probe.py # coarse probe for a claimed slot
│ ├── focus_terminal.py # iTerm2 / Apple Terminal focus by tty
│ ├── claude_hook.py # Claude Code hook → semantic state
│ └── install_claude_hooks.py # append-only hook registration
├── scripts/
│ ├── generate_launchers.py
│ ├── install_runtime.py
│ ├── install_skill.py
│ ├── probe_environment.py
│ ├── smoke_test.py
│ └── validate_cockpit.py
├── templates/streamdeck-plugin/
├── references/
├── evals/
└── tests/
Every command below that starts with python3 scripts/… runs from
skills/streamdeck-agent-cockpit/.
For plugin-manager installs (Claude Code, Codex, Gemini CLI, npx skills, agy), see
INSTALL.md. To copy the skill into a skills directory yourself:
cd skills/streamdeck-agent-cockpit
# preview all supported locations
python3 scripts/install_skill.py --target all --dry-run
# install
python3 scripts/install_skill.py --target allSupported targets:
| Target | Destination |
|---|---|
claude |
~/.claude/skills/streamdeck-agent-cockpit |
agents |
~/.agents/skills/streamdeck-agent-cockpit |
jcode |
convenience default ~/.jcode/skills/streamdeck-agent-cockpit; override with --destination when the installed build uses a different discovery path |
all |
all unique destinations above |
Use --destination /verified/local/skills/path when a harness uses another discovery directory. Use --mode symlink for an editable development install. Existing destinations are refused unless --force is supplied; forced replacement first creates a timestamped backup.
cd skills/streamdeck-agent-cockpit
python3 scripts/probe_environment.py --json # inspect before assuming anything
python3 scripts/install_runtime.pyThis creates:
~/.agent-cockpit/
├── bin/
├── cockpit.json
├── state.json # created as needed
└── token # generated by the daemon with mode 0600
The installer does not register a startup service or modify Stream Deck profiles.
Edit ~/.agent-cockpit/cockpit.json. The included example defines one named tmux session for each agent:
session.claude.mainsession.codex.mainsession.pi.mainsession.jcode.main
Verify the actual command names and flags installed on the machine:
claude --help
codex --help
pi --help
jcode --helpThen validate:
python3 ~/.agent-cockpit/bin/validate_cockpit.py \
~/.agent-cockpit/cockpit.jsonpython3 ~/.agent-cockpit/bin/cockpitd.py \
--config ~/.agent-cockpit/cockpit.jsonIn another terminal:
python3 ~/.agent-cockpit/bin/cockpitctl.py \
--config ~/.agent-cockpit/cockpit.json health
python3 ~/.agent-cockpit/bin/cockpitctl.py \
--config ~/.agent-cockpit/cockpit.json controlsInvoke a configured tap:
python3 ~/.agent-cockpit/bin/cockpitctl.py \
--config ~/.agent-cockpit/cockpit.json \
invoke session.claude.main --gesture tapA hold-confirmed interrupt is explicit:
python3 ~/.agent-cockpit/bin/cockpitctl.py \
--config ~/.agent-cockpit/cockpit.json \
invoke session.claude.main --gesture longPress --confirmGenerate platform launchers:
python3 ~/.agent-cockpit/bin/generate_launchers.py \
--config ~/.agent-cockpit/cockpit.json \
--output ~/.agent-cockpit/launchersIn the Stream Deck application, place a built-in Open action and select the launcher for the desired control. This path is fully independent and does not compile a plugin. It supports tap actions only.
Use the current official Stream Deck SDK to create a local plugin scaffold. Apply the files in skills/streamdeck-agent-cockpit/templates/streamdeck-plugin/ as described in its README. Each action instance stores only a logical controlId and contacts the local daemon for state and invocation.
The plugin must not read or rewrite Stream Deck's internal profile database. Users place the action normally or install an optional profile owned by this plugin.
It also cannot generically inspect or invoke arbitrary third-party plugin actions. Combine those actions manually in Stream Deck, or connect to the underlying service only when that service/plugin exposes a documented local API.
Infrastructure can verify that a tmux session exists, but that does not prove an agent is running, waiting, blocked, or done. Without an event source, the UI shows only coarse state.
An agent hook or workflow can report semantic state:
python3 ~/.agent-cockpit/bin/report_state.py \
--config ~/.agent-cockpit/cockpit.json \
--session session.codex.main \
--state running \
--label "Reviewing changes" \
--ttl 180Later:
python3 ~/.agent-cockpit/bin/report_state.py \
--config ~/.agent-cockpit/cockpit.json \
--session session.codex.main \
--state needs_attention \
--label "Approval required" \
--ttl 600When a report expires, the daemon falls back to coarse adapter state. A percentage is accepted only when explicitly reported by a real workflow.
The sections above cover sessions the cockpit launches. Agent work usually already runs in terminal tabs you opened yourself, and those can appear on the deck too — with live state, and without moving them into tmux.
The daemon only accepts reports for sessions declared in cockpit.json, so a running session cannot register itself. Instead, predeclare a fixed number of slots and let a Claude Code hook bind live sessions to them. Start from cockpit.live-sessions.example.json, which pairs four attached slots with one tmux launch control:
cp skills/streamdeck-agent-cockpit/assets/cockpit.live-sessions.example.json \
~/.agent-cockpit/cockpit.json
python3 ~/.agent-cockpit/bin/validate_cockpit.py ~/.agent-cockpit/cockpit.jsonRegister the hook bridge — append-only, idempotent, and previewable:
python3 ~/.agent-cockpit/bin/install_claude_hooks.py --dry-run
python3 ~/.agent-cockpit/bin/install_claude_hooks.pyBack up your settings file before the first write. State then comes from hook events only:
| Hook event | Key shows |
|---|---|
SessionStart, Stop |
IDLE |
UserPromptSubmit |
RUN |
Notification (permission, idle, elicitation) |
CHECK |
SessionEnd |
slot released, key returns to OFF |
Want finer detail? --extended also registers the tool, permission, elicitation, subagent, task, and compaction events, so a key can show BLOCKED when permission is denied and FAILED when a turn ends in error. The trade is that the bridge then runs on every tool call, so start without it:
python3 ~/.agent-cockpit/bin/install_claude_hooks.py --extended --dry-runEach key label carries the session's project directory name, never prompt text or model output.
Tapping a slot focuses the owning pane. On macOS focus_terminal.py supports iTerm2 and Apple Terminal, matching on the tty recorded when the slot was claimed. Windows Terminal has no scriptable tty, so it is addressed by exact tab title instead:
python3 ~/.agent-cockpit/bin/focus_terminal.py --tab-title "Claude · Main"Known limits of this path, all deliberate:
- Sessions already running when you install the bridge stay invisible until they restart.
- Slots are finite. When all are held by live sessions, a new one is ignored rather than evicting someone.
- No interrupt gesture on attached slots. There is no supported way to send a scoped
Ctrl-Cthrough terminal automation, so interrupt stays on tmux-backed sessions wheretmux send-keysis exact. - Slot discovery is macOS only. Binding a running session to a slot depends on
psancestry and AppleScript. The Windows Terminal focus path works, but the title is something you set rather than something discovered, so it proves nothing about the session behind that tab. - Terminal titles are never scraped. They look like a usable signal but cannot separate "thinking" from "waiting for approval".
From skills/streamdeck-agent-cockpit/:
python3 -m compileall -q bin scripts tests
python3 -m unittest discover -s tests -v
python3 scripts/validate_cockpit.py assets/cockpit.example.json
python3 scripts/validate_cockpit.py assets/cockpit.live-sessions.example.json
python3 scripts/smoke_test.pyPhysical-device behavior still requires testing in the Stream Deck application and on the target terminal. See skills/streamdeck-agent-cockpit/references/verification.md.
- Launcher-only mode cannot display live state, distinguish hold, or process dial input.
- The reference plugin template must be adapted and built with the current official SDK; no prebuilt plugin binary is included.
- The official plugin boundary does not provide a safe generic API for editing arbitrary profiles or controlling unrelated third-party plugin actions.
- Terminal focus behavior is terminal-specific and requires on-device verification.
- Without a hook/RPC/workflow report, session state is coarse only.
- Attached sessions occupy a fixed number of slots and carry no interrupt gesture; the reference hook bridge covers Claude Code only, and the bundled probe and focus helpers are macOS-only.
- Loopback binding by default.
- Random token stored in a local mode-0600 file.
- No raw command endpoint.
- Command argv arrays with
shell=False. - No command output returned unless a future implementation deliberately adds a reviewed redaction path.
- Confirmed hold required for the example interrupt actions.
- No arbitrary Stream Deck profile edits.
- No credentials in cockpit JSON, launchers, button settings, icons, or logs.
Stop the daemon, remove the action/profile owned by Agent Cockpit through the Stream Deck application, uninstall the local plugin if installed, and delete:
rm -rf ~/.agent-cockpit
rm -rf ~/.claude/skills/streamdeck-agent-cockpit
rm -rf ~/.agents/skills/streamdeck-agent-cockpit
rm -rf ~/.jcode/skills/streamdeck-agent-cockpitThis does not remove or alter unrelated profiles or third-party actions.
