Claude Code × DJ Deck Controller

Code in terminal.
Decisions on deck.

While multiple Claude Code sessions run in the background, you can stay focused on other work.
Whenever a session needs a response, choices appear on the deck — one button press moves it forward.

Gaming, writing docs, focused on another window —
no terminal switch, no typing, just press the button.

Skill Injection AskUserQuestion → Deck Cross-Session Focus Subagent Tracking 7 Hook Pipeline 220+ Tests

// live state simulation

Approve
Deny
2sessions
api-svr
ROOT
🖥 SYSTEM D200 Info Display
Claude

Why Claude DJ

Multi-monitor, multi-session, one deck

Claude Code sessions run in the background while I stay on task. I only look at the deck when a response is needed.

01

Never break your flow

Gaming, writing, browsing — whatever you're doing, when Claude needs input just press a deck button. No terminal switch, no typing, no context break.

02

All sessions, one place

Main task, side branch, separate project — however many sessions, whichever one needs a response surfaces automatically on the deck.

03

Deck or touch, your choice

Ulanzi D200 physical buttons, browser virtual deck, touch monitor — pick the interface that fits your setup. The behavior is the same regardless of input method.

Extensibility

Any device becomes a deck

Claude DJ clients connect to the Bridge over a single WebSocket. No restrictions on input device form factor.

A

Physical Deck

USB stream deck like the Ulanzi D200. Tactile button feedback lets you operate without looking. Translator Plugin bridges Bridge and UlanziStudio.

B

Browser Virtual Deck

Launch Virtual DJ in a browser with no extra hardware. Fastest way to get started. Direct WebSocket connection to the local Bridge.

C

Touch Monitor

Attach a touch monitor to your workstation and run the web app fullscreen. See all choices at a glance and respond with a tap.

D

Remote Device

An Android tablet or mini PC with its own CPU communicates with the Bridge over the network — a fully independent deck terminal separate from your workstation.

Claude Code Session │ Hooks (HTTP) Bridge Server (localhost:39200) │ WebSocket /ws ┌───────────┬───┴───────┬──────────────┐ [A] Physical Deck [B] Browser [C] Touch Monitor [D] Remote Device Translator Plugin localhost Local Network LAN / Wi-Fi │ Virtual DJ Fullscreen Web App Android · Mini PC WS (port 3906) │ UlanziStudio App │ USB HID Ulanzi D200

How It Works

The loop in five steps

00

Skill Injection

The choice-format skill is automatically injected into the session. From that point Claude outputs every decision as an AskUserQuestion tool call.

01

Hook Event Received

When AskUserQuestion fires the PermissionRequest hook, the Bridge transitions the session to WAITING_CHOICE. For standard tools it transitions to WAITING_BINARY. Blocks up to 60s waiting for a response.

02

Layout Switch

The moment state changes, the new layout is broadcast over WebSocket. Cross-session focus filtering ensures other sessions don't overwrite the current choice screen.

03

Button Press

Review choices on the deck and press a button. No focus switch, no typing. The response is routed precisely to the focused session.

04

Response Delivered

The Bridge returns the selection as a hook response, and Claude Code immediately proceeds to the next step based on that choice.

Core Mechanism

One skill reshapes Claude's output

The heart of Claude DJ is the choice-format skill. This single skill changes Claude's behavior at the model level — making it call the structured AskUserQuestion tool at every decision point.

Before (no skill)

Text-based selection

Claude outputs a numbered list as plain text. The user must manually type a number in the terminal — the deck cannot detect this.

Which approach? 1. Refactor 2. Rewrite 3. Patch → terminal focus required
After skill injection

AskUserQuestion tool call

Claude calls the AskUserQuestion tool. This triggers the PermissionRequest hook — the only hook that blocks.

AskUserQuestion({ options: [ "Refactor", "Rewrite", "Patch" ] }) → deck buttons auto-displayed

Key Distinction

Choice vs. confirmation

The skill blocks the anti-pattern of listing plan descriptions as choices. "Shall we proceed?" is a confirmation, not a choice.

Real Choice

Different paths

2–4 genuine alternatives exist, and the outcome differs depending on which option is chosen.

[1] Refactor [2] Rewrite [3] Patch → 3 buttons, each a different result
Confirmation

Plan approval request

Describe the plan in text, then ask for approval with two options: proceed or change direction.

Plan: apply upgraded model to Pioneer [1] Proceed [2] Change direction → Y/N binary, concise
Anti-pattern (forbidden)

Plan disguised as choices

Mixing plan descriptions and additional scope as same-level choices. The user cannot meaningfully "choose" anything.

✗ "Fix X and apply it" ✗ "Also apply to existing scaffold" → scope bundling. Describe plan first, then ask Y/N

State Machine

State is layout

The moment a session state transitions, the deck's button layout changes entirely. During processing, buttons pulse in a wave to visually signal what's running.

IDLE

Idle

Task complete or awaiting input. All buttons deactivated.

PROCESSING

Processing

When tool execution is detected, buttons blink with staggered delays to visually indicate work in progress.

WAITING_BINARY

Awaiting Permission

Permission request received. Only Approve and Deny are active; all other buttons are inactive.

WAITING_CHOICE

Awaiting Choice

The skill-driven AskUserQuestion call blocks the hook. Pressing a button instantly delivers the selection to Claude.

1
2
3
4
MULTI-SELECT

Multi-select

Multi-select mode. Toggle each item on or off, then submit all at once with the Done button.

☑1
☐2
☑3
☐4
AWAITING INPUT

Awaiting Input

Claude output choices as text and is paused. A waiting indicator appears on the deck; you must respond directly in the terminal.

Architecture

System diagram

Claude Code Session │ │ choice-format skill injected → all decisions output as AskUserQuestion │ ├─ PermissionRequest → hooks/permission.js → POST /api/hook/permission [BLOCKING 60s] │ tool_name=AskUserQuestion → WAITING_CHOICE + updatedInput.answer returned │ tool_name=Bash/Edit/Write → WAITING_BINARY + allow/deny/alwaysAllow returned │ ├─ PreToolUse → hooks/notify.js → POST /api/hook/notify [async] │ → PROCESSING (focus filtering: WAITING session protected) │ ├─ PostToolUse → hooks/postToolUse.js → POST /api/hook/postToolUse [async] │ → PROCESSING + lastToolResult recorded │ ├─ Stop → hooks/stop.js → POST /api/hook/stop [async] │ → transcript parse → WAITING_RESPONSE or IDLE │ ├─ UserPromptSubmit → hooks/userPrompt.js → GET /api/events/:id [async] │ → read deck events from events.jsonl │ ├─ SubagentStart → hooks/subagentStart.js → POST /api/hook/subagentStart [async] │ → add agent to session.agents Map │ └─ SubagentStop → hooks/subagentStop.js → POST /api/hook/subagentStop [async] → remove agent from session.agents MapBridge Server (localhost:39200) ├─ SessionManager (state, focus, agents, prune) ├─ ButtonManager (state → layout, choice resolution) ├─ WsServer (broadcast, late-join sync) └─ Static serve → Virtual DJ │ WebSocket /ws ┌──────┴──────────────────┐ Virtual DJ Translator Plugin (Phase 3) (browser) ↓ WS (port 3906) UlanziStudio → USB → D200 │ [0][1][2][3][4] ← dynamic (choices / approve-deny) [5][6][7][8][9] ← dynamic (choices, max 10) [10][11][12][INFO DISPLAY] SC SN AG ← SC=session count, SN=session switch, AG=agent switch Plugin System: ├─ .claude-plugin/plugin.json + marketplace.json ├─ hooks/hooks.json (7 hooks, auto-discovered) └─ skills/choice-format/SKILL.md ← core of Claude behavior change

WebSocket Protocol

Bridge ↔ client communication spec

WebSocket message schema exchanged between the Bridge Server and clients (Virtual DJ or Ulanzi D200).

← Bridge → Client: LAYOUT (full state)
{ "type": "LAYOUT", "preset": "binary" | "choice" | "processing" | "idle" | "response", "sessionCount": 3, "session": { "id": "abc", "name": "api-server", "state": "WAITING_CHOICE" }, "agent": { // subagent (null=root) "agentId": "ag1", "type": "Explore", "state": "PROCESSING" }, "agentCount": 2, // child count of current session "focusSwitched": true, // on slot 11/12 switch "prompt": { ... }, // binary preset "choices": [ ... ] // choice/response preset }
← Bridge → Client: Other messages
// full dim (on session end) { "type": "ALL_DIM" } // initial handshake { "type": "WELCOME", "version": "0.1.0", "sessions": [ { "id": "abc", "name": "api-server", "state": "PROCESSING", "agents": [ { "agentId": "ag1", "type": "Explore", "state": "PROCESSING" } ] } ] }
→ Client → Bridge: Input
// button press { "type": "BUTTON_PRESS", "slot": 0, "timestamp": 1743000000000 } // client ready { "type": "CLIENT_READY", "clientType": "virtual", "version": "0.1.0" } // slot mapping: // slot 0 = Approve (binary) or Choice 1 // slot 1 = Deny/Always or Choice 2 // slot 11 = session switch (cycleFocus) // slot 12 = agent switch (cycleAgent)
← Hook response (permission.js stdout)
// AskUserQuestion → returns selection number { "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow", "updatedInput": { "answer": "2" } } } } // standard Permission → allow/deny/alwaysAllow { "hookSpecificOutput": { "hookEventName": "PermissionRequest", "decision": { "behavior": "allow", "message": "Claude DJ: allow via button" } } }

Implementation Milestones

Phased development roadmap

Phase 1 ✅

BINARY + CHOICE + PROCESSING

Bridge Server + SessionManager + ButtonManager PermissionRequest hook (blocking 60s timeout) AskUserQuestion → WAITING_CHOICE pipeline PreToolUse/PostToolUse → PROCESSING wave Stop hook → transcript parse + IDLE Virtual DJ FE full loop verified
Phase 2 ✅

Skill Injection + Multi-session + Subagents

choice-format skill — Claude behavior change Cross-session focus management (getFocusSession updated) Focus filtering — WAITING session protection SubagentStart/Stop hook + agents Map tracking Slot 11 session switch, slot 12 agent switch Plugin install/uninstall CLI + marketplace compatible Bridge auto-start via SessionStart hook Miniview mode — always-on-top PiP window 220+ automated tests
Phase 3

Physical D200 + Distribution

Translator Plugin (Bridge WS ↔ UlanziStudio WS) Key state PNG rendering (Canvas/SVG → base64) UlanziStudio plugin packaging (manifest.json) MCP wrapper — Bridge auto-launch (POC) D200 physical button E2E tests npm publish + GitHub release

Get Started

Quick Start

Two lines in a Claude Code session and you're set. Start using the deck right away.

Step 1 — Install Plugin

Run in a Claude Code session

# In a Claude Code session: /plugin marketplace add https://github.com/whyjp/claude-dj /plugin install claude-dj-plugin

7 hooks + choice-format skill registered automatically.

Step 2 — Start Bridge

Auto-start (or manual)

# SessionStart hook auto-launches # manual start: node bridge/server.js ./scripts/start-bridge.sh --debug

Virtual DJ dashboard at http://localhost:39200

Step 3 — Use It

Launch Claude Code

claude # hooks + skills auto-loaded # all permissions/choices → deck buttons

No terminal focus, no typing — just press the button.

Manual Install (git clone)

# 1. git clone https://github.com/whyjp/claude-dj.git && cd claude-dj # 2. npm install # 3. node claude-plugin/bridge/server.js # start bridge # 4. npx claude-dj install # register hooks + skills # 5. claude # launch Claude Code # 6. npx claude-dj status # verify installation
View on GitHub ↗ Issues & Feedback