diff --git a/internal/cli/run.go b/internal/cli/run.go index 5ff832bd6..364d4751c 100644 --- a/internal/cli/run.go +++ b/internal/cli/run.go @@ -1097,34 +1097,38 @@ func buildClaudeCommand(agentName, model, repoDir string, pluginDirs []string, d // Defense-in-depth: escape single quotes even though Validate() rejects them. safe := strings.ReplaceAll(agentName, "'", "'\\''") - modelFlag := "" - if model != "" { - modelFlag = fmt.Sprintf("--model '%s' ", strings.ReplaceAll(model, "'", "'\\''")) - } - - var pluginDirParts []string - for _, pd := range pluginDirs { - pluginDirParts = append(pluginDirParts, fmt.Sprintf("--plugin-dir '%s'", strings.ReplaceAll(pd, "'", "'\\''"))) - } - pluginDirFlags := "" - if len(pluginDirParts) > 0 { - pluginDirFlags = strings.Join(pluginDirParts, " ") + " " + claudeParts := []string{ + "claude", + "--print", + "--verbose", + "--output-format stream-json", } - debugFlags := "" if debug != "" { - debugFlags = fmt.Sprintf("--debug-file '%s/%s' ", sandbox.SandboxWorkspace, claudeDebugLog) + claudeParts = append(claudeParts, fmt.Sprintf("--debug-file '%s/%s'", sandbox.SandboxWorkspace, claudeDebugLog)) if debug != "*" { - debugFlags += fmt.Sprintf("--debug '%s' ", strings.ReplaceAll(debug, "'", "'\\''")) + claudeParts = append(claudeParts, fmt.Sprintf("--debug '%s'", strings.ReplaceAll(debug, "'", "'\\''"))) } } + if model != "" { + claudeParts = append(claudeParts, fmt.Sprintf("--model '%s'", strings.ReplaceAll(model, "'", "'\\''"))) + } + + for _, pd := range pluginDirs { + claudeParts = append(claudeParts, fmt.Sprintf("--plugin-dir '%s'", strings.ReplaceAll(pd, "'", "'\\''"))) + } + claudeParts = append(claudeParts, + fmt.Sprintf("--agent '%s'", safe), + "--dangerously-skip-permissions 'Run the agent task'", + ) + return fmt.Sprintf( // --verbose increases log output in the job log. If artifact upload is // added to this workflow, consider whether verbose output should be // redacted or made conditional via an env var. - "cd %s && . %s && claude --print --verbose --output-format stream-json %s%s%s--agent '%s' --dangerously-skip-permissions 'Run the agent task'", - repoDir, envFile, debugFlags, modelFlag, pluginDirFlags, safe, + "cd %s && . %s && %s", + repoDir, envFile, strings.Join(claudeParts, " "), ) } diff --git a/internal/cli/run_test.go b/internal/cli/run_test.go index ca05647f5..bc1ec4019 100644 --- a/internal/cli/run_test.go +++ b/internal/cli/run_test.go @@ -147,6 +147,26 @@ func TestBuildClaudeCommand_DebugEscapesQuotes(t *testing.T) { assert.Contains(t, cmd, "--debug 'api'\\''hooks'") } +func TestBuildClaudeCommand_UsesSingleSpacesAroundOptionalFlags(t *testing.T) { + cmd := buildClaudeCommand("agent", "sonnet", "/tmp/workspace/repo", []string{ + "/tmp/claude-config/plugins/gopls-lsp", + "/tmp/claude-config/plugins/other-lsp", + }, "api,hooks") + + assert.Contains(t, cmd, "--output-format stream-json --debug-file") + assert.Contains(t, cmd, "claude-debug.log' --debug 'api,hooks' --model 'sonnet'") + assert.Contains(t, cmd, "--model 'sonnet' --plugin-dir '/tmp/claude-config/plugins/gopls-lsp'") + assert.Contains(t, cmd, "--plugin-dir '/tmp/claude-config/plugins/other-lsp' --agent 'agent'") + assert.NotContains(t, cmd, " ") +} + +func TestBuildClaudeCommand_UsesSingleSpacesWithoutOptionalFlags(t *testing.T) { + cmd := buildClaudeCommand("agent", "", "/tmp/workspace/repo", nil, "") + + assert.Contains(t, cmd, "--output-format stream-json --agent 'agent'") + assert.NotContains(t, cmd, " ") +} + func TestBuildPluginConfigs_SinglePlugin(t *testing.T) { dir := t.TempDir() pluginDir := filepath.Join(dir, "gopls-lsp")