Skip to content

Commit de22062

Browse files
authored
security: replace execSync with execFileSync in tab-setter.ts (#1046)
Applies execFileSync pattern to Releases/v4.0.3/.claude/hooks/lib/tab-setter.ts. Equivalent fix already applied to upcoming 5.1.0 source which carries 7 additional cmux call sites that weren't in v4.0.3.
1 parent 9fb9c86 commit de22062

1 file changed

Lines changed: 23 additions & 30 deletions

File tree

‎Releases/v4.0.3/.claude/hooks/lib/tab-setter.ts‎

Lines changed: 23 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010

1111
import { existsSync, writeFileSync, mkdirSync, readdirSync, unlinkSync, readFileSync } from 'fs';
1212
import { join } from 'path';
13-
import { execSync } from 'child_process';
13+
import { execFileSync } from 'child_process';
1414
import { TAB_COLORS, PHASE_TAB_CONFIG, ACTIVE_TAB_BG, ACTIVE_TAB_FG, INACTIVE_TAB_FG, type TabState, type AlgorithmTabPhase } from './tab-constants';
1515
import { paiPath } from './paths';
1616

@@ -121,12 +121,19 @@ function cleanupStaleStateFiles(): void {
121121
const defaultSocket = `/tmp/kitty-${process.env.USER}`;
122122
const socketPath = process.env.KITTY_LISTEN_ON || (existsSync(defaultSocket) ? `unix:${defaultSocket}` : null);
123123
if (!socketPath) return; // No socket — skip cleanup to avoid escape sequence IPC
124-
const liveOutput = execSync(`kitten @ --to="${socketPath}" ls 2>/dev/null | jq -r ".[].tabs[].windows[].id" 2>/dev/null`, {
124+
// Validate socket path before passing to execFileSync (prevent injection via env var)
125+
if (!/^[a-zA-Z0-9_\-]+:[a-zA-Z0-9/_\-.]+$/.test(socketPath)) return;
126+
const rawLs = execFileSync('kitten', ['@', `--to=${socketPath}`, 'ls'], {
125127
encoding: 'utf-8', timeout: 2000,
126128
}).trim();
127-
if (!liveOutput) return;
129+
if (!rawLs) return;
128130

129-
const liveIds = new Set(liveOutput.split('\n').map(id => id.trim()));
131+
const liveIds = new Set<string>();
132+
try {
133+
const osWindows = JSON.parse(rawLs) as Array<{ tabs: Array<{ windows: Array<{ id: number }> }> }>;
134+
for (const os of osWindows) for (const tab of os.tabs) for (const win of tab.windows) liveIds.add(String(win.id));
135+
} catch { return; }
136+
if (liveIds.size === 0) return;
130137

131138
for (const file of files) {
132139
const winId = file.replace('.json', '');
@@ -156,27 +163,20 @@ export function setTabState(opts: SetTabOptions): void {
156163
return;
157164
}
158165

159-
const escaped = title.replace(/"/g, '\\"');
160166
// Set BOTH tab title AND window title. Kitty's tab_title_template uses
161167
// {active_window.title} (the window title). OSC escape codes from Claude Code
162168
// reset set-tab-title overrides, so the template falls back to window title.
163169
// By setting both, our title survives OSC resets.
164-
const toFlag = `--to="${kittyEnv.listenOn}"`;
165-
console.error(`[tab-setter] Setting tab: "${escaped}" with toFlag: ${toFlag}`);
166-
execSync(`kitten @ ${toFlag} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
167-
execSync(`kitten @ ${toFlag} set-window-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
170+
const toArg = `--to=${kittyEnv.listenOn}`;
171+
console.error(`[tab-setter] Setting tab: "${title}" via ${toArg}`);
172+
execFileSync('kitten', ['@', toArg, 'set-tab-title', title], { stdio: 'ignore', timeout: 2000 });
173+
execFileSync('kitten', ['@', toArg, 'set-window-title', title], { stdio: 'ignore', timeout: 2000 });
168174

169175
// For idle state, reset ALL colors to Kitty defaults (no lingering backgrounds)
170176
if (state === 'idle') {
171-
execSync(
172-
`kitten @ ${toFlag} set-tab-color --self active_bg=none active_fg=none inactive_bg=none inactive_fg=none`,
173-
{ stdio: 'ignore', timeout: 2000 }
174-
);
177+
execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', 'active_bg=none', 'active_fg=none', 'inactive_bg=none', 'inactive_fg=none'], { stdio: 'ignore', timeout: 2000 });
175178
} else {
176-
execSync(
177-
`kitten @ ${toFlag} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TAB_FG} inactive_bg=${colors.inactiveBg} inactive_fg=${INACTIVE_TAB_FG}`,
178-
{ stdio: 'ignore', timeout: 2000 }
179-
);
179+
execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', `active_bg=${ACTIVE_TAB_BG}`, `active_fg=${ACTIVE_TAB_FG}`, `inactive_bg=${colors.inactiveBg}`, `inactive_fg=${INACTIVE_TAB_FG}`], { stdio: 'ignore', timeout: 2000 });
180180
}
181181
console.error(`[tab-setter] Tab commands completed successfully`);
182182
} catch (err) {
@@ -323,24 +323,17 @@ export function setPhaseTab(phase: AlgorithmTabPhase, sessionId: string, summary
323323
return;
324324
}
325325

326-
const escaped = title.replace(/"/g, '\\"');
327-
const toFlag = `--to="${kittyEnv.listenOn}"`;
326+
const toArg = `--to=${kittyEnv.listenOn}`;
328327

329-
execSync(`kitten @ ${toFlag} set-tab-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
330-
execSync(`kitten @ ${toFlag} set-window-title "${escaped}"`, { stdio: 'ignore', timeout: 2000 });
328+
execFileSync('kitten', ['@', toArg, 'set-tab-title', title], { stdio: 'ignore', timeout: 2000 });
329+
execFileSync('kitten', ['@', toArg, 'set-window-title', title], { stdio: 'ignore', timeout: 2000 });
331330

332331
if (phase === 'IDLE') {
333-
execSync(
334-
`kitten @ ${toFlag} set-tab-color --self active_bg=none active_fg=none inactive_bg=none inactive_fg=none`,
335-
{ stdio: 'ignore', timeout: 2000 }
336-
);
332+
execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', 'active_bg=none', 'active_fg=none', 'inactive_bg=none', 'inactive_fg=none'], { stdio: 'ignore', timeout: 2000 });
337333
} else {
338-
execSync(
339-
`kitten @ ${toFlag} set-tab-color --self active_bg=${ACTIVE_TAB_BG} active_fg=${ACTIVE_TAB_FG} inactive_bg=${config.inactiveBg} inactive_fg=${INACTIVE_TAB_FG}`,
340-
{ stdio: 'ignore', timeout: 2000 }
341-
);
334+
execFileSync('kitten', ['@', toArg, 'set-tab-color', '--self', `active_bg=${ACTIVE_TAB_BG}`, `active_fg=${ACTIVE_TAB_FG}`, `inactive_bg=${config.inactiveBg}`, `inactive_fg=${INACTIVE_TAB_FG}`], { stdio: 'ignore', timeout: 2000 });
342335
}
343-
console.error(`[tab-setter] Phase tab: "${escaped}" (${phase}, bg=${config.inactiveBg})`);
336+
console.error(`[tab-setter] Phase tab: "${title}" (${phase}, bg=${config.inactiveBg})`);
344337
} catch (err) {
345338
console.error(`[tab-setter] Error setting phase tab:`, err);
346339
}

0 commit comments

Comments
 (0)