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

  1. Reproduce the plugin manually: `echo '{"args":1}' | ./plugin-binary` and check it starts without crashing
  2. Check zeroclaw logs — the 'failed to write args to stdin' event carries the plugin name and io::Error kind
  3. Verify the manifest's tool name and binary_path point at the intended executable and it is runnable on this platform
  4. 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

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


AI-assisted analysis of zeroclaw-labs/zeroclaw@88bb9c8533 (2026-08-23). Data as JSON: /api/errors/fb05edcbe8c15a77. Report an issue: GitHub.