A web-based remote terminal for Claude Code and Codex CLI. Access either agent from any device — phone, tablet, or desktop — over your local network or Tailscale.
A real terminal, not a chat wrapper. xterm.js + node-pty + tmux run the upstream CLI interactively, exactly as it behaves in your local terminal. Layered on top is an optional chat view — the same live session rendered as clean, scrollable message bubbles, so reading long replies and typing on a phone feel native, without giving up the real terminal underneath.
- Two backends, one UI — Spawn a Claude Code session or a Codex session from the same browser; each session is tagged with its backend (blue for Claude, emerald for Codex)
- History browser — Cross-backend history view for up to the 25 most recently active sessions in the current backend / project filter. Search filters that loaded result set (there is no pagination yet); any listed session can be resumed in one click
- Real terminal — xterm.js renders the full terminal experience: colors, cursor, scrollback, links
- Chat view — Flip any live session into a structured chat: message bubbles, rendered Markdown, collapsible tool-call strips, auto-scroll that pauses when you scroll up. It reads the CLI's own transcript file, so it holds the complete, scrollable record — the terminal viewport can truncate a long reply, the chat view never does. One tap back to the real terminal for TUI prompts and pickers.
- Send & interrupt from chat — Type and send straight from the chat view (it writes to the PTY, same as typing in the terminal); a stop button interrupts the running agent. Attach an image or file inline — send a phone screenshot and the agent reads it.
- Live status in the session list — Each session shows what it is doing right now (the current tool call while working) or a preview of its last reply when idle, with a pulsing dot for active work.
- Responsive split layout — On tablet/desktop the session list is a persistent narrow pane next to the content; on a phone it collapses to a slim icon rail. List and content stay on one screen either way.
- New-session parameters — Choose model, reasoning effort, and permission mode (Claude) or reasoning / sandbox (Codex) before spawning; every value is validated against the CLI's own flags.
- Multi-session — Click "+" to spawn up to 10 concurrent sessions, switch freely between them
- tmux-backed persistence — Sessions survive server restarts; the PTY lives in tmux, the WebSocket just attaches to it
- Session ring buffer — 5 MB of output per session is replayed on reconnect, so switching devices doesn't lose context
- File upload — Drag & drop onto the terminal, or click the paperclip in either view, to hand files to the running agent
- Cross-device — Works on iPhone, iPad, Android, and desktop browsers via Tailscale
- iPad / iOS-friendly — Touch key bar (Esc, Tab, Ctrl+C, arrows) and IME fixes for iOS 26
- Dark/Light theme — Follows system preference, toggleable in sidebar
- Token auth — The token is never embedded in the page HTML; supply it via a
?token=link or the login prompt, and it's remembered per browser - Single port — HTTP + WebSocket on one port (default 3109), simple firewall setup
Browser (any device) Server (your Mac)
┌──────────────────┐ ┌──────────────────────┐
│ xterm.js │◄──WS──►│ server.ts │
│ (Terminal UI) │ │ ├─ Next.js (pages) │
│ + backend tabs │ │ ├─ WebSocket server │
│ │ │ └─ TerminalManager │
│ IndexedDB │ │ ├─ tmux:ccrt-#1 │──► claude (PTY)
│ (session list) │ │ ├─ tmux:ccrt-#2 │──► codex (PTY)
└──────────────────┘ │ └─ ... │
│ │
│ History scanner: │
│ ~/.claude/projects/* │
│ ~/.codex/sessions/* │
└──────────────────────┘
- server.ts — Custom HTTP server serving Next.js pages + WebSocket upgrade on
/ws/terminal - TerminalManager — Manages tmux-backed PTY lifecycles, ring buffers, attach/detach per session
- backends.ts — Picks the right CLI (
claudeorcodex) and builds the right argv, including--resumesemantics for each - history-index.ts — Scans both
~/.claude/projects/*/(Claude Code) and~/.codex/sessions/*/(Codex) and renders a unified history browser - transcript-hub.ts / transcript-parser.ts / session-discovery.ts — The read-side chat layer: finds the transcript file the CLI writes for a live session, tails it incrementally, parses it into structured messages + metadata (model, tokens, branch, tool calls), and streams them to the chat view. The CLI and terminal path stay untouched.
- WebSocket protocol — JSON messages:
create/attach/input/resize/kill/list(terminal), pluschat_attach/chat_event/chat_input/interrupt/watch_status(chat + live status)
-
Node.js 20+ (or Bun)
-
tmux (
brew install tmuxon macOS) -
At least one of:
- Claude Code CLI — looked up at
~/.local/bin/claude,/opt/homebrew/bin/claude,/usr/local/bin/claude - Codex CLI — looked up at
/opt/homebrew/bin/codex,/usr/local/bin/codex,~/.local/bin/codex
You can install only one if you only need that backend; the UI will just fail to spawn the missing one.
- Claude Code CLI — looked up at
-
macOS (node-pty prebuilds are darwin-arm64; Linux should also work with rebuild)
git clone https://github.com/AliceLJY/cc-remote-term.git
cd cc-remote-term
npm install
# Local development: create a private env file without printing the token
umask 077
printf 'CC_TERMINAL_TOKEN=%s\n' "$(openssl rand -hex 32)" > .env.local
# Build & start
npm run build
npm startOpen http://localhost:3109 in your browser and paste the token into the login prompt. The token is saved in that browser. The sidebar's "+" button creates a new terminal; on the home screen, the All / CC / Codex filter selects which backend new terminals start with.
The server binds to 127.0.0.1 by default. For remote access, bind it explicitly to your Tailscale address (preferred) or to all interfaces only when the host firewall is configured:
CC_TERMINAL_HOST=100.x.x.x npm startWhen a network bind is enabled, the server auto-detects and prints the Tailscale URL:
[cc-terminal] Tailscale: http://100.x.x.x:3109
Access from any device on your Tailnet at http://100.x.x.x:3109, then paste the token into the login prompt. Avoid putting tokens in URLs because URLs can enter browser history and logs.
Generate a token in a private file outside the repository first. The command creates ~/.config/cc-remote-term/token with mode 600 inside a mode-700 directory and copies the token to the clipboard without printing it:
npm run token:initCreate a private log directory, then drop a plist like this into ~/Library/LaunchAgents/com.cc-remote-term.web.plist and bootstrap it. The plist contains no token; the wrapper validates the private file's type, owner, mode, and format before reading it at process start.
install -d -m 700 ~/Library/Logs/cc-remote-term<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>com.cc-remote-term.web</string>
<key>WorkingDirectory</key><string>/Users/YOU/Projects/cc-remote-term</string>
<key>ProgramArguments</key>
<array>
<string>/Users/YOU/Projects/cc-remote-term/scripts/run-launchd.sh</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>NODE_ENV</key><string>production</string>
<key>PORT</key><string>3109</string>
<key>CC_TERMINAL_HOST</key><string>100.x.x.x</string>
</dict>
<key>Umask</key><integer>63</integer>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/YOU/Library/Logs/cc-remote-term/out.log</string>
<key>StandardErrorPath</key><string>/Users/YOU/Library/Logs/cc-remote-term/err.log</string>
</dict>
</plist>launchctl bootstrap gui/$UID ~/Library/LaunchAgents/com.cc-remote-term.web.plistUse npm run token:copy whenever another device needs the current token. Use npm run token:init again to rotate it, then restart the LaunchAgent; neither command prints the value.
| Environment Variable | Default | Description |
|---|---|---|
CC_TERMINAL_TOKEN |
(required) | Auth token; supplied directly for development or by the private-file launchd wrapper |
CC_TERMINAL_TOKEN_FILE |
~/.config/cc-remote-term/token |
Private launchd token file; must be a user-owned regular file with mode 600 |
CC_TERMINAL_HOST |
127.0.0.1 |
Bind address; set explicitly to a Tailscale IP or 0.0.0.0 for remote access |
PORT |
3109 |
Server port |
NODE_ENV |
development |
Set to production for optimized builds |
CC_TERMINAL_TIME_ZONE |
Asia/Singapore |
IANA time zone used in transcript timestamps |
Session limits (in lib/types.ts):
| Constant | Default | Description |
|---|---|---|
MAX_SESSIONS |
10 | Maximum concurrent PTY sessions |
IDLE_TIMEOUT |
30 min | Auto-kill detached idle sessions |
RING_BUFFER_SIZE |
5 MB | Output history per session (replayed on attach / reconnect) |
- Frontend: Next.js 16 (App Router), React 19, Tailwind CSS 4, xterm.js 6
- Backend: Custom Node.js HTTP server, WebSocket (ws), node-pty, tmux (for session persistence)
- Storage: IndexedDB (client-side session list); session metadata persisted in
~/.cc-remote-term-sessions.json
Inspired by Happy.
MIT