diff --git a/main.js b/main.js index 20b7f954..d0d403b5 100644 --- a/main.js +++ b/main.js @@ -1391,6 +1391,16 @@ ipcMain.handle('open-terminal', async (_event, sessionId, projectPath, isNew, se let claudeCmd = harness.binary; if (cliArgs.length) claudeCmd += ' ' + quoteArgvForShell(shell, cliArgs); + // customArgs is raw shell too, like preLaunchCmd below: the user's shell + // does the quoting, e.g. --settings '{"outputStyle": "Learning"}' + if (sessionOptions?.customArgs) { + const custom = String(sessionOptions.customArgs); + if (/[\r\n]/.test(custom)) { + return { ok: false, error: 'customArgs must not contain newlines' }; + } + claudeCmd += ' ' + custom; + } + // preLaunchCmd is raw shell by design (e.g. "aws-vault exec profile --") — block newlines only if (sessionOptions?.preLaunchCmd) { const pre = String(sessionOptions.preLaunchCmd); diff --git a/projects.js b/projects.js index 26c990ad..ee23c2ae 100644 --- a/projects.js +++ b/projects.js @@ -1347,7 +1347,7 @@ function legacyScheduleConfig(cli = {}) { addDirs: cli['add-dirs'] || '', dangerouslySkipPermissions: false, worktree: false, worktreeName: '', chrome: false, mcpEmulation: false, - preLaunchCmd: '', + preLaunchCmd: '', customArgs: '', // The old runner passed --model. Effort did not exist, so it is pinned // unset rather than picking up a folder default the old run never had. model: cli.model || '', diff --git a/public/session-config.js b/public/session-config.js index 52ba0b1b..005dc205 100644 --- a/public/session-config.js +++ b/public/session-config.js @@ -35,6 +35,7 @@ { key: 'model', label: 'Model', type: 'text', default: '', more: true, placeholder: 'default', suggestions: ['fable', 'opus', 'sonnet'], description: "Blank uses Claude's default. An alias or a full model name" }, { key: 'effort', label: 'Effort', type: 'select', default: '', more: true, choices: CLAUDE_EFFORTS, description: "Default uses Claude's own setting" }, { key: 'allowedTools', label: 'Allowed Tools', type: 'text', default: '', more: true, wide: true, description: 'Tools allowed without a permission prompt (comma-separated)' }, + { key: 'customArgs', label: 'Custom Arguments', type: 'text', default: '', more: true, wide: true, placeholder: `e.g. --settings '{"outputStyle": "Learning"}'`, description: 'Appended to the CLI command, as shell' }, { key: 'appendSystemPrompt', label: 'Additional System Prompt', type: 'textarea', default: '', more: true, wide: true, description: 'Instructions appended to Claude’s system prompt' }, { key: 'worktree', label: 'Worktree', type: 'boolean', default: false, description: 'Run each new session in an isolated git worktree' }, { key: 'worktreeName', label: 'Worktree Name', type: 'text', default: '', placeholder: 'name (optional)' }, diff --git a/test/session-config.test.js b/test/session-config.test.js index 344b14a3..ee911bd1 100644 --- a/test/session-config.test.js +++ b/test/session-config.test.js @@ -75,9 +75,9 @@ test('scheduled launches use current folder defaults with only the selected CLI test('optional settings wait behind More options until they hold a value', () => { const empty = SessionConfig.resolveOptions('claude', {}, {}); - assert.deepEqual(SessionConfig.fieldsBehindMore('claude', empty), ['model', 'effort', 'allowedTools', 'appendSystemPrompt', 'preLaunchCmd', 'addDirs']); + assert.deepEqual(SessionConfig.fieldsBehindMore('claude', empty), ['model', 'effort', 'allowedTools', 'customArgs', 'appendSystemPrompt', 'preLaunchCmd', 'addDirs']); const filled = SessionConfig.resolveOptions('claude', { preLaunchCmd: 'aws-vault exec prod --' }, { model: 'opus' }); - assert.deepEqual(SessionConfig.fieldsBehindMore('claude', filled), ['effort', 'allowedTools', 'appendSystemPrompt', 'addDirs'], + assert.deepEqual(SessionConfig.fieldsBehindMore('claude', filled), ['effort', 'allowedTools', 'customArgs', 'appendSystemPrompt', 'addDirs'], 'a saved model and a folder pre-launch command stay in view'); assert.deepEqual(SessionConfig.fieldsBehindMore('codex', SessionConfig.resolveOptions('codex', {}, {})), ['codexModel', 'codexEffort', 'preLaunchCmd', 'addDirs']); assert.ok(!SessionConfig.fieldsBehindMore('claude', empty).includes('permissionMode'), 'the main settings are never tucked away');