From bb117658373cddaf3d69661fe63a510ae3d13fa8 Mon Sep 17 00:00:00 2001 From: Herik Webb Date: Mon, 25 May 2026 11:35:32 -0400 Subject: [PATCH] fix(frontend): quote Claude resume commands Route Claude session resume launches through the shared command builder and shell-quote the transcript-derived session id before writing the command to a terminal. This prevents malicious session metadata from breaking out of claude --resume and executing shell commands when a user resumes a listed session. --- src/index.ts | 8 ++++---- src/utils.ts | 5 +++-- tests/ts/utils.test.ts | 14 +++++++++++--- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/src/index.ts b/src/index.ts index 3073633..e3c65a9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -106,6 +106,7 @@ import { cellOutputHasError, chooseWorkspaceDirectory, compareSelections, + buildResumeCommand, extractLLMGeneratedCode, getSelectionInEditor, getTokenCount, @@ -1403,10 +1404,9 @@ const plugin: JupyterFrontEndPlugin = { return React.createElement(LauncherPicker, { onSessionSelected: (session: IClaudeSessionInfo) => { dialog.close(); - const cmd = session.cwd - ? `cd ${session.cwd} && claude --resume ${session.session_id}` - : `claude --resume ${session.session_id}`; - launchCliInTerminal(cmd); + launchCliInTerminal( + buildResumeCommand(session.cwd ?? '', session.session_id) + ); } }); } diff --git a/src/utils.ts b/src/utils.ts index 2641729..9cc23fa 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -407,10 +407,11 @@ export function safeAnchorUri(uri: string | undefined | null): string | null { * user happens to be in the JupyterLab working directory. */ export function buildResumeCommand(cwd: string, sessionId: string): string { + const quotedSessionId = shellSingleQuote(sessionId); if (!cwd) { - return `claude --resume ${sessionId}`; + return `claude --resume ${quotedSessionId}`; } - return `cd ${shellSingleQuote(cwd)} && claude --resume ${sessionId}`; + return `cd ${shellSingleQuote(cwd)} && claude --resume ${quotedSessionId}`; } /** diff --git a/tests/ts/utils.test.ts b/tests/ts/utils.test.ts index 85f4bec..1d56b04 100644 --- a/tests/ts/utils.test.ts +++ b/tests/ts/utils.test.ts @@ -480,18 +480,26 @@ describe('shellSingleQuote', () => { describe('buildResumeCommand', () => { it('wraps cd around the resume invocation when cwd is provided', () => { expect(buildResumeCommand('/tmp/proj', 'abc-123')).toBe( - "cd '/tmp/proj' && claude --resume abc-123" + "cd '/tmp/proj' && claude --resume 'abc-123'" ); }); it('quotes paths with spaces correctly', () => { expect(buildResumeCommand('/Users/me/My Project', 'xyz')).toBe( - "cd '/Users/me/My Project' && claude --resume xyz" + "cd '/Users/me/My Project' && claude --resume 'xyz'" + ); + }); + + it('quotes session ids before shell interpolation', () => { + expect(buildResumeCommand('/tmp/proj', "abc'; touch /tmp/pwned; '")).toBe( + "cd '/tmp/proj' && claude --resume 'abc'\\''; touch /tmp/pwned; '\\'''" ); }); it('falls back to a bare resume when cwd is empty', () => { - expect(buildResumeCommand('', 'abc-123')).toBe('claude --resume abc-123'); + expect(buildResumeCommand('', 'abc-123')).toBe( + "claude --resume 'abc-123'" + ); }); });