How no-mistakes Handles Process Cancellation Using Unix Process Groups

no-mistakes ensures clean subprocess termination by isolating commands in their own Unix process groups and sending SIGKILL to the entire group when cancellation is triggered.

Long-running subprocesses that spawn child processes (like test runners or Git commands) often leave orphaned grandchildren if the parent process is killed. The no-mistakes project solves this by leveraging Unix process group isolation to guarantee complete termination of entire process trees. This implementation lives primarily in the internal/shellenv package and is reused across the codebase for consistent cleanup semantics.

The Unix Process Group Strategy

Unix process groups provide a mechanism to treat related processes as a single unit for signal delivery. When a process creates a new process group and becomes its leader, all its descendants inherit that group ID by default.

no-mistakes exploits this behavior to solve the "orphan process" problem through two coordinated actions:

  1. Process Group Creation – By setting Setpgid: true in syscall.SysProcAttr, the spawned process becomes the leader of a new process group. This mirrors the setpgid(2) system call, ensuring that any grandchildren automatically join the same group.

  2. Group-Wide Termination – Instead of killing only the parent process, the cancellation routine sends SIGKILL to the negative process ID, which targets the entire process group.

This approach guarantees that when a context is cancelled, no stray subprocesses remain to lock worktrees or consume resources.

Implementation in shellenv.ConfigureShellCommand

The core logic resides in internal/shellenv/shell_command_unix.go within the ConfigureShellCommand function. This helper transforms a standard exec.Cmd into a properly isolated, cancellable command.

When invoked, the function performs two critical modifications to the command:

cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}

cmd.Cancel = func() error {
    err := syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
    // Handle ESRCH (process already gone)...
    return err
}

The Setpgid flag places the process in its own group, while the custom Cancel closure overrides the default behavior. The negative sign in -cmd.Process.Pid instructs the kernel to deliver the signal to every process in that group, not just the leader.

If the group no longer exists (returning ESRCH), the function returns os.ErrProcessDone, signaling that termination has already occurred.

Practical Code Examples

Spawning a Cancellable Shell Command

To execute a long-running task that can be fully aborted, wire the context through exec.CommandContext and apply the shellenv configuration:

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

cmd := exec.CommandContext(ctx, "bash", "-c", "some-long-running-task")
shellenv.ConfigureShellCommand(cmd) // Isolates in its own process group

if err := cmd.Start(); err != nil {
    log.Fatalf("start failed: %v", err)
}

// Later, to abort the entire process tree:
cancel() // Triggers cmd.Cancel → SIGKILL to the whole group

Calling cancel() propagates through the context, triggering the custom Cancel function that kills the entire process group.

Integration in Pipeline Steps

Pipeline steps utilize this helper to ensure cleanup even when scripts spawn complex subprocess hierarchies:

func runShellStep(step *pipeline.Step) error {
    ctx, cancel := context.WithCancel(step.Ctx)
    defer cancel()

    cmd := exec.CommandContext(ctx, step.Shell, step.Script)
    shellenv.ConfigureShellCommand(cmd) // Ensures proper cancellation

    cmd.Stdout = step.Stdout
    cmd.Stderr = step.Stderr
    return cmd.Run()
}

This pattern ensures that if the step times out or is interrupted, no grandchildren persist beyond the step's lifecycle.

Testing the Behavior

The test suite in internal/pipeline/steps/shell_unix_test.go validates this behavior by launching a child process that spawns a grandchild. When the test triggers context cancellation, it verifies that both processes terminate, confirming that Setpgid combined with group-wide SIGKILL successfully cleans up the entire tree.

Cross-Cutting Usage Across the Codebase

The ConfigureShellCommand helper is not limited to shell execution. The same pattern appears in internal/daemon/recover_servers_unix.go, where daemon-managed servers use process group isolation to ensure recovery routines can terminate stale server processes completely.

Similarly, cmd/recordfixture/proc_unix.go reuses this logic for fixture-recording subprocesses, ensuring that recording tools and their dependencies are fully terminated when fixtures are captured or aborted.

While the Unix implementation relies on Setpgid and syscall.Kill, the Windows counterpart in internal/shellenv/shell_command_windows.go achieves equivalent semantics using job objects, providing consistent cross-platform behavior.

Summary

  • no-mistakes prevents orphaned processes by leveraging Unix process groups in internal/shellenv/shell_command_unix.go.
  • The ConfigureShellCommand function sets Setpgid: true to create a new process group and overrides cmd.Cancel to send SIGKILL to the entire group.
  • The negative PID in syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL) targets all processes in the group, not just the parent.
  • This pattern is reused across the codebase, including daemon recovery and fixture recording, ensuring consistent termination semantics.
  • The implementation gracefully handles cases where the process group has already exited by returning os.ErrProcessDone.

Frequently Asked Questions

How does no-mistakes ensure that grandchild processes are killed when a command is cancelled?

By setting Setpgid: true in syscall.SysProcAttr, the spawned process becomes a process group leader. All descendants inherit this group ID. When cancellation occurs, no-mistakes calls syscall.Kill with the negative process ID (e.g., -cmd.Process.Pid), which delivers SIGKILL to every process in that group, ensuring grandchildren are terminated along with their parent.

What happens if the process group no longer exists when cancellation is triggered?

The syscall.Kill call returns ESRCH (no such process) if the group has already terminated. The implementation catches this condition and returns os.ErrProcessDone, indicating that the cleanup has already occurred and no further action is needed.

Where else in the no-mistakes codebase is this process group logic used?

Beyond shell commands in internal/shellenv/shell_command_unix.go, the same process group isolation appears in internal/daemon/recover_servers_unix.go for terminating daemon-managed servers and in cmd/recordfixture/proc_unix.go for managing fixture-recording subprocesses. This ensures consistent cleanup behavior across all subprocess management in the application.

Is there a Windows equivalent to this Unix process group behavior?

Yes, the Windows implementation in internal/shellenv/shell_command_windows.go uses job objects instead of process groups. This provides equivalent functionality for grouping and terminating processes on Windows platforms, maintaining consistent cross-platform semantics for process cancellation.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →