---
title: Signal forwarding and exit-code propagation
status: implemented
---

## Intent

When the user runs `bear -- make` in a terminal and presses `SIGINT`
(Ctrl-C), the interrupt must reach the build. Bear must not swallow
the signal, leaving `make` and the whole process tree underneath it
running in the background. The same applies when a CI runner sends
`SIGTERM` to a running Bear to abort a build: the build being
supervised must stop too.

The reverse direction matters just as much. Shells, CI runners, and
parent `make` rules look at Bear's exit code to decide whether the
build succeeded. Bear must therefore report the same exit code that
the build command produced, so the caller sees the real result rather
than a success or failure Bear invented.

Users interact with a single command, `bear`. The interception mode
Bear selects (see `interception-preload-mechanism` and
`interception-wrapper-mechanism`) is an internal detail: the contract
below must hold identically regardless of which mode is active.

## Acceptance criteria

- Pressing Ctrl-C while `bear -- <build>` runs stops the build
- Sending `SIGTERM` (and `SIGQUIT` on Unix) to a running Bear stops
  the build
- Between the signal arriving and both Bear and the build ending,
  less than one second elapses on a system not under heavy load
- When the build exits normally, Bear's exit code equals the build
  command's exit code: `0` is preserved for success, non-zero codes
  are preserved for failure
- Exit codes in the portable range (0-255 on Unix) are propagated
  byte-for-byte
- When the build is terminated by a signal rather than exiting
  normally, Bear exits with a non-zero code so that scripts and CI
  systems see a failed build
- The build receives the same signal Bear received: a `SIGINT` arrives
  as `SIGINT` and a `SIGTERM` as `SIGTERM`, so a build that handles the
  two differently sees the real one. Bear relays the signal rather than
  substituting one of its own
- The whole process tree under the build is stopped, not just the
  immediate child: descendants the build spawned do not survive Bear
- If the build is a shell script that installs its own signal trap,
  the script receives the signal, its trap runs, and Bear's exit
  code reflects whatever the script ultimately exited with
- The build is first asked to stop with the received signal and given
  a brief grace period to shut down cleanly before it is forced; a
  build that ignores the signal is still stopped within the time budget
- Every acceptance criterion above applies to every supported
  interception mode

## Non-functional constraints

- Platform support: Linux, macOS, BSD, and Windows
- Bear must not interfere with the build tool's own signal handling.
  Build drivers such as `make -j`, `ninja`, and `cmake --build`
  install their own handlers to stop their workers on termination;
  Bear relies on that behaviour rather than re-implementing it
- Running a build under `bear --` must add no perceptible delay
  compared with running the same command directly

## Known limitations

**The signal that terminated the build is not encoded in Bear's exit
code.** The shell convention of `128 + signal_number` is not
followed. Scripts that inspect Bear's exit code to identify *why* a
build stopped cannot distinguish signal termination from a regular
build failure. (The build itself still receives the real signal; this
limitation is only about Bear's own exit code.)

**A child that re-detaches into its own session can still survive.**
Bear stops the whole process tree by signalling the build's process
group. A child that deliberately starts a new session of its own (a
daemon that calls `setsid`) leaves that group and may keep running
after Bear exits. On Linux, where the host provides a usable cgroup,
Bear closes this gap by terminating the build's cgroup instead, which a
child cannot leave; where no such cgroup is available Bear falls back
to the process-group behaviour and the limitation applies.

**Windows signal coverage is limited.** Only `SIGTERM` and `SIGINT`
are observed. Other Windows-specific termination mechanisms (such as
`CTRL_BREAK_EVENT` or a parent calling `TerminateProcess` on
`bear.exe`) are not explicitly handled.

**Nested `bear` invocations are unsupported.** Running
`bear -- bear -- make` is not a supported configuration and its
signal/exit-code behaviour is undefined. Users who need to record
multiple separate builds into a single database should use
`--append` (see `output-append`) rather than nesting Bear.

## Testing

Given a long-running build under `bear --`:

> When the test runs `bear -- sleep 10`, waits briefly for the sleep
> to start, then sends a termination signal to the `bear` process,
> then both `bear` and the `sleep` child terminate within one
> second,
> and `bear` reports a non-success exit status.

Given a build that exits successfully:

> When the user runs `bear -- true`,
> then `bear` exits with code `0`.

Given a build that exits with a non-zero code:

> When the user runs `bear -- false`,
> then `bear` exits with a non-zero code matching the build's.

Given an interception-only run (`bear intercept`), the exit-code
contract still holds:

> When the user runs `bear intercept -- true`, the exit code is `0`;
> when the user runs `bear intercept -- false`, the exit code is
> non-zero and matches the build's.

Given a build that is interrupted mid-compile:

> When the user presses Ctrl-C while the compiler is running,
> then the compiler terminates,
> and `bear` exits with a non-zero status.

## Notes

- Related: `interception-preload-mechanism`,
  `interception-wrapper-mechanism`.

## Rationale

- [process-tree teardown and graceful forwarding](../rationale/process-tree-teardown.md)
