diff --git a/crates/forge-attractor/src/backends/agent_provider.rs b/crates/forge-attractor/src/backends/agent_provider.rs index f2d9a46..715805c 100644 --- a/crates/forge-attractor/src/backends/agent_provider.rs +++ b/crates/forge-attractor/src/backends/agent_provider.rs @@ -46,6 +46,10 @@ impl AgentSubmitter for AgentProviderSubmitter { model_override: options.model, reasoning_effort: options.reasoning_effort, system_prompt_override: options.system_prompt_override, + // Pipeline stages run with stdin=null by design. CLI adapters + // (Claude Code, etc.) need permission prompts skipped or every + // tool call gets silently denied. See AgentRunOptions doc. + auto_approve_tools: true, ..Default::default() }; diff --git a/crates/forge-llm/src/agent_provider.rs b/crates/forge-llm/src/agent_provider.rs index ce14b33..ee9f150 100644 --- a/crates/forge-llm/src/agent_provider.rs +++ b/crates/forge-llm/src/agent_provider.rs @@ -53,6 +53,17 @@ pub struct AgentRunOptions { pub env_vars: Option>, /// Real-time event callback for observability. pub on_event: Option>, + /// Skip per-tool-call permission prompts. + /// + /// Required for CLI adapters (Claude Code, Codex, Gemini) that run with + /// `stdin=null`: without this they hang at the first permission prompt + /// and every tool call gets silently denied. Defaults to `false` so + /// library consumers explicitly opt in; the pipeline runtime + /// (`AgentProviderSubmitter`) sets it to `true` because pipeline stages + /// run non-interactively by design. + /// + /// Maps to `--dangerously-skip-permissions` on the Claude Code adapter. + pub auto_approve_tools: bool, } impl std::fmt::Debug for AgentRunOptions { @@ -66,6 +77,7 @@ impl std::fmt::Debug for AgentRunOptions { .field("system_prompt_override", &self.system_prompt_override) .field("env_vars", &self.env_vars) .field("on_event", &self.on_event.as_ref().map(|_| "...")) + .field("auto_approve_tools", &self.auto_approve_tools) .finish() } } diff --git a/crates/forge-llm/src/cli_adapters/claude_code.rs b/crates/forge-llm/src/cli_adapters/claude_code.rs index 179fd53..d209760 100644 --- a/crates/forge-llm/src/cli_adapters/claude_code.rs +++ b/crates/forge-llm/src/cli_adapters/claude_code.rs @@ -46,6 +46,16 @@ impl ClaudeCodeAgentProvider { .arg("stream-json") .arg("--verbose"); + // The spawned session has stdin=null so it cannot answer permission + // prompts. With auto_approve_tools=false (the default), every tool + // call gets silently denied — the stage looks "complete" but the + // tool_result events all have is_error=true and the agent reports + // partial_success without doing real work. Callers running stages + // non-interactively must opt in via AgentRunOptions::auto_approve_tools. + if options.auto_approve_tools { + cmd.arg("--dangerously-skip-permissions"); + } + let model = options .model_override .as_deref()