Send claude-code prompt via stdin (#141)

Fixes shell escaping.
This commit is contained in:
Solomon
2025-07-11 18:13:10 +01:00
committed by GitHub
parent af972143ab
commit f3a6c3f267

View File

@@ -55,23 +55,21 @@ Task title: {}"#,
// Use shell command for cross-platform compatibility
let (shell_cmd, shell_arg) = get_shell_command();
let claude_command = format!(
"npx -y @anthropic-ai/claude-code \"{}\" -p --dangerously-skip-permissions --verbose --output-format=stream-json",
prompt.replace("\"", "\\\"")
);
// Pass prompt via stdin instead of command line to avoid shell escaping issues
let claude_command = "npx -y @anthropic-ai/claude-code -p --dangerously-skip-permissions --verbose --output-format=stream-json";
let mut command = Command::new(shell_cmd);
command
.kill_on_drop(true)
.stdin(std::process::Stdio::null())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir(worktree_path)
.arg(shell_arg)
.arg(&claude_command)
.arg(claude_command)
.env("NODE_NO_WARNINGS", "1");
let child = command
let mut child = command
.group_spawn() // Create new process group so we can kill entire tree
.map_err(|e| {
crate::executor::SpawnContext::from_command(&command, "Claude")
@@ -80,6 +78,28 @@ Task title: {}"#,
.spawn_error(e)
})?;
// Write prompt to stdin safely
if let Some(mut stdin) = child.inner().stdin.take() {
use tokio::io::AsyncWriteExt;
tracing::debug!(
"Writing prompt to Claude stdin for task {}: {:?}",
task_id,
prompt
);
stdin.write_all(prompt.as_bytes()).await.map_err(|e| {
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
.with_task(task_id, Some(task.title.clone()))
.with_context("Failed to write prompt to Claude CLI stdin");
ExecutorError::spawn_failed(e, context)
})?;
stdin.shutdown().await.map_err(|e| {
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
.with_task(task_id, Some(task.title.clone()))
.with_context("Failed to close Claude CLI stdin");
ExecutorError::spawn_failed(e, context)
})?;
}
Ok(child)
}
@@ -487,23 +507,23 @@ impl Executor for ClaudeFollowupExecutor {
) -> Result<AsyncGroupChild, ExecutorError> {
// Use shell command for cross-platform compatibility
let (shell_cmd, shell_arg) = get_shell_command();
// Pass prompt via stdin instead of command line to avoid shell escaping issues
let claude_command = format!(
"npx -y @anthropic-ai/claude-code \"{}\" -p --dangerously-skip-permissions --verbose --output-format=stream-json --resume={}",
self.prompt.replace("\"", "\\\""),
"npx -y @anthropic-ai/claude-code -p --dangerously-skip-permissions --verbose --output-format=stream-json --resume={}",
self.session_id
);
let mut command = Command::new(shell_cmd);
command
.kill_on_drop(true)
.stdin(std::process::Stdio::null())
.stdin(std::process::Stdio::piped())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.current_dir(worktree_path)
.arg(shell_arg)
.arg(&claude_command);
let child = command
let mut child = command
.group_spawn() // Create new process group so we can kill entire tree
.map_err(|e| {
crate::executor::SpawnContext::from_command(&command, "Claude")
@@ -514,6 +534,32 @@ impl Executor for ClaudeFollowupExecutor {
.spawn_error(e)
})?;
// Write prompt to stdin safely
if let Some(mut stdin) = child.inner().stdin.take() {
use tokio::io::AsyncWriteExt;
tracing::debug!(
"Writing prompt to Claude stdin for session {}: {:?}",
self.session_id,
self.prompt
);
stdin.write_all(self.prompt.as_bytes()).await.map_err(|e| {
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
.with_context(format!(
"Failed to write prompt to Claude CLI stdin for session {}",
self.session_id
));
ExecutorError::spawn_failed(e, context)
})?;
stdin.shutdown().await.map_err(|e| {
let context = crate::executor::SpawnContext::from_command(&command, "Claude")
.with_context(format!(
"Failed to close Claude CLI stdin for session {}",
self.session_id
));
ExecutorError::spawn_failed(e, context)
})?;
}
Ok(child)
}