Claude Code gets its message on stdin, so a long brief starts on Windows (#430)

Claude Code's message goes to its stdin instead of argv, so a builder brief over Windows' 32,767-character command-line limit no longer stops the build with os error 206. Fixes #402.
This commit is contained in:
BFlat
2026-11-02 23:34:47 +07:00
committed by GitHub
parent 0c0a776ed6
commit 3357daf48e
4 changed files with 50 additions and 12 deletions
+10 -10
View File
@@ -66,6 +66,11 @@ pub trait AgentAdapter: Send + Sync {
fn answers_questions(&self) -> bool {
false
}
/// The message goes in on stdin, not in argv: a long one passes Windows' command-line limit
/// ("The filename or extension is too long", os error 206, #402).
fn prompt_on_stdin(&self) -> bool {
false
}
/// The process for a local run of `program` (an adapter may clean its environment).
fn process(&self, program: &str) -> Process {
Process::new(program)
@@ -169,7 +174,6 @@ const CHANGING_TOOLS: &[&str] =
fn question_args(mut a: Vec<String>, cx: &ArgsCx) -> Vec<String> {
a.extend([
"-p".into(),
cx.prompt.to_string(),
"--output-format".into(),
"stream-json".into(),
"--verbose".into(),
@@ -201,6 +205,10 @@ impl AgentAdapter for ClaudeCode {
true
}
fn prompt_on_stdin(&self) -> bool {
true
}
fn check_tool_server(&self, stop: &CancelToken) -> Result<(), ToolError> {
cli_can_start(craft_anything_platform::self_cli_path().as_deref(), stop)
}
@@ -211,15 +219,7 @@ impl AgentAdapter for ClaudeCode {
return question_args(a, cx);
}
let mcp_json = json!({"mcpServers": {MCP_NAME: {"command": cx.mcp.command, "args": cx.mcp.args}}}).to_string();
a.extend([
"-p".into(),
cx.prompt.to_string(),
"--output-format".into(),
"stream-json".into(),
"--verbose".into(),
"--append-system-prompt".into(),
cx.preamble.to_string(),
]);
a.extend(["-p".into(), "--output-format".into(), "stream-json".into(), "--verbose".into(), "--append-system-prompt".into(), cx.preamble.to_string()]);
// Opus 5.5 everywhere unless config.toml names another model on purpose; the effort
// goes in through the environment (see `run_cli`).
a.extend(["--model".into(), cx.options.cli_model.clone().unwrap_or_else(|| crate::anthropic::DEFAULT_MODEL.into())]);
+32 -2
View File
@@ -551,6 +551,9 @@ fn run_cli(spec: &CliSpec, text: &str, tx: &Sender<AgentEvent>, stop: &Stop, ses
if let Some(p) = spec.project.as_ref().filter(|_| remote.is_none()) {
proc = proc.cwd(p);
}
if adapter.prompt_on_stdin() {
proc = proc.stdin_bytes(text);
}
let result = proc.run();
let p = parser.lock().unwrap_or_else(PoisonError::into_inner);
if let Some(id) = &p.session_id {
@@ -789,7 +792,8 @@ mod tests {
fn claude_args() {
let a = build_args(adapter_for("claude-code").as_ref(), &BackendOptions::default(), "-what?", "PRE", &mcp(), Some("sess-1"), Access::Analyse);
assert_eq!(a[0], "-p");
assert_eq!(a[1], " -what?");
// #402: the message goes in on stdin, so a long one can't pass Windows' command-line limit.
assert!(adapter_for("claude-code").prompt_on_stdin() && !a.iter().any(|x| x.contains("what?")), "{a:?}");
let pos = |f: &str| a.iter().position(|x| x == f).unwrap();
assert_eq!(a[pos("--output-format") + 1], "stream-json");
assert!(a.contains(&"--verbose".to_string()));
@@ -801,7 +805,7 @@ mod tests {
assert_eq!(b[b.iter().position(|x| x == "--model").unwrap() + 1], "opus");
let cfg: Value = serde_json::from_str(&a[pos("--mcp-config") + 1]).unwrap();
assert_eq!(cfg["mcpServers"]["craft-anything"]["args"][0], "mcp");
// variadic flags come last so the prompt is never swallowed
// variadic flags come last: nothing after them is read as one of their values
assert!(pos("--mcp-config") > pos("--resume"));
}
@@ -1080,6 +1084,32 @@ mod tests {
assert!(text.contains("Request"), "preamble is passed too");
}
/// #402: a 34,641-character message in argv passed Windows' command-line limit (os error 206);
/// Claude Code reads it from stdin instead.
#[cfg(unix)]
#[test]
fn claude_code_reads_a_long_message_on_stdin() {
use std::os::unix::fs::PermissionsExt;
let dir = tempfile::tempdir().unwrap();
let program = dir.path().join("claude");
let reply = r#"{"type":"assistant","message":{"content":[{"type":"text","text":"read %s bytes"}]}}\n"#;
std::fs::write(&program, format!("#!/bin/sh\nn=$(wc -c)\nprintf '{reply}' $n\n")).unwrap();
std::fs::set_permissions(&program, std::fs::Permissions::from_mode(0o755)).unwrap();
let config =
BackendConfig { name: "Claude".into(), kind: BackendKind::AgentCli, model: "claude-code".into(), base_url: None, ssh_host: None, enabled: true };
let options = BackendOptions { command: Some(program.display().to_string()), ..Default::default() };
let mut c = CliConversation::new(CliSpec::new(config, options, false, None));
c.send(&"a".repeat(40_000));
let deadline = std::time::Instant::now() + Duration::from_secs(10);
let mut all = Vec::new();
while std::time::Instant::now() < deadline && !all.iter().any(|e| matches!(e, AgentEvent::TurnDone { .. })) {
all.extend(c.poll());
std::thread::sleep(Duration::from_millis(10));
}
let text: String = all.iter().filter_map(|e| if let AgentEvent::TextDelta { text } = e { Some(text.clone()) } else { None }).collect();
assert_eq!(text, "read 40000 bytes", "{all:?}");
}
/// #91: a Mac app with no CLI beside it reached Claude Code, which stopped with a stack trace.
#[test]
fn a_missing_or_broken_helper_is_said_plainly() {
+3
View File
@@ -555,6 +555,9 @@ Owner: open · Status: in progress
manifest) skip the approval policy; `maki acp` takes no tool flags to stop it (0.7.0). Start
it in `app/` (paths in the prompt relative to that), or ask Maki for a way to make those
writes ask; part of the approval decision above
- [ ] Codex, Gemini CLI, OpenCode, Hermes, Maki and the plain adapter still take the message in
argv, so a long one can pass Windows' command-line limit as Claude Code's did (#402); hand
each its message on stdin or in a file where its CLI reads one
### R2.3 Website coverage and a better visual metric
Owner: open · Status: done
+5
View File
@@ -143,6 +143,11 @@ external tool a live test that checks it really produced output, not just that i
It happened again with Explorer: project folders are kept as `D:/Projects/…\name`, and "Open
folder" opened Documents. Paths for the system's programs go through `platform::native_path`.
**A message on the command line.** Claude Code was handed the whole task as `-p <message>`; a
34,641-character brief passed Windows' 32,767-character command-line limit, so the build never
started ("The filename or extension is too long", os error 206, #402). Give a program anything
that can grow (messages, briefs, lists of files) on stdin or in a file, never in argv.
**Console windows popping up over the app.** Only the detached launcher hid the console; the
supervised runner didn't, so Ghidra's `.bat` and PowerShell each opened a console window during a
real Deep-mode run (the CLI never shows it: it already has a console). Every child started from