# How no-mistakes Handles Process Cancellation Using Unix Process Groups

> Learn how no-mistakes ensures clean subprocess termination by using Unix process groups to send SIGKILL, guaranteeing complete cancellation.

- Repository: [Kun Chen/no-mistakes](https://github.com/kunchenguid/no-mistakes)
- Tags: internals
- Published: 2026-06-25

---

**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`](https://github.com/kunchenguid/no-mistakes/blob/main/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:

```go
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:

```go
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:

```go
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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/shellenv/shell_command_unix.go), the same process group isolation appears in [`internal/daemon/recover_servers_unix.go`](https://github.com/kunchenguid/no-mistakes/blob/main/internal/daemon/recover_servers_unix.go) for terminating daemon-managed servers and in [`cmd/recordfixture/proc_unix.go`](https://github.com/kunchenguid/no-mistakes/blob/main/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`](https://github.com/kunchenguid/no-mistakes/blob/main/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.