Skip to content

Repository files navigation

slopdeck

A Stream Deck for Claude Code.

An old phone docked beside the monitor becomes an aesthetic cockpit: it shows what Claude is doing, with character and motion, while your attention is elsewhere — and it is where you approve permissions and answer Claude's questions without touching the terminal.

The product is the object. It is watched, not carried: being looked at is the medium.

Runs on Android and iOS — any phone with a browser. It is a web app, not a native one; there is nothing to install from a store. The only difference between the two platforms is that iOS does not vibrate (see below).

Vocabulary: CONTEXT.md. Design record: DECISIONS.md.

Android and iOS

Both are supported and get the same Deck: the same clock, the same Clawd, the same speech bubble, the same takeover cards, the same pause and offline states.

The one difference — no haptics on iOS. iOS Safari does not implement navigator.vibrate, so an iPhone Deck flashes without buzzing. Every vibration call is feature-detected, so nothing breaks and nothing errors; the haptic is an enhancement of the boundary flash, never the alert itself, and the flash is what an alert actually is on both platforms. If a silent Deck matters to you, dock the Android phone.

Neither platform gets notifications when you are away from the desk — that is deliberate, not a platform gap. See Deliberate boundaries.

What the deck does

A session clock that never lies. Claude Code UserPromptSubmit and Stop hooks POST to the gateway, which streams them over SSE to the PWA. The deck is a landscape desk clock: time of day while idle, an incrementing session timer from the moment a prompt is submitted, frozen on Stop — labeled with the session's directory name, with a badge counting concurrently running sessions. Events are server-timestamped and replayed from the ring buffer on reconnect (Last-Event-ID), so a network blip never leaves the deck stale; while the stream is down the deck shows an unmistakable gray scanline OFFLINE state. Idle/running render dim for the always-on OLED, the layout pixel-shifts every minute against burn-in, and Wake Lock keeps the docked screen alive.

Clawd narrates, in a speech bubble. The mascot is the primary way the deck tells you its state without being read: sleeping when idle, typing while a session runs, waving when a turn completes, alarmed when something needs you, plus paused and offline poses. Every state has exactly one expression.

Beside him, a speech bubble shows the latest completed tool call — one line at a time, as if Clawd is narrating: the command, the file path, the tool name, with a colored write/edit label before filenames. PostToolUse hooks feed it, registered only for Write|Edit|MultiEdit|NotebookEdit|Bash so reads never pay a hook round trip, and a table-driven classifier marks high-impact Bash commands (installs, migrations, docker, pushes, deploys).

Between submitting a prompt and the turn's first tool call there is nothing to narrate, so the bubble cycles one of Claude Code's own thinking verbs instead of sitting empty — the deck looks like it is thinking because it is.

The bubble shows one thing, not a history — glanceable from arm's length rather than something you must stop and read. It speaks exactly while Clawd types and clears the moment his pose changes, so it can never outlive the state that produced it. The centered SLOPDECK title steps aside while he speaks and returns at rest.

Earlier versions rendered this as a scrolling activity ticker along the bottom. That strip is gone — several simultaneous scrolling rows are noise on an object watched peripherally. The hooks behind it are unchanged.

Completion alerts that stay meaningful. A Stop alerts only when the turn ran at least the threshold (10s default, SLOPDECK_ALERT_THRESHOLD_MS) — short chat turns stay silent, so an alert always means real work finished. The alert lands on the deck itself: a green boundary flash, plus a vibration tap on Android. There is no notification to a phone you aren't looking at — the deck is an ambient display, not a pager, and no server-side notification channel exists to leak across workspaces.

Approve or deny from the deck. A PermissionRequest http hook — it fires only when a permission dialog would genuinely appear, so allowlisted commands never reach the deck — is held open by the gateway while the deck takes over the screen: tool name plus the exact command/path payload in real monospace, with Allow (hold-to-fill, scaled to risk), Deny, and Ask-in-terminal taps. Allow/Deny answer the hook with the documented decision JSON; Ask-in-terminal — and every fallback — returns no decision, letting the terminal dialog proceed normally. Fallbacks never auto-deny: immediately when no deck is connected, at 540s (under the 600s hook timeout) when a connected deck stays silent. Prompts queue FIFO, oldest first, with a queue-depth badge counting what waits behind the visible card.

Every card names its own project. The deck renders one session large, but a held prompt may come from a different one — so the card carries its project name at heading scale, with the session id small and dim beside it (two sessions in one project stay tellable apart). You are never approving a command without knowing which project asked. Attribution never outweighs the payload, which stays the card's primary read.

Pause is one tap, no arming ritual: it flips the gateway to passthrough, and while paused every PermissionRequest falls back to the terminal instantly. The deck tints purple while paused; tapping again resumes interception. The same switch is reachable from the workstation as slopdeck on / slopdeck off.

Answer Claude's questions remotely⚠️ experimental, opt-in, and expected to re-ask in the terminal sometimes.

Read this before enabling. Claude Code offers no supported way to answer AskUserQuestion remotely. This feature works by exploiting undocumented behavior (the deny-with-reason hack below), which means:

  • Terminal re-asks are normal, not bugs. Any fallback — no deck connected, paused, timeout, an unrecognized payload shape — puts the question back in the terminal. Expect this to happen and treat it as the feature working.
  • A Claude Code upgrade can break it silently. Run npm run canary after upgrading; if it reports the session ignored the deny reason, disable question interception until it is revalidated.
  • It ships off by default, behind its own opt-in and its own hook matcher, so it cannot affect permission handling if it misbehaves.

Permission approval (above) rests on documented, decision-capable hooks and carries none of these caveats. These are two different levels of stability in one product; do not assume the guarantees transfer.

With question interception enabled, a PreToolUse http hook matched to AskUserQuestion alone lets the deck render the question with one tap-target per choice (plus an Ask-in-terminal escape); the tapped choice returns as permissionDecision: "deny" with reason User selected: <choice> — which Claude reads as the answer. Multi-question calls step through the card one question at a time, and multiSelect questions render toggleable choices with a CONFIRM tap. Every fallback (no deck, pause, timeout, unrecognized payload shape) returns permissionDecision: "ask" so the question renders in the terminal normally. An unanswered card falls back after 60s total for the whole call (SLOPDECK_QUESTION_TIMEOUT_MS) — a stale answer to a question mid-plan is worse than a terminal re-ask; permission prompts keep their 540s window. The canary test drives a real Claude Code session end-to-end to revalidate the hack (needs working credentials, consumes tokens).

Deliberate boundaries

Two things slopdeck does not do. Both are design decisions, not gaps — knowing them up front is the difference between the tool fitting your desk or not.

It shows one session large, never several side by side. The deck renders the active session — the one that most recently emitted — and acknowledges other running sessions only as a count. It is an object glanced at from arm's length, where one thing shown clearly beats several shown poorly. If you want a multi-session dashboard, this is not one. (Held prompts are the exception that proves it: a prompt from a non-active session still takes over the screen, which is why every card names its own project.)

It does nothing when you are not looking at it. There are no push notifications, no emails, no off-deck alerts of any kind — a backgrounded or locked phone is deliberately silent. slopdeck is watched, not carried: being looked at is the medium. If you need to know a turn finished while you are in another room, use something else; an ambient object that can also interrupt you anywhere is just a notification app with a nicer clock.

Privacy

slopdeck sends your work to a gateway. If that gateway is the hosted one, it is a machine operated by the slopdeck maintainer, and you should know exactly what reaches it.

What transits the gateway. Hook payloads carry what the deck must render:

  • Project pathscwd for each session (its basename becomes the label).
  • Command text and file paths — for each completed Write, Edit, MultiEdit, NotebookEdit, and Bash call.
  • Tool names and session_id.
  • The exact tool_input payload for permission and question cards — that is the point of showing you what you are approving, and it means command text reaches the gateway in full.

slopdeck does not see your conversation with Claude, the model's responses, or file contents it was not handed in a payload.

Events are never written to disk. Event history lives in an in-memory ring buffer, and every live workspace is process memory. There is no activity database anywhere in the system. A gateway restart drops event history and any held prompt — which is safe by design: a dropped prompt falls back to the terminal and is never auto-denied.

Keys are stored hashed. The workspace key table is the only durable store, and it holds SHA-256 hashes, never key material, verified with a timing-safe comparison. Dumping that table does not yield a working hook key or deck key.

Idle workspaces are swept. On the hosted gateway, a workspace untouched for longer than SLOPDECK_EPHEMERAL_TTL_MS (default 7 days) is deleted outright — stored keys and in-memory runtime together. Ingest and stream activity keep it alive, so only genuinely abandoned workspaces are collected.

Workspaces cannot see each other. A workspace owns its events, its pause state, its held prompts, and its decks. Nothing crosses that boundary — it is the product's core safety property, and it is enforced identically hosted and self-hosted.

Self-hosting is a real alternative, not a disclaimer. Same code, same isolation rules, same payloads — what changes is who operates the box. If you would rather none of the above leave your own infrastructure, follow the walkable path.

How this differs from Claude Code Remote Control

Anthropic ships Remote Control (claude remote-control), which lets you approve, monitor, and steer sessions from the Claude mobile app. It is official, supported, and works from anywhere. If what you want is remote approval, use it — slopdeck does not try to beat it at that, and it is a better tool for the job of controlling Claude Code from away from your desk.

slopdeck is a different object. Remote Control is an app you launch when you want to check on something. slopdeck is a screen already sitting on your desk, already on, already showing you the answer — no launching, no unlocking, no reaching for a phone. The difference is ambient presence versus deliberate access, and it is the entire reason slopdeck exists.

What follows from that:

  • It runs on a dedicated docked phone, not the one in your pocket — an old handset given a second life as a desk instrument.
  • It is designed to be looked at for hours: dim ambient states, burn-in pixel-shift, a strict motion budget, and a mascot whose pose tells you the state without reading.
  • It is deliberately deskbound (see above) — it will never chase you with a notification, which is precisely what Remote Control is good at.

They are not mutually exclusive. Run both if you want the desk object and away-from-desk reach.

Quick start (use the hosted deck)

Don't want to run a server? Use the always-on hosted gateway. One command on your workstation — no clone, no tokens, no .env:

npx slopdeck install

Pick hosted when asked, and scan the QR with your phone — Android or iPhone, either works. That's it: the deck pairs over HTTPS and works from anywhere. Requires Node ≥ 22.6 and Claude Code already set up on the machine.

Read Privacy before pointing your work at the hosted gateway.

Prefer a global install? npm install -g slopdeck, then slopdeck install.

To self-host the gateway instead (your own VPS, full privacy), follow the walkable path below.

The walkable path (self-hosted)

Four steps: deploy the gateway → wire the workstation → scan the QR → dock the phone.

1. Deploy the gateway (VPS)

First time, on the VPS:

git clone <this repo> slopdeck && cd slopdeck
cp deploy/.env.example deploy/.env   # fill in tokens + DECK_DOMAIN
cd deploy && docker compose up -d --build

Caddy provisions HTTPS for DECK_DOMAIN automatically. Every update after that is one local command:

SLOPDECK_DEPLOY_HOST=deploy@your-vps ./deploy.sh
# or: ./deploy.sh deploy@your-vps
# SLOPDECK_DEPLOY_PATH overrides the remote checkout dir (default: slopdeck)

deploy.sh SSHes in, fast-forwards main, and rebuilds the compose stack. Deploys are deliberately manual — no CI trigger. See Restart behaviour for what a redeploy costs and why it is safe. Deploy when you know the deck is quiet, not whenever a branch merges.

2. Wire the workstation

In a clone of this repo on the workstation:

npm install
npx slopdeck install

The installer asks for the gateway URL, verifies the gateway is reachable, prompts for the hook token (hidden — it never lands on screen or in shell history), and verifies it against the gateway before touching anything. Then it merges the hook config into ~/.claude/settings.json (surgically — a malformed settings file aborts with the disk untouched), writes a marked export block to ~/.zshrc, and asks whether to enable question interception. Open a new shell afterwards so the hook token is exported.

3. Pair the phone

Install ends with the pairing finale: it prompts for the deck token and prints a QR encoding https://your-deck-domain/#deck-token=… — the token rides the URL fragment, so it never reaches HTTP requests or server logs. Scan it with the phone camera and the deck is paired; the installer then sends a handshake through the real hook path, and the mascot waves on the phone when the whole chain (DNS, TLS, hook token, gateway, SSE, deck token) works end to end.

Re-pair any time — new phone, new domain — with slopdeck qr.

4. Dock it

Tap the idle clock: the deck goes fullscreen and locks landscape. Add to Home Screen for the installed-PWA experience — Chrome on Android, Safari's Share → Add to Home Screen on iOS. Wake Lock keeps the screen alive in the dock.

Restart behaviour

A gateway restart is safe at any moment, and that is a design property rather than luck. Restarting — a redeploy, a crash, an OOM kill — loses exactly two things, and neither one can hurt you:

  • Event history. The ring buffer is memory. The deck reconnects, replays what survives, and carries on; the clock absorbs the replay silently and only near-live completions flash, so a restart never produces a burst of stale alerts.
  • Any held prompt. A permission or question request the gateway was holding open is dropped.

The second is the one that sounds alarming and isn't, because of the fallback rule: slopdeck never auto-denies. Every path that fails to produce an answer — no deck connected, gateway restarted, gateway unreachable, timeout, paused — returns no decision, and the terminal dialog appears exactly as if slopdeck were not installed. A hook that cannot reach the gateway is a non-blocking error in Claude Code by design, so a dead gateway degrades to normal terminal prompting on its own.

The failure mode of slopdeck is therefore "you approve it in the terminal instead" — never a fabricated refusal mid-task, and never a session wedged waiting on a server that is no longer there. That is what makes manual deploys acceptable: the worst case of restarting at a bad moment is one prompt you answer where you would have answered it anyway.

What survives a restart: workspace keys, provided SLOPDECK_DB_PATH points at a mounted volume. Without it the key store is in-memory too, and a restart means re-pairing.

Day-to-day controls

slopdeck on|off     flip interception (the deck's Pause switch, remotely)
slopdeck status     diagnose the whole chain on one screen
slopdeck qr         re-print the phone-pairing QR
slopdeck uninstall  remove the hooks and the .zshrc block

Develop

npm install
npm test                 # vitest suite (unit + e2e tracer + deploy.sh)
npm run typecheck
SLOPDECK_HOOK_TOKEN=$(openssl rand -hex 32) \
SLOPDECK_DECK_TOKEN=$(openssl rand -hex 32) \
npm run dev              # gateway + PWA on :8484

About

Ambient companion for Claude Code: dock a phone as a live session dashboard and approve agent permission prompts remotely. Hono + SSE gateway, PWA with Web Push, Dockerized.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages