fix: v0.5.0 security and robustness fixes from the 2026-10-07 review
- Ask Claude: strip control characters, send as bracketed paste (closes #2) - end: forget before kill, so ending from inside a session leaves no ghost (closes #3) - reconnects use `attach --existing` and never resurrect ended/renamed sessions (closes #4) - repo: git --no-optional-locks, no safe.directory override, refresh on connect/bell/60s (closes #5) - uploads: unique names via mktemp, cleanup trap on hangup (closes #6) - clipboard: write-only OSC 52 provider, tmux allow-passthrough off (closes #7) - uploads: size check up front, streamed slices, idle timeout (closes #9) - --list: path last in TSV rows, tolerant numbers (closes #10) - tokens via curl config on stdin; userinfo stripped from remote URLs (closes #11) - ttyd max clients 32 and at most 2 concurrent side-channel calls (#8; BroadcastChannel polling still open) - tmux: fixed terminal-features index (no duplicates on re-source), no status-right clock Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+17
-11
@@ -3,7 +3,7 @@ import { createTerminal, loadFonts } from './term.js';
|
||||
import {
|
||||
TtydConnection, fetchAlerts, fetchRepo, fetchToken, uploadFile, AuthExpiredError, UnauthorizedError,
|
||||
} from './connection.js';
|
||||
import { parseTitle, sessionFromSearch } from './protocol.js';
|
||||
import { parseTitle, pasteSafe, sessionFromSearch } from './protocol.js';
|
||||
import { SessionsPanel, rememberSession, sessionStatus } from './sessions.js';
|
||||
import { renderAlerts } from './alerts.js';
|
||||
import { renderContext } from './context.js';
|
||||
@@ -246,7 +246,9 @@ function updateUrl(session) {
|
||||
|
||||
// ---------------------------------------------------------------- connection
|
||||
|
||||
function connect(session) {
|
||||
// reconnect: automatic re-attach after a drop. It must not re-create a session
|
||||
// that was ended or renamed meanwhile, so it asks for --existing (devdesk#4).
|
||||
function connect(session, { reconnect = false } = {}) {
|
||||
clearTimeout(state.retryTimer);
|
||||
state.conn?.dispose();
|
||||
state.session = session;
|
||||
@@ -260,7 +262,7 @@ function connect(session) {
|
||||
term.reset();
|
||||
|
||||
const conn = new TtydConnection({
|
||||
arg: session,
|
||||
arg: session && reconnect ? ['--existing', session] : session,
|
||||
size: () => ({ cols: term.cols, rows: term.rows }),
|
||||
onOpen: () => {
|
||||
state.openedAt = Date.now();
|
||||
@@ -353,13 +355,13 @@ function scheduleReconnect() {
|
||||
showOverlay({
|
||||
title: 'Reconnecting',
|
||||
text: `Connection lost; Claude keeps running on the server. Retrying in ${seconds}s (attempt ${state.attempts}).`,
|
||||
actions: [{ label: 'Retry now', primary: true, run: () => connect(state.session) }, pickerAction],
|
||||
actions: [{ label: 'Retry now', primary: true, run: () => connect(state.session, { reconnect: true }) }, pickerAction],
|
||||
});
|
||||
state.retryTimer = setTimeout(() => connect(state.session), delay);
|
||||
state.retryTimer = setTimeout(() => connect(state.session, { reconnect: true }), delay);
|
||||
}
|
||||
|
||||
function retrySoon() {
|
||||
if (state.retryTimer && !state.conn?.isOpen) connect(state.session);
|
||||
if (state.retryTimer && !state.conn?.isOpen) connect(state.session, { reconnect: true });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- titles & bell
|
||||
@@ -474,6 +476,10 @@ function wireChrome() {
|
||||
setInterval(() => {
|
||||
if (!document.hidden && ui.body.classList.contains('dd-sidebar-open')) refreshAlerts();
|
||||
}, 30 * 1000);
|
||||
// Repo status: on connect, after Claude's turns, and once a minute (devdesk#5, #8).
|
||||
setInterval(() => {
|
||||
if (!document.hidden && ui.body.classList.contains('dd-sidebar-open')) refreshRepo();
|
||||
}, 60 * 1000);
|
||||
ui.attention.addEventListener('click', () => {
|
||||
if (ui.attention.dataset.target) connect(ui.attention.dataset.target);
|
||||
});
|
||||
@@ -500,6 +506,7 @@ async function boot() {
|
||||
term.onBell(() => {
|
||||
notify.ring(state.session, ui.paneTitle.textContent);
|
||||
refreshSoon(); // Claude finished a turn: context numbers just changed
|
||||
setTimeout(refreshRepo, 2000); // …and maybe committed or pushed
|
||||
});
|
||||
|
||||
let fitFrame = 0;
|
||||
@@ -537,14 +544,13 @@ async function boot() {
|
||||
ui.sessionChip.textContent = to;
|
||||
sessionsPanel.setCurrent(to);
|
||||
},
|
||||
onData: () => {
|
||||
showContext();
|
||||
refreshRepo();
|
||||
},
|
||||
onData: () => showContext(),
|
||||
},
|
||||
});
|
||||
bindAskClaude($('sidebar'), (text) => {
|
||||
state.conn?.sendInput(text);
|
||||
// Alert text is untrusted: no control characters (a newline would run it in a
|
||||
// shell window, CR would submit), and bracketed paste around it (devdesk#2).
|
||||
term.paste(pasteSafe(text));
|
||||
term.focus();
|
||||
});
|
||||
wireChrome();
|
||||
|
||||
Reference in New Issue
Block a user