jdx/mise · error
a process it started kept its output open
Error message
a process it started kept its output open
What it means
Thrown by `run_with_limits` in describe_command.rs when the command's output channel produces nothing within `output_grace` after the shell exits. This indicates a descendant process the shell spawned kept the inherited stdout/stderr pipe open, so the collector cannot distinguish 'shell finished' from 'shell still writing'. The library intentionally refuses to wait forever on orphaned descendants and kills the remaining active process instead.
Source
Thrown at src/system/history/describe_command.rs:175
let status = loop {
if let Some(status) = child.try_wait()? {
break status;
}
if started.elapsed() >= timeout {
// the shell and whatever it started; the reader thread ends
// with the last writer of the pipe, so it is not waited for
active.0.kill();
let _ = child.kill();
let _ = child.wait();
bail!("took longer than {}s", timeout.as_secs());
}
std::thread::sleep(Duration::from_millis(100));
};
// a descendant that outlived the shell and kept the pipe is not the
// shell's answer: the output is waited for a moment, not forever
let Ok(output) = receiver.recv_timeout(output_grace) else {
active.0.kill();
bail!("a process it started kept its output open");
};
if output.len() > DIFF_LIMIT {
bail!("description output exceeded {} bytes", DIFF_LIMIT);
}
if !status.success() {
bail!("exited with {status}");
}
let Some(line) = first_line(&output) else {
return Ok(None);
};
annotate(
store,
entry,
Annotation {
description: Some(line.clone()),
description_source: Some(DescriptionSource::Command),
labels: None,
updated_at: store::now_rfc3339(),View on GitHub (pinned to afd2eddd3a)
Solutions
- Rewrite the command so background children do not inherit stdout/stderr: redirect them to a file or /dev/null (e.g. `my-daemon &>/dev/null &`).
- Use `setsid`/`disown` with full redirection so the descendant detaches from the inherited pipe.
- Increase the grace window if the command legitimately produces output slowly, via the run_with_limits configuration.
- Ensure the description command itself exits promptly instead of blocking on children it spawns.
Example fix
// before logging-agent --follow & echo done // after logging-agent --follow >/dev/null 2>&1 & disown echo done
Defensive patterns
Strategy: try-catch
Validate before calling
// shell script audit: no background jobs inheriting stdout // grep -nE '&([^>]|$)' my-hook.sh — ensure each is redirected
Try / catch
// match run_with_limits(...) {
// Err(e) if e.message().contains("kept its output open") => {
// // command spawned a daemon; treat as inconclusive, not fatal
// }
// other => other?,
// } Prevention
- Always redirect background children (`>/dev/null 2>&1 & disown`).
- Keep description commands single-purpose and fast-exiting.
- Avoid sourcing profile files that spawn daemons during commands.
When it happens
Trigger: Running a description command whose shell spawns a background child (daemon, `tail -f`, `nohup ... &`, detached process) that inherits the stdout pipe and outlives the shell; `receiver.recv_timeout(output_grace)` then times out even though the shell itself exited.
Common situations: A dotfile hook or annotation command that starts a background agent or logging process; scripts that leave `&` background jobs attached to the terminal; watchdog processes spawned by shell profile files executed during command startup.
Understand the failure class
Background: Request timed out: what client-side request timeouts mean across libraries (Request timed out, TIMED_OUT, APITimeoutError) — this error's family across 39 libraries.
Related errors
- timed out after {duration:?}
- failed to read command {stream}: {err}
- command output pipes did not close within {pipe_drain_timeou
- timed out after {timeout:?}
- --connect-timeout must be greater than zero
AI-assisted analysis of jdx/mise@afd2eddd3a (2026-09-09).
Data as JSON: /api/errors/4ad3fcb36fc50b4d.
Report an issue: GitHub.