WW

workerdeck/workerdeck

Developer tools
2 stars Quality 67 Trend 28MIT

AI Agent sessions your app can embed, watch, and control.

Overview

A real session, mid-turn. The agent wants to edit a file; it is blocked on your answer. Same tab from your laptop, your phone, your editor, or docked inside an app you build. npx workerdeck is the whole install. Gateway web dashboard on one port at http://127.0.0.1:8787, with nothing to clone and no config file. Open it, point a session at a project directory, give it a prompt, and watch the transcript stream while you approve or deny the tool calls it wants to make. To reach it from another machine (your phone on the same tailnet, say), bind it and give it a secret: --auth-key is one secret over two transports: browsers get a login page and an HttpOnly cookie, services send the same secret as a header. Off loopback without a key the instance generates one rather than serving open: printed once, kept in /auth-key, reused across restarts. Browser logins persist beside it, so a restart does not sign every tab out; rotating the key invalidates them all.

README

WorkerDeck

= 22" />

Your coding agent, in a browser tab. One command, no clone, no config file:

npx workerdeck

A real session, mid-turn. The agent wants to edit a file; it is blocked on your answer. Same tab from your laptop, your phone, your editor, or docked inside an app you build.

Quickstart

npx workerdeck is the whole install. Gateway and web dashboard on one port at http://127.0.0.1:8787, with nothing to clone and no config file. Open it, point a session at a project directory, give it a prompt, and watch the transcript stream while you approve or deny the tool calls it wants to make.

To reach it from another machine (your phone on the same tailnet, say), bind it and give it a secret:

npx workerdeck --host 0.0.0.0 --auth-key "$SECRET" --cwd-root ~/projects

--auth-key is one secret over two transports: browsers get a login page and an HttpOnly cookie, services send the same secret as a header. Off loopback without a key the instance generates one rather than serving open: printed once, kept in /auth-key, reused across restarts. Browser logins persist beside it, so a restart does not sign every tab out; rotating the key invalidates them all. --cwd-root is what confines sessions to a directory tree.

Docs: workerdeck.github.io/workerdeck covers the full flag surface, embedding, permissions, profiles, the job queue, and the protocol reference.

What this is

A coding agent (Claude Code, OpenAI Codex, or any model provider) is a terminal program: it runs where you started it, and it is yours only while that terminal is open. WorkerDeck puts a session server, a typed wire protocol and an approve/deny UI around one, so the same session is reachable from a browser, your phone, your editor, or an app you build yourself.

Self-hosted, MIT, and no credentials of its own: your agent’s CLI resolves its own login exactly as it does in your terminal.

One server, four ways in

The gateway is the base layer: it owns the sessions, the approvals and the event stream. Every client above it is a view onto the same sessions - attach from three of them at once and they stay in step, because there is one ordered, seq-numbered stream and everything replays from it.

What it actually gives you

  • Close-to-real sessions. A session behaves like the agent’s own CLI launched in that directory: the same skills, the same project instructions, the same MCP surface, the same permission system.
  • Human-in-the-loop permissions. A tool call not covered by the session’s permission mode becomes a pending approval, and the tool blocks until someone decides - deny-on-timeout by default. This is what makes it safe to point at a real checkout.
  • Attach, replay, resume. One ordered stream of seq-numbered events. Clients reconnect and replay from their last seen seq; a closed session resumes from the engine’s own on-disk store with the prior transcript backfilled.
  • Three engines on one protocol. Clients render from each engine’s capability record, so an affordance an engine lacks is hidden rather than a control that silently does nothing.
  • Unattended runs. A job queue with bounded concurrency, token budgets, retries, a wall-clock watchdog, and webhooks. A job is an ordinary registry session, so the dashboard watches it stream live.
  • Work that outlives the turn. A session can park on something nothing here is doing - a batch job, a human approving on Monday - and wake days later, mid-turn, as itself. A parked run frees its concurrency slot and stops its wall-clock budget.
  • Long commands you can see and step around. A running tool row shows how long it has run and the tail of its output, so “still going or stuck?” needs no agent. On claude, move it to the background (or send a message with ⌥↵) and the agent answers while the command keeps running.
  • Idle sessions that cost no memory. An engine process and its MCP servers are almost all of a gateway’s footprint. Put an idle session to sleep, or let --engine-sleep-after 30m do it for the ones nobody is watching: the process goes, the session stays listed with its transcript, and the next message resumes the conversation.
  • Reasoning effort you can see and steer. Every session reports the effort its engine will actually send (after the engine’s own defaults and clamping), the VS Code status bar shows it beside the model, one click switches it live, and --effort-default opus=high sets a level per model. The agent can read its own effort through the session_info tool.
  • Agents that manage their own context. With --agent-context-reset on (or per session or profile), a session gets the context_reset tool: once its turn ends, the gateway gives it a clean start and sends the prompt it wrote as the first message. A named agent gets a brand-new session (the old one is closed and kept), any other session has its conversation cleared. That’s how a long unattended session keeps going on a clean context, or an agent moves on to its next task. Resets are rate-limited, and each one leaves a line in the transcript saying why.
  • Reaching a person who isn’t watching. Server-wide webhooks for the four moments a human acts on (permission requested, turn finished, error, closed). The permission payload carries the whole request, so a consumer can answer it over REST - which is what makes an Approve button in a chat message, or on a phone’s lock screen, work. The server itself holds no push credentials.
  • The host’s files, in the trees sessions already run in. Browse, read and fuzzy-search over your --cwd-root directories, so a remote client gets a real file tree instead of guessing at paths. Reading needs no extra grant; writing is a separate opt-in.
  • Images in the transcript, from either engine. A picture a tool returned (Claude reading a PNG), an image codex generated or viewed, and a markdown image in the reply (![shot](/abs/path.png), served through the same host-file routes) all draw inline at a capped size, in both views. Pressing one opens a viewer with fit and 1:1, zoom, pan and download.
  • $ for a shell, as in the CLI. Type $ first in the composer and the rest of the line runs on the host in the session’s cwd, in a real terminal, with the output in the transcript and in the model’s next context - no turn spent, no permission card, no rewording by the agent. Every command is a tracked shell record you can list, expand and kill, and any row opens as a full terminal: keystrokes go to the PTY, the pane resizes it, and several clients can watch and drive the same shell at once. Which is exactly why it is --shell, off by default, and offered to an operator only. The agent can read every shell of its session; with --shell-agent-write gated it can also start, type into and kill shells of its own, each keystroke behind a permission card that shows it verbatim, and ask to take over one of yours (shell_request_write), which you grant or revoke from the shell’s row.
  • A status line the agent writes. Every session gets set_status: one short line (and an emoji) shown under its name in every client, like a chat status - “waiting on CI”, “blocked: needs an API key”. It stays until the agent changes it, clears when the conversation resets, and you can set or clear it yourself from the card menu. An AGENTS.md line telling agents when to use it is all the setup there is.
  • Sessions that know their own budget. Every session, on any engine, gets session_info: engine, vendor and model, context usage (tokens used, window, percent left), turns, cost and the account rate-limit windows. An agent can check it before a big step and wrap up, summarise or ask for compaction instead of running into the wall.
  • Sessions that can talk to each other. Every session, on any engine, gets three tools: peers_list, peers_peek (status, checklist, the last few lines, without interrupting) and peers_send. A message lands as a peer-stamped message that never cuts into a running turn, so a Claude session in one repo can ask a Codex session in another to do something and read how far it got. Scope-bounded, rate-limited, and loop-guarded by a hop chain a human turn resets. With a workerdeck relay running on one always-on machine, sessions on your other gateways show up too, as gateway:session, behind rules the relay and each gateway enforce. In the composer, # names one of them: it completes from the sessions this gateway shows and sends a hint, not an order - “commit what #Astra left unstaged” tells the agent which session to go and read, and the transcript keeps the bare text you typed.
  • Agents that outlive their sessions, in teams. An agent is a name, an avatar and a standing brief the gateway keeps across restarts; it owns one session at a time and New conversation starts it fresh. Drag one agent onto another to form a team (one level deep): members sit under their lead in every client, reach only their lead and teammates through the peer tools, and sleep when idle (--agent-sleep-after, 15 minutes by default). # in the composer finds an agent by name. Over a relay a team can span machines and owners: every agent answers to an owner (--owner, per profile, enrolled with --owners), and agents of different owners meet only in a team or as two agents marked Shared (--agent-sharing; Private by default).
  • Projects, not folder basenames. A .workerdeck.json at the root of a repo gives it a name and an icon, found by an ancestor walk from the session’s cwd - so a list of sessions reads as a list of projects, and can be filtered, grouped and sorted by one. The gateway resolves it (a phone cannot see the gateway’s filesystem) and the wire carries an address, not bytes. An optional "shortcode": "WD" (2 to 5 characters, A-Z and 0-9) is what narrow list rows draw instead of the name, with the full name still on the tooltip; anything else is ignored silently.
  • Sessions you can put in front of your own users. A gateway embedded in an app’s backend can run the provider engine with nothing granted - no shell, no host filesystem, no egress, just the QuickJS guest and an in-memory VFS (sandboxedProviderProfile()) - and tag each session with opaque scope tags the gateway then enforces at every route, the WS attach and the job queue. A scope miss answers 404. What the tags mean is your authorizeSession policy: “space” and “user” are your app’s vocabulary, not ours. This is scoped embedding, not multi-tenant SaaS - your edge is still the authorization boundary and this is defense in depth behind it.

Engines

A profile is what a session runs as, and it picks the engine.

  • Claude Code, via the Anthropic Agent SDK.
  • OpenAI Codex - the local codex binary, driven over its app-server JSON-RPC surface with token streaming and interactive approvals. Its ask channels map onto the same permission surface, with one difference carried honestly in the request itself: a codex command approval is usually an escalation after its sandbox already refused the command, not a gate before execution, and approving re-runs it unsandboxed. Network is codex’s third axis, not an approval question: outbound access is off by default in workspace-write, so a git push fails rather than asking. WorkerDeck honours - and never overwrites - your [sandbox_workspace_write] settings.
  • Any provider the AI SDK supports, through a host hook - no CLI process. This engine trades ambient authority for a sandbox: no shell, no host filesystem, capability-scoped tools, and untrusted code confined to a QuickJS guest that can run in the user’s own browser tab so client-held documents never reach the server.

Every profile answers with its engine’s capability record, a model catalog shipped with the release (a real picker from the first request, no warm-up session), and whether its credentials currently probe as usable. All three implement one Runner interface and speak the same protocol, so the client, the React layer, the panel and the queue are unchanged either way. Profiles also scope who may run as what, because each person under their own profile is each person using their own account. See Profiles.

Packages

Two tiers: @workerdeck/* are the libraries you embed, workerdeck is the instance you run. Each package has its own README, with the code for using it.

Package What it is
workerdeck The turnkey instance: gateway + dashboard on one port, shared-secret auth, durable parking, restart guard.
@workerdeck/protocol The wire protocol - events, commands, REST shapes, and the few rules every client must agree on. Dependency-free, browser-safe. The product boundary, versioned from day one.
@workerdeck/core The engines, as adapters - each with a capability record, a shipped model catalog and a credential probe - behind one Runner interface, plus tool execution on a swappable seam and park/restore. No transport.
@workerdeck/sandbox The untrusted-code boundary: QuickJS-NG WASM guest, in-memory scratch VFS, by-value host bridge, interpreter-enforced memory and time limits. Runs server-side or in a tab.
@workerdeck/queue The job queue: concurrency, token budgets, retries, watchdog, retention, webhooks. Pluggable adapter (in-memory bundled).
@workerdeck/server The gateway: HTTP + WebSocket, session registry, auth hook, profiles, job routes, session notifications, browser tool bridge, parked-session storage, opt-in host-file routes.
@workerdeck/client Typed client for browsers and Node: REST + WS attach with auto-reconnect and replay-from-last-seq. Zero runtime deps.
@workerdeck/react Headless React: the session hook, attachment and host-file hooks, and pure reducers for the transcript and the sessions list. No styling opinion.
@workerdeck/ui Styled agent-control components: the session panel (transcript, tool-call cards, permission prompts, composer with attachments and @file / /command / #session completion), the sessions browser, and the workspace around them. Tailwind v4 + Base UI.
@workerdeck/web The dashboard as prebuilt static files, for serving from your own host. Zero runtime deps.

The apps - apps/ios, apps/vscode, the docs site and apps/embedded - are not published to npm; each has its own README. The extension ships on the Visual Studio Marketplace instead, in lockstep with the packages above.

Putting an agent in front of your own users? Start with Embed WorkerDeck in your app, then read apps/embedded, which is the guide as a working app. It is a small wiki whose right-hand sidebar is a sandboxed agent, with the gateway inside the app’s own server - one port, the app’s cookie as the only credential, scope as the entire ownership model, and the app’s data reaching the loop as a real MCP server. It is a working app, not a snippet: pnpm --filter @workerdeck/embedded-example dev.

Auth & the providers’ terms

WorkerDeck performs no model-provider authentication of its own - by design. It spawns the official SDK or CLI, which resolves whatever credentials the operator’s environment provides. It never implements a provider’s OAuth flow and never touches a CLI’s credential store. The one broker it offers is opt-in and runs the official CLI: Connect account on a Claude profile runs claude setup-token for you, shows its sign-in link, passes back the code you paste, and keeps the long-lived token it prints for that profile only (a 0600 file, never in an API response or a log). Sign in on a codex profile with its own codexHome runs codex login --device-auth and shows its link and one-time code; codex keeps that login itself, and WorkerDeck never reads it.

Our good-faith reading, not legal advice: an API key (or Bedrock/Vertex) is the supported path for anything that is a service - unattended runs, multi-user deployments, anything you expose to others - because Anthropic’s Agent SDK docs are explicit that third-party developers may not offer claude.ai login or subscription rate limits in their products. Set ANTHROPIC_API_KEY and use requireApiKey: true to fail closed on subscription credentials. Your own subscription for your own single-user use (the equivalent of running the CLI yourself) is the one case where those may be appropriate; the server allows it with a one-time notice, and every session reports its provenance. A connected account is that same case: one person’s own subscription on their own profile, never shared. Whether OpenAI’s terms restrict headless ChatGPT-subscription codex use the same way is unresolved, and we take the same posture there. The compliance and legal review of this project is settled; your own deployment’s use of credentials stays your responsibility. Full discussion.

Red lines for contributors (PRs crossing these are rejected): no hand-rolled provider OAuth or PKCE, no refresh-token custody, no hand-written CLI credential files, no extraction of tokens from a CLI’s credential store, no spoofing of an official client’s identity, no multi-account pooling or rate-limit circumvention. The OAuth itself stays 100% provider-owned code.

Honest constraints

  • No serverless. A CLI engine is a long-running subprocess with filesystem state. Realistic targets: a VM, a container with min-instances, any Node ≥22 host with a real disk.
  • Sessions are single-host. Transcripts live on the server’s local disk; resume works across restarts on the same host.
  • The server trusts its host app. For CLI engines a create request accepts MCP servers and tool policy - gate creation behind your own auth and clamp it server-side. (Provider sessions are tighter by construction: MCP is declared on the profile, never by the caller.)
  • Parking is single-host either way. The file store survives a restart, but two servers over one directory would race to rebuild the same sessions.
  • The VS Code extension can run the server for you. Turn on workerdeck.host.enabled and it supervises a workerdeck process on your machine - via npx if you have not installed it - with start/stop/restart in the palette and the dashboard on the same port. One server per machine, not per window.
  • VS Code is on the Marketplace; iOS is not in a store. Install the extension from the Visual Studio Marketplace or with code --install-extension silkweave.workerdeck-vscode. The iOS app is still built and side-loaded from this repo.

Contributing

pnpm install
pnpm dev:server   # gateway on http://127.0.0.1:8787, no auth (loopback only!)
pnpm dev:web      # optional: vite dashboard on :5191 with HMR, proxying /v1 to the gateway

pnpm typecheck   # tsgo (TypeScript 7 native preview)
pnpm test        # vitest - core runner, server integration, transcript reducer
pnpm lint        # oxlint

Dev never builds: apps and tests resolve packages straight to TS source via the @workerdeck/source export condition, and build/ exists only for publishing. Start with docs/ARCHITECTURE.md for the package map and dependency rule, and docs/GOTCHAS.md for the invariants that bite. CONTRIBUTING.md has the rest; security reports go through SECURITY.md, not public issues.

Status

0.15.0 - early but real. Three engines, the protocol, server, client, headless React layer, styled UI, dashboard, job queue, sandbox and deferred execution are all in and tested. 0.9 landed the engine adapters and the Codex engine; 0.10 added the session workspace and codex skills and generated images; 0.11 published the VS Code extension; 0.12 rebuilt its navigation around native editor chrome and lifted the sessions-list and unread models into the protocol so every client shows the same list; 0.13 added cross-origin gateway auth and rebuilt the dashboard; 0.14 and 0.15 shipped together as 0.15.0 - session scope enforced at every door, sandboxedProviderProfile(), a loud MCP failure instead of a silently tool-less session, host tools at a stated trust, and apps/embedded as the reference embedding. The iOS app’s APNs push reached a physical phone from a live gateway for the first time on 2026-08-20 - registration, delivery and a tapped notification opening the session it names are all confirmed; the lock-screen Approve/Deny action still is not. That first real pass immediately found two bugs (every tap aborted the app on a main-thread assert, and a dial could fail silently), so treat push as newly proven rather than settled. PROTOCOL_VERSION guards breaking changes and is at 1 - reset at the 1.0.0 launch and locked since, so every breaking wire change now costs a bump and a mismatch banner.

MIT © Tobias Strebitzer

View this README on GitHub

Recommended Tools

Try a different keyword or remove a filter.

Install

npx skillfish add workerdeck/workerdeck