Configuration

Every knob, ranked by likelihood of caring.

Table of contents
  1. Environment variables
    1. Required
    2. Recommended
    3. Optional
    4. Hardcoded — not env-configurable
  2. systemd units
    1. deploy/terminalcat.service — the origin
    2. deploy/cloudflared-terminalcat.service — the tunnel
  3. cloudflared.yml
  4. File paths
  5. Scrollback / per-tab memory
  6. Tmux ergonomics

Environment variables

Set in .env (gitignored), or via systemd Environment= / EnvironmentFile=. The installer prompts for the required ones.

Required

Variable What Where to find
CF_ACCESS_TEAM_DOMAIN The part before .cloudflareaccess.com (e.g. acme) CF Zero Trust → Settings → General → Team domain
CF_ACCESS_AUD 64-char hex Application Audience tag of your Access app CF Zero Trust → Access → Applications → your app → Overview

Server fails to start (with a clear error) if either is missing.

Variable What
ALLOWED_ORIGIN Canonical URL (e.g. https://shell.example.com). When set, WS upgrades whose Origin header doesn’t match get a 403. Permissive on missing Origin (CLI tools / curl unaffected). Recommended in production.

Optional

Variable Default What
LOG_LEVEL info pino level: trace / debug / info / warn / error / fatal
LOG_DIR unset If set, logs go to a daily-rotating file under this dir (in addition to stderr)
OUTPUT_BATCH_MS 0 Coalesce stdout chunks for N ms before flushing one combined WS frame. Helps redraw-spam workloads (htop, animations) at the cost of N ms of added latency. Off (default) keeps the headline keystroke RTT lowest. Range 1..100.

Hardcoded — not env-configurable

Deliberately. The bind address must stay loopback-only; making it tunable is just an opportunity to misconfigure into a public listener.

Constant Value Why hardcoded
HOST 127.0.0.1 Refuses to start if anything else; defense-in-depth
PORT 7682 Pick another and edit src/server.ts if you need to; rare

systemd units

Two units live in deploy/. scripts/install.sh does the User= / paths substitution automatically.

deploy/terminalcat.service — the origin

Type=simple, runs as your unprivileged user, Restart=always, sandbox flags (NoNewPrivileges, ProtectSystem=strict, ProtectHome=read-only, ReadWritePaths=$INSTALL_DIR /tmp), StartLimitBurst=10/60s, memory cap 1 GB, OOMScoreAdjust=-100 so cloudflared and tmux die before us under memory pressure.

Two ExecStart options:

# default in the template — runs source via tsx (slower cold start, no build step)
ExecStart=/home/<user>/terminalcat/node_modules/.bin/tsx src/server.ts

# alternative — runs compiled JS (~300ms faster cold start, +20MB RSS)
ExecStart=/usr/bin/node /home/<user>/terminalcat/dist/server.js

Switch via a drop-in (recommended) or by editing the unit:

sudo systemctl edit terminalcat
# (paste:)
[Service]
ExecStart=
ExecStart=/usr/bin/node /home/<user>/terminalcat/dist/server.js

After switching, pnpm build whenever the source changes — scripts/update.sh handles this.

deploy/cloudflared-terminalcat.service — the tunnel

Type=notify, runs as the same unprivileged user, OOMScoreAdjust=-500 (killing the tunnel cuts off all access). Reads ingress config from ~/.cloudflared/terminalcat.yml.

cloudflared.yml

Sample in deploy/cloudflared.yml. The real one lives at ~/.cloudflared/terminalcat.yml:

tunnel: <YOUR-TUNNEL-UUID>
credentials-file: /home/<user>/.cloudflared/<YOUR-TUNNEL-UUID>.json

ingress:
  - hostname: shell.YOUR-DOMAIN
    service: http://127.0.0.1:7682
    originRequest:
      noTLSVerify: false
      connectTimeout: 10s
      tcpKeepAlive: 30s
      keepAliveTimeout: 5m
      keepAliveConnections: 8

  - service: http_status:404

The catch-all 404 entry is required by cloudflared (last entry must be a service-only rule).

File paths

Path Owner Purpose
~/terminalcat/ you source checkout (override via TERMINALCAT_DIR=)
~/terminalcat/.env you, mode 0600 env vars (gitignored)
~/terminalcat/dist/ you compiled JS output (gitignored)
~/.cloudflared/cert.pem you one-time CF login credential
~/.cloudflared/<tunnel-uuid>.json you, mode 0400 tunnel credentials
~/.cloudflared/terminalcat.yml you per-machine tunnel ingress config
/etc/systemd/system/terminalcat.service root origin unit
/etc/systemd/system/cloudflared-terminalcat.service root tunnel unit
/run/user/<uid>/terminalcat-open.sock you, mode 0600 UNIX socket for webdl / webnotify shims
/var/log/terminalcat/ you (if LOG_DIR set) daily-rotated pino logs

Scrollback / per-tab memory

Configurable in public/index.html (look for scrollback: in the new Terminal({...}) options). Default 1000 lines per tab.

Cost: scrollback × N tabs lives in browser memory. 1000 lines × 200 chars × 4 (xterm.js overhead) × N tabs ≈ ~1 MB per tab. Mostly negligible unless you’re on a phone with 50 tabs open.

Tmux ergonomics

src/sessions.ts configures the tmux server on first attach:

  • mouse mode on (drag to select, click cancels copy-mode)
  • status bar disabled (the frontend’s info bar replaces it)
  • right-click pane menus unbound
  • set-clipboard on (OSC 52 — copy-from-tmux lands in your browser clipboard)
  • split-pane keys unbound

If you want different tmux config, edit src/sessions.ts — those calls run once per server-startup. Or just use ~/.tmux.conf; tmux applies it on session creation, after our config calls (so user config wins).