zeroclaw-labs/zeroclaw · error
failed to write args to plugin '{}' stdin: {}
Error message
failed to write args to plugin '{}' stdin: {} What it means
SubprocessTool::execute spawns the plugin with piped stdio and writes the JSON args plus a newline to its stdin. BrokenPipe is deliberately tolerated (children that never read stdin are legal), so this bail fires only for other I/O errors during the write — and the child is killed first. The zeroclaw log record includes the plugin name and the io::Error for diagnosis.
Source
Thrown at crates/zeroclaw-hardware/src/subprocess.rs:154
stdin.write_all(b"\n").await?;
Ok::<(), std::io::Error>(())
}
.await;
if let Err(e) = write_result
&& e.kind() != std::io::ErrorKind::BrokenPipe
{
let _ = child.kill().await;
::zeroclaw_log::record!(
ERROR,
::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Fail)
.with_outcome(::zeroclaw_log::EventOutcome::Failure)
.with_attrs(::serde_json::json!({
"plugin": self.manifest.tool.name,
"error": format!("{}", e),
})),
"subprocess plugin: failed to write args to stdin"
);
anyhow::bail!(
"failed to write args to plugin '{}' stdin: {}",
self.manifest.tool.name,
e
);
}
// stdin dropped here → child receives EOF
}
// Take stdout and stderr handles before we move `child`.
let stdout_handle = child.stdout.take();
let stderr_handle = child.stderr.take();
// Read one line from stdout with a hard timeout.
let read_result = match stdout_handle {
None => {
// No stdout — kill and error.
let _ = child.kill().await;
return Ok(ToolResult {View on GitHub (pinned to 88bb9c8533)
Solutions
- Reproduce the plugin manually: `echo '{"args":1}' | ./plugin-binary` and check it starts without crashing
- Check zeroclaw logs — the 'failed to write args to stdin' event carries the plugin name and io::Error kind
- Verify the manifest's tool name and binary_path point at the intended executable and it is runnable on this platform
- On resource-starved hosts, raise fd limits or reduce concurrent subprocess plugins
Defensive patterns
Strategy: try-catch
Validate before calling
let ok = std::process::Command::new(&plugin_binary)
.arg("--help")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if !ok {
anyhow::bail!("plugin does not run standalone; check binary and manifest");
} Try / catch
match tool.execute(args).await {
Err(e) if format!("{e}").contains("failed to write args to") => {
// plugin's stdin failed at OS level (EPIPE is tolerated elsewhere):
// verify the plugin starts standalone, check the zeroclaw log event,
// then retry once
}
rest => rest,
} Prevention
- Test plugin binaries standalone (`echo args | ./plugin`) before registering them
- Keep plugin manifests' binary_path correct and executable for the host platform
- Watch fd/memory limits when running many subprocess plugins concurrently
When it happens
Trigger: The child's stdin fails at the OS level with something other than EPIPE: the pipe torn down by a concurrent kill/timeout, a plugin binary that crashes instantly while stdin is being written, or a resource-exhausted host (fd limits) failing the pipe write.
Common situations: Plugin binaries crashing at startup; hosts at fd/memory limits running many subprocess plugins; races between the subprocess timeout machinery and the stdin write.
Related errors
- Edge TTS subprocess timed out
- edge-tts failed (exit {}): {}
- update --check failed: {}
- manifest missing [tool] name
- manifest missing [tool] description
AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23).
Data as JSON: /api/errors/fb05edcbe8c15a77.
Report an issue: GitHub.