WooTTY is a clean-slate browser terminal designed for one non-negotiable outcome: a terminal experience that stays reliable under real pressure (resize storms, reconnects, long output, and unstable networks).
- Terminal-first UI: maximum viewport, compact status bar, floating controls.
- Reconnect-safe sessions: resume by
sessionId, replay buffered output. - Tab-safe defaults: each browser tab starts its own live session unless the operator explicitly resumes one.
- Explicit multi-session actions:
Resumefor controllable sessions,Watchfor sessions already controlled elsewhere (read-only). - Resize fidelity: client and PTY stay in sync during rapid window changes.
- Operational defaults: high scrollback, keyboard-first controls, low-friction deployment.
- Deployment flexibility: minimal image by default, plus
-opensshimage variant for SSH workflows. - Modern stack: Go 1.26+, Node 24+, React 19 + compiler, xterm.js.
- Quick Start
- Run with Docker
- Kubernetes Deployments
- Run from Source
- Operator Controls
- Configuration
- Architecture
- Testing and Quality
- Contributing
- Security
Stable image from GitHub Container Registry:
docker run --rm -it -p 8080:8080 ghcr.io/icoretech/wootty:latestThen open http://127.0.0.1:8080.
For image flavors, SSH usage, direct command args, compose profiles, and custom images, see Run with Docker.
pnpm install
pnpm devRun dev with SSH alias target:
WOOTTY_COMMAND=/usr/bin/ssh WOOTTY_COMMAND_ARGS="my-ssh-host-alias" pnpm devRun WooTTY against codex exec (non-interactive Codex task in the terminal session):
cd apps/server
go run ./cmd/woottyd run --port 8080 \
codex exec --skip-git-repo-check --ephemeral "Reply with exactly: hello-from-codex"Same flow using environment variables:
WOOTTY_COMMAND=codex \
WOOTTY_COMMAND_ARGS='exec --skip-git-repo-check --ephemeral "Reply with exactly: hello-from-codex"' \
pnpm dev- Web:
http://localhost:5173 - Server:
http://127.0.0.1:8080(auto-falls back to next free port if8080is busy) - Set
WOOTTY_PORTexplicitly to force a fixed dev port.
Production-like local run:
pnpm build
cd apps/server
go run ./cmd/woottyd run --port 8080 bashBuild locally:
docker build -t wootty:dev .
docker run --rm -it -p 8080:8080 wootty:devRun profile-based examples from the root docker-compose.yml:
# default shell/runtime
docker compose up --build wootty
# login shell
docker compose --profile bash up --build wootty-bash
# ssh command mode (set your destination in docker-compose.yml)
docker compose --profile ssh up --build wootty-ssh
# long-running detached session retention profile (72h TTL)
docker compose --profile retention up --build wootty-retention
# deterministic fake PTY mode for tests/e2e
docker compose --profile test up --build wootty-fake-ptyThe wootty-ssh profile builds the final-openssh target so /usr/bin/ssh is available in that container.
Build your own image with custom binaries:
# Dockerfile.custom
FROM ghcr.io/icoretech/wootty:latest
# Install additional runtime tools your command needs.
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
ffmpeg \
rsync \
openssh-client \
&& rm -rf /var/lib/apt/lists/*
# Optional: add your own binary/script
# COPY ./bin/my-tool /usr/local/bin/my-tool
# RUN chmod +x /usr/local/bin/my-tooldocker build -f Dockerfile.custom -t wootty:custom .
docker run --rm -it -p 8080:8080 \
-e WOOTTY_COMMAND=/usr/bin/ssh \
-e WOOTTY_COMMAND_ARGS="user@example.com" \
wootty:customEquivalent direct-args form:
docker run --rm -it -p 8080:8080 \
wootty:custom \
run /usr/bin/ssh user@example.comUse this pattern whenever WOOTTY_COMMAND depends on binaries not present in the default image.
The container serves:
- backend API/websocket on
/api/* - web UI bundled from
apps/web/src
# Your existing runtime image
FROM your-runtime-image:tag
# Keep this pinned and automated (example Renovate comment)
# renovate: datasource=docker depName=ghcr.io/icoretech/wootty versioning=semver
COPY --from=ghcr.io/icoretech/wootty:<version> /usr/local/bin/wootty /usr/local/bin/woottyThen start WooTTY as your container command (or entrypoint), pointing to the shell/binary you want:
wootty run bashapiVersion: apps/v1
kind: Deployment
metadata:
name: myapp-webcli
namespace: myapp
spec:
replicas: 1
selector:
matchLabels:
app: myapp-webcli
template:
metadata:
labels:
app: myapp-webcli
spec:
containers:
- name: webcli
# Or use ghcr.io/icoretech/wootty:latest-openssh or your own custom image.
image: ghcr.io/icoretech/wootty:latest
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-lc"]
args:
- |
exec wootty run bash
ports:
- name: http
containerPort: 8080
env:
- name: WOOTTY_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: wootty-auth
key: token
readinessProbe:
httpGet:
path: /api/health
port: http
livenessProbe:
httpGet:
path: /api/health
port: http
---
apiVersion: v1
kind: Service
metadata:
name: myapp-webcli
namespace: myapp
spec:
selector:
app: myapp-webcli
ports:
- name: http
port: 8080
targetPort: http- Keep web CLI in a dedicated deployment (
myapp-webcli) so scaling/restarts are independent from your main app. - Protect ingress with SSO, IP allowlists, or both; avoid exposing an unauthenticated terminal endpoint.
- Set
WOOTTY_AUTH_TOKENfrom a Secret and rotate it like any other credential. - Use direct command args when you want a non-shell target, for example
wootty run /usr/bin/ssh user@example.comorwootty run /usr/local/bin/your-admin-tool --flag value.
Keyboard shortcuts:
Ctrl/Cmd+Shift+R: reconnectCtrl/Cmd+Shift+K: clear viewportCtrl/Cmd+Shift+=: increase font sizeCtrl/Cmd+Shift+-: decrease font sizeCtrl/Cmd+Shift+0: reset font sizeCtrl/Cmd+Shift+F: fullscreenCtrl/Cmd+Shift+B: toggle controls
Status bar metrics:
- connection status and latency
- session id
- attach mode (
ControlorRead-only) - reconnect count
- buffered/dropped input size (humanized units)
- output size (humanized units)
Session controls:
- click the
Sessionbadge in the status bar to open the session menu. New session: start a fresh session in the current tab.Resume last: reattach the last session id seen in this browser.- session menu separates
Live sessions(running on server) fromRecent session ids(browser memory only). - sessions already controlled in another tab/operator are shown as
Watch(read-only attach). - resumable sessions are shown as
Resume(full control attach). - recent ids that are not running are shown as unavailable.
- tabs do not implicitly steal active sessions from each other.
- terminal font starts at minimum (
11px) by default and can be changed from controls/shortcuts.
| Variable | Default | Description |
|---|---|---|
WOOTTY_HOST |
0.0.0.0 |
Bind address |
WOOTTY_PORT |
8080 |
HTTP/WebSocket port |
WOOTTY_DETACHED_TTL_MS |
86400000 |
Hard TTL for running detached sessions (24h). 0 disables this TTL |
WOOTTY_HISTORY_BYTES |
5242880 |
Buffered output bytes for replay |
WOOTTY_COMMAND |
$SHELL or bash |
Executed command in the woottyd runtime environment (host or container) |
WOOTTY_COMMAND_ARGS |
empty | Shell-like command args string (supports quotes and escapes) |
WOOTTY_CWD |
current directory | Process working directory |
WOOTTY_STATIC_DIR |
auto-detected | Directory with built web assets |
WOOTTY_AUTH_TOKEN |
empty | Optional bearer token required by /api/sessions and /api/terminal when set |
WOOTTY_ALLOWED_ORIGINS |
empty | Optional comma-separated websocket origin allowlist |
WOOTTY_FAKE_PTY |
0 |
Set to 1 for deterministic fake PTY mode |
WOOTTY_COMMAND_ARGS examples:
# 1) Run a shell command with spaces
WOOTTY_COMMAND=/bin/bash
WOOTTY_COMMAND_ARGS='-lc "echo hello world && whoami"'
# 2) SSH with multiple options
WOOTTY_COMMAND=/usr/bin/ssh
WOOTTY_COMMAND_ARGS='-o StrictHostKeyChecking=no -p 2222 user@example.com'
# 3) Include escaped quotes inside an argument
WOOTTY_COMMAND=/bin/bash
WOOTTY_COMMAND_ARGS='-lc "echo \"quoted text\" && date"'
# 4) Pass an explicit empty argument ("")
WOOTTY_COMMAND=/bin/bash
WOOTTY_COMMAND_ARGS='-lc "printf \"[%s]\\n\" \"\" \"non-empty\""'If WOOTTY_COMMAND_ARGS has invalid quoting (for example an unterminated quote), WooTTY fails fast on startup with a config error.
CLI equivalent is available for detached retention timing: --detached-ttl-ms.
For non-local deployments, set WOOTTY_AUTH_TOKEN (and optionally WOOTTY_ALLOWED_ORIGINS) to protect session and websocket endpoints.
- Session metadata and PTY state are in-memory only.
- If a terminal process exits, the session is removed immediately.
- If a terminal process is still running and no controller/watcher is attached:
WOOTTY_DETACHED_TTL_MS > 0: session is cleaned up after that TTL.WOOTTY_DETACHED_TTL_MS = 0: timer cleanup is disabled; session remains available until process exit or server restart.
- Server restart clears all sessions because there is no persistent session store.
Recommended for long-running jobs with occasional reconnects:
WOOTTY_DETACHED_TTL_MS=259200000 # 72hCompose example:
services:
wootty:
image: ghcr.io/icoretech/wootty:latest
ports:
- "8080:8080"
environment:
WOOTTY_DETACHED_TTL_MS: "259200000"flowchart LR
B["Browser UI (React + xterm)"] -- "WebSocket (/api/terminal)" --> S["WooTTY Server (Go)"]
B -- "HTTP (/api/sessions)" --> S
S -- "PTY attach/input/resize" --> P["Shell Process (creack/pty)"]
P -- "output stream" --> S
S -- "output + status events" --> B
S -- "session history buffer" --> H["In-memory replay buffer"]
Frontend module ownership:
apps/web/src/App.tsx: composition entrypoint that mounts the terminal feature app.apps/web/src/features/terminal/app/TerminalApp.tsx: terminal app entrypoint and top-level composition shell.apps/web/src/features/terminal/app/composition/*: app-level composition boundaries (platform, domain, and controller wiring) that join environment adapters with feature hooks.apps/web/src/features/terminal/app/engine/*: transport lifecycle, runtime boot/IO bridge, and connection state projection.apps/web/src/features/terminal/app/bindings/*: browser/document/window/session bindings (shortcuts, refresh cadence wiring, resize/fullscreen wiring, title updates).apps/web/src/features/terminal/environment/*: environment contracts shared by app bootstrap and controller layers.apps/web/src/features/terminal/commands/*: terminal command contract + registry ownership (UI actions and shortcut mapping).apps/web/src/features/terminal/contracts/*: shared terminal contracts (session + transport types and ready-state constants).apps/web/src/features/terminal/platform/*: platform-facing utilities shared by app/engine bindings (for example scheduler abstractions).apps/web/src/features/terminal/components/*: presentational controls, status bar, and session menu UI.apps/web/src/features/terminal/view/*: UI-facing formatting and presenter mapping for menu/session copy.apps/web/src/features/terminal/commands/floating-controls/*: floating-controls registry, metadata, and descriptor assembly.apps/web/src/features/terminal/notifications/*: user-facing terminal notice mapping.apps/web/src/features/terminal/session/domain/*: session candidate derivation and domain-level selection helpers.apps/web/src/features/terminal/session/protocol/*: session payload parsing and refresh failure protocol ownership.apps/web/src/features/terminal/session/persistence/*: storage adapters and storage key ownership.apps/web/src/features/terminal/lib/*: terminal-only utility helpers (formatting, outbox buffering).apps/web/src/features/terminal/protocol/*: protocol parsing owned by the terminal feature.apps/web/src/features/terminal/adapters/*: transport adapters owned by the terminal feature.apps/web/src/features/terminal/runtime/*: xterm runtime loading owned by the terminal feature.
apps/web/src/features/terminal/protocol/terminal-protocol.ts is the client-side source of truth for websocket payload parsing.
- Supported inbound message
typevalues:ready,output,exit,error,pong. - Required fields:
ready:sessionId(string),readOnly(boolean),version(must matchTERMINAL_WIRE_CONTRACT_VERSION)output:data(string)exit:code(number),signal(number)error:message(string), optionalcode(known server code string). Unknown non-empty codes are surfaced asrawCode.pong: no additional fields
- Compatibility policy:
- Additive fields are allowed and ignored by older clients.
- Unknown message
typevalues are treated as unsupported and surfaced as a user notice. - Invalid payload shapes are dropped by the parser and do not mutate terminal state.
Transport responsibilities are split by contract:
-
apps/web/src/features/terminal/contracts/transport/transport.tsdefines the transport surface and ready-state constants used by app runtime and test doubles. -
apps/web/src/features/terminal/app/engine/transport/state/transport-policy.tsdefines heartbeat intervals, close codes, and reconnect delay policy. -
apps/web/src/features/terminal/adapters/transport-event-normalizer.tsadapts browser runtime events into typed contract payloads. -
Canonical ready states:
TRANSPORT_READY_STATE.CONNECTING(0)TRANSPORT_READY_STATE.OPEN(1)TRANSPORT_READY_STATE.CLOSING(2)TRANSPORT_READY_STATE.CLOSED(3)
-
Heartbeat policy:
- Client sends
pingevery12swhile connected. - Missing
pongfor12striggers close code4103(pong timeout) and reconnect flow.
- Client sends
-
Close/reconnect policy:
- Manual reconnect closes with
4101. - Starting a fresh session closes old transport with
4102. - Backoff uses
reconnectDelayMs(attempt)(300ms * 1.8^attempt, capped at5000ms).
- Manual reconnect closes with
Standard quality gates:
pnpm lint
pnpm test
pnpm build
pnpm test:e2eOne-shot local CI parity:
pnpm ciCross-browser browser matrix (Chromium + Firefox + WebKit):
pnpm test:e2e:crossNotes:
pnpm lintapplies Biome fixes and then runs typecheck.- CI enforces zero formatting drift (
git diff --exit-code). - Test environment ownership:
- Browser test polyfills and setup wiring live under
apps/web/test/support/. - E2E URL/port defaults live under
apps/web/config/e2e/e2e-env.ts. - App integration harness composition lives in
apps/web/test/integration/app/harness/.
- Browser test polyfills and setup wiring live under
Read CONTRIBUTING.md before opening a PR.
Report vulnerabilities through SECURITY.md.
