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'" + ); }); });