Skip to content

y49/tlive

Repository files navigation

tlive

Vendor-neutral, self-hosted remote-approval + live-monitoring for claude / codex. Approve tool calls, watch runs, take over typing — from Telegram, Feishu, or a web terminal.

npm version CI License: MIT

English · 简体中文

Your claude / codex runs in your terminal as usual. tlive rides the open hook mechanism both vendors support to push status (and, when you turn it on, approval cards) to Telegram / Feishu, and serves a web dashboard + real terminal off your own machine — watch a run, reply to continue, send a screenshot, or take over typing, from any device. Works regardless of subscription or API key; your session and data never leave your machine.

Out of the box tlive watches and notifies (mode: notify, the safe default — it can never hold up a tool call). Flip on remote approval — hold each tool call so you can Allow/Deny it from your phone — with one command: tlive mode full (or let tlive setup offer it).

Warning

v2.0 is a ground-up rewrite — breaking, with no migration. tlive is no longer the Agent-SDK IM bridge of v1.x and earlier: it no longer drives or owns your sessions, and the old bridge model, its config schema, and its commands (workspace, /use, chat-binding, …) are gone and unsupported. Don't try to carry an old ~/.tlive config forward — just run tlive setup fresh. The final SDK-bridge release is preserved at git tag v1.0-sdk-bridge.

Jump to: Quick start · Integration levels · Features · Install · Codex companion · Security · CLI · Config · Architecture

30 seconds to running

npm install -g tlive

tlive setup        # wizard: registers the tlive plugin with Claude Code's/
                    # Codex's own plugin manager (hooks, skill, /tlive:*
                    # commands), then IM credentials — or skip IM entirely
                    # and say "help me configure tlive" inside Claude/Codex
tlive start        # daemon up — prints web URLs + a QR code for your phone

tlive run claude   # (optional) wrap the session → live web terminal + preview card

Scan the QR once — the dashboard lists every session and streams each run. Turn on remote approval (tlive mode full) and, when a tool call needs approval, your IM gets a card with Allow / Deny / Always-allow buttons and the web card lights up red.

The two integration levels

hooks/companion only (claude/codex as usual) wrapped (tlive run claude/codex)
Approval cards (IM + web)
Stop-resume by IM reply ✅ (reply window)
Session card on dashboard ✅ status / last message ✅ + live terminal preview
Real web terminal (xterm) ✅ multi-device, phone keys
IM text injection (quote-reply → typed into the pty)
IM photo/file → agent ✅ (downloaded, path injected)
Web paste/drop upload

Hooks-only always works — wrapping is pure addition. The Approval cards row needs remote approval on (tlive mode full); the default notify mode does everything else — live monitoring, turn-finished / waiting notifications, reply-to-continue, the web terminal — without ever holding a tool call. IM messages carry a label · prefix (the session's directory) but no longer mark wrapped vs. hooks-only visually — the continue card's own "Reply to continue" line makes the distinction moot for what you actually do.

What's in the box

  • Posture — notify (default) / full / off. One coarse switch that sits above every fine toggle. notify (default) watches and notifies but the shim can never hold or block an approval — every prompt stays 100% native (your local terminal dialog, or CC's own auto-deny when headless). full turns on remote approval: tlive holds each tool call so you can answer it from IM / desktop / dashboard (everything the Approvals bullet below describes). off makes every hook a no-op (kill switch — no gating, notifications, monitoring, or daemon autostart). Flip it live with tlive mode off|notify|full; the shim re-reads config on the next hook, so no restart or new session is needed, and tlive status shows the effective mode. Remote approval is opt-in by design — a freshly-installed tool must never be able to silently hang a workflow.
  • Approvals (remote approval — mode: full) — a tool call that needs approval is held so you can answer it from IM buttons or the web card:
    • Parallel, first-answer-wins — on Claude Code the PermissionRequest hook fires alongside the local permission dialog; both are live, and answering at the keyboard resolves the remote card ("answered in terminal") within seconds. Nothing is ever auto-denied — an unanswered card just leaves the local prompt in charge.
    • Grace before sendingapprovals.approvalGraceSec (default 10s, 0 disables) holds the card first, so answering right away means it's never sent; otherwise it stays answerable for ~24h.
    • Codex — tlive spawns/adopts a codex app-server companion and Codex TUIs auto-attach; the remote card and native prompt race the same way, with no window to configure (see Security).
    • Rendering — diffs/commands, risky-pattern flags, secret masking. Telegram cards stay restrained (bold titles, plain-text buttons; the only emoji anywhere is ⚠️ on a risky flag / error), with expandable quotes for long diffs — use a recent Telegram app for best rendering.
    • Power toggles"Always allow <tool>" grants a per-tool pass (in-memory, cleared on restart; on Claude Code it answers the native dialog for you remotely); /trust on|off pauses approvals entirely.
    • Sub-agents pass through by default — a backgrounded/async sub-agent has no parallel local dialog to fall back on, so tlive returns {} and lets CC handle it natively; hold them for a remote answer with approvals.holdSubagents: true.
  • Answer AskUserQuestion from IM (Claude Code only) — CC fires a PermissionRequest for its own question tool; tlive relays it as a single-select or multi-select card (checkboxes, a live Submit (N) count, Skip) instead of Allow/Deny. The local question prompt still renders in parallel and always wins a race, so an answer given at the keyboard is never overridden — Skip just passes the tool through so the local prompt can be answered instead; it is not an auto-approval of anything. Codex has no equivalent concept.
  • Resume — on Stop, reply to the IM message (or the web reply box) and the session keeps going. The card's excerpt sits inside a collapsed expandable quote (headings, lists, tables and code all survive the conversion — nothing is cut mid-word or mid-fence); while that card is still pending, the 60-second idle "waiting for your input" notification is suppressed instead of piling a second message on top of it.
  • Daemon lazy-start — hooks-only sessions no longer need a manual tlive start first: on SessionStart (and when tlive run launches), the daemon is started detached (non-blocking) if it isn't already up. Disable with daemon.autoStart: false; tlive start still works and is unaffected.
  • Failure alerts (Claude Code only)PostToolUseFailure (a tool call errored) and StopFailure (session-level error, e.g. rate-limit/billing) push a ⚠️-prefixed IM message. Pure side-channel, never affects approval decisions; Codex has no equivalent hooks, so this only fires for Claude Code.
  • In-session welcome hint (Claude Code only) — if IM isn't configured yet, SessionStart injects a one-line prompt into the session context nudging you to say "help me configure tlive"; it stops appearing once IM is set up. Not injected for Codex.
  • Web terminaltlive run <cmd> serves the pty at /s/<id>: xterm.js, multi-device with last-input sizing (whoever types owns the grid; everyone else sees a scaled view), screen rebuild for late joiners, soft-keyboard aware layout, view/input modes on touch, draggable key bar with Esc/Tab/⇧Tab/Ctrl-C/…, font size controls, copy-screen modal.
  • Dashboard/ lists sessions: status badge, "stuck Nm" staleness, last assistant message, colored approval cards, live terminal previews, per-session mute, 📎 file upload, reply box.
  • Send anything to the agent — IM quote-reply text, IM photos/files, terminal-page paste/drag-drop, dashboard 📎. All land as local paths in ~/.tlive/inbox (auto-swept: 48 h age / 256 MB total) and are typed into the pty via bracketed paste.

Install: plugins, not config writes

tlive setup (and tlive setup --hooks-only) no longer hand-edits ~/.claude/settings.json or ~/.codex/hooks.json — it orchestrates each vendor's own plugin manager:

  • Claude Code: claude plugin marketplace add <bundled dir> then claude plugin install tlive@tlive --scope user.
  • Codex (if codex is on PATH): codex plugin marketplace add <bundled dir> then codex plugin add tlive@tlive.

The Claude Code plugin bundles the 9 hook events, a tlive skill (usage, diagnostics, security model, under the /tlive:* namespace), and slash commands /tlive:url and /tlive:status. The Codex plugin ships only the skill — Codex has no hooks and needs none; its integration is the app-server companion (see below). The vendor copies the plugin into its own cache (~/.claude/plugins/cache for Claude Code, $CODEX_HOME/plugins/cache/tlive/tlive/local/ for Codex) — after upgrading tlive itself, re-run tlive setup --hooks-only to refresh that copy.

Ran a pre-plugin dev build that wrote hooks directly into vendor config? Remove those entries by hand once (they'd double-fire otherwise) — see the appendix in docs/manual-hooks.md. tlive itself never edits your vendor config files.

Old vendor versions without a plugin CLI: tlive setup detects this (claude plugin list / codex plugin marketplace add failing) and prints a pointer to the manual config appendix: docs/manual-hooks.md — full settings.json hooks block and hooks.json you can paste in by hand.

Uninstalling (npm uninstall -g tlive) best-effort removes the plugin via each vendor's CLI and cleans up any leftover direct-write hooks; your ~/.tlive config and logs are preserved. Full purge and migrating from v0.x/v1 steps: docs/uninstall.md.

Try it from GitHub first (no npm publish needed): claude plugin marketplace add y49/tlive then claude plugin install tlive@tlive pulls the plugin (hooks/skill/commands) straight from the repo's root marketplace.json. You still need the engine itself — npm i -g tlive — for the daemon/CLI the hooks call into.

tlive setup asks which vendor(s) to install the plugin into when it detects both claude and codex on PATH: [1] Claude Code [2] Codex [3] both (default). Plugin registration always runs before the IM credential prompts, and the IM step is fully skippable — press Enter through it and later say "help me configure tlive" inside Claude Code or Codex — or, in Claude Code, run /tlive:setup — to have the AI walk you through it interactively (Codex has no slash commands; use the phrase).

Codex: the app-server companion

Codex has no hooks and no trust step — the entire Claude-style hooks/trust dance above doesn't apply. Instead, tlive takes custody of a codex app-server --listen unix://… process: it adopts one already listening on tlive's socket path, or spawns and owns one (with respawn/backoff) if none is there. Any codex TUI you run auto-attaches to that socket — this is a Codex feature, not something tlive configures per-session.

Over that RPC connection tlive subscribes to Codex's own thread/turn events and drives approvals through ServerRequest: when Codex asks for a permission decision, tlive broadcasts the same request to IM/web and to the native TUI prompt simultaneously — first answer wins, exactly like Claude Code's parallel channel. There is no approval window to configure: the native prompt is never blocked waiting on tlive, so there's nothing that can time out.

If the companion can't be reached (Codex not installed, respawn exhausted its backoff, or you're on win32 where codex app-server isn't wired up yet), tlive status says so plainly (codex: app-server companion unreachable — approvals local-only) and Codex just runs with its normal local approval flow — no IM/web card, no crash, no degraded behavior beyond losing the remote channel.

Why not the official remotes?

Official remotes (Claude Remote Control / Codex mobile / Channels) have structural gaps tlive fills:

  • Cross-agent — one setup for Claude Code and Codex.
  • API-key users — official remotes exclude them; tlive doesn't care.
  • Self-hosted — no vendor cloud in the path; a single token gates the web.
  • Feishu — official channels don't cover it.

tlive deliberately does not try to be "vibe-code from your phone" — the official remotes do that better. tlive is the approval / monitoring / interject layer for sessions you already run.

Security model

  • Web: every HTTP/WS request requires the single token (~/.tlive/web-token, 0600). Default bind is 0.0.0.0 so your phone can reach it on the LAN — the token is the gate. Set web.bind: "127.0.0.1" to go loopback-only. Cards never carry a link to the dashboard. The deep link that used to ship there carried the token itself — full control over every session — and sending it to IM would park that token on the messaging provider's servers permanently. Open the dashboard yourself (over your own tailscale/HTTPS reverse proxy if you need it off the LAN); IM is push, web is pull.
  • IM inbound: fail-closed. Messages/button-taps are dropped unless they come from the configured chat; add allowedSenders for per-user hardening in group chats.
  • /trust on and "Always allow" are power tools: they auto-approve. Both are in-memory and cleared on daemon restart. Prefer per-tool grants over /trust. Your own permissions.deny in Claude settings always wins — hooks cannot override it.
  • Fallback is silence: no configured chat, timeout, or a daemon that's down → the hook emits {} and Claude prompts in your local terminal as if tlive weren't there. On Claude Code the local dialog is live the whole time anyway (parallel channels), so "fallback" just means the remote card goes quiet.
  • Codex approvals are fail-safe by construction — the native prompt is never blocked waiting on tlive (no window, nothing to time out); if the companion is unreachable Codex just runs its own local approval flow and tlive status reports codex: app-server companion unreachable — approvals local-only. When the companion is up, the remote card and the native prompt race — first answer wins, same semantics as Claude Code's parallel channel.

CLI

tlive setup            wizard + registers the vendor plugin(s) (idempotent); --hooks-only
tlive start | stop     daemon lifecycle (stop is idempotent)
tlive status           health, effective mode, web URLs + QR, config paths
tlive logs [-f]        tail the daemon log
tlive run <cmd> …      wrap a process: local terminal + web terminal
tlive url              print the dashboard URL + QR (when a full-screen app hid the run banner)
tlive mode off|notify|full   set posture (see "What's in the box"); takes effect on the next hook
tlive hook <event>     hook shim (called by Claude Code, not by you;
                        Codex has no hooks — see the app-server companion)

setup, start, stop, status, logs, run, url, hook are the frozen surface (locked by tests/contract/); mode and the runtime toggles mute | trust | safe (on|off) and desktop (on|off) are additive.

IM commands: /mute on|off (silence IM notifications), /trust on|off (pause approvals — auto-allow everything), /safe on|off (auto-allow routine ops), /help. Tapping a bare command from the client's command menu replies with on/off buttons instead of an error. Quote-reply any session message to type into that session.

Config (~/.tlive/config.json)

Full annotated config — every field is optional; defaults shown
{
  // posture: "off" | "notify" (default) | "full" (remote approval on).
  // Also set live with `tlive mode …`; unset/unknown falls back to notify.
  "mode": "notify",
  "adapters": {
    "telegram": { "token": "", "chatIdAllowList": ["123"] },
    "feishu":   { "appId": "", "appSecret": "", "chatId": "oc_…" }
  },
  "web": {
    "enabled": true,          // default true
    "bind": "0.0.0.0",        // default; use 127.0.0.1 for loopback-only
    "port": 7681
  },
  "daemon": {
    "autoStart": true         // default true; false disables session-start lazy-start
  },
  "approvals": {
    // remote-approval window in seconds, shared by both vendors. The remote
    // channel runs parallel to the local prompt, so a long window costs
    // nothing — timing out never approves or denies anything, it only
    // forces you back to the keyboard.
    "windowSec": 86200,       // default ~24h (also the max; min 60)
    // grace period before an approval card is sent — answering at the
    // keyboard within this window means it's never sent at all
    "approvalGraceSec": 10,   // default 10s, 0 disables
    // desktop notification (Linux notify-send) when a card goes out —
    // background tool calls render no local dialog while the hook pends, so
    // this is the at-the-computer pointer to the phone card / dashboard
    "desktopNotify": true,    // default true; silent no-op without notify-send
    // how much auto-approves without a card:
    //  "readonly" (default) — only Read/Glob/Grep; everything else asks
    //  "safe"               — also auto-allow routine ops (non-dangerous Bash,
    //                         edits to non-sensitive paths); dangerous ops
    //                         (rm -rf, sudo, curl|sh, sensitive-path writes…),
    //                         MCP/unknown tools, and AskUserQuestion still ask.
    // Cuts the card volume for autonomous / agent-driven runs where there's no
    // local dialog. Toggle live with /safe on|off. Never crosses the danger
    // floor — only /trust on auto-allows dangerous ops.
    "autoApprove": "readonly",
    // hold a backgrounded/async sub-agent's approval for a remote answer too
    // (default false: sub-agents pass through to CC-native handling). Only
    // relevant in mode: full.
    "holdSubagents": false,
    // what a HELD approval does when its window times out with nobody
    // answering: "defer" (default) → pass-through {} (CC-native fallback);
    // "deny" → deny with a "timed out" message so the turn can end and the
    // continuation card can redirect the agent. Never auto-allows.
    "timeoutAction": "defer"
  },
  "allowedSenders": [{ "channel": "telegram", "userId": "42" }]  // optional
}

Tips

  • Persistent sessions: tlive intentionally does not own sessions — tlive run dies with your terminal. Want detach/reattach? Combine: tmux new -s work tlive run claude. tmux keeps it alive; tlive keeps the web/IM layer on it.
  • Scroll on phone: view mode converts touch-drag into wheel events — full-screen TUIs (claude) scroll their transcript exactly like a desktop mouse wheel. Use Ctrl-R on the key bar for claude's transcript mode.
  • Am I wrapped? Wrapped processes see TLIVE_SESSION=<id> in their environment (like $TMUX); tlive run refuses to nest inside a wrapped session. Several wrapped sessions in the SAME directory are fine — each is its own card, and hook traffic from inside a wrapper is routed to that exact card via TLIVE_SESSION.
  • Windows: supported by design (named pipes, ConPTY) but less battle-tested than Linux/macOS — issues welcome.

Architecture

flowchart LR
  A["your <b>claude</b>"] -->|hooks| S["tlive hook shim"]
  B["your <b>codex</b>"] -->|rpc| C["app-server companion"]
  R["tlive run"] -->|owns pty| P["PtyBridge"]
  S -->|IPC| D[("daemon")]
  C --> D
  P --> D
  D --> IM["IM adapters<br/>Telegram · Feishu"]
  D --> W["web · token-gated<br/>dashboard + /s/&lt;id&gt; terminal"]
Loading
  • The daemon never owns sessions; it brokers approvals/resumes, fans out pty bytes, and serves the web.
  • The frozen surface (contracts locked by tests/contract/) is documented in KERNEL.md.

Upgrading from v1.0

v1.0 drove sessions via the Agent SDK; v2.0 is the hook layer (see CHANGELOG.md). Breaking, no migration: re-run tlive setup. v1.0 is preserved at git tag v1.0-sdk-bridge.

Development

git clone https://github.com/y49/tlive
cd tlive
pnpm install
npm run typecheck && npm test && npm run build

License

MIT. See LICENSE. Contributions: CONTRIBUTING.md.

About

Self-hosted remote approvals + live monitoring for Claude Code / Codex — via Telegram, Feishu, or a web terminal. Any subscription or API key.

Topics

Resources

Contributing

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages