# fix(cli): gate silent-abort on signal handler firing; test exit codes

`54a30ec`→[main](/content/gh/entireio/cli/commits/main/index.html)·toothbrush·2w ago·2 files·+51 added/-6 removed

Addresses two trail findings:

- Matching context.Canceled at the top level was too broad: an internally-cancelled sub-context would have silently re-raised SIGINT (exit 130) and broken an enclosing loop even without a Ctrl-C. Gate the abort path on caughtSignal being set, so a signal-less context.Canceled falls through to normal error reporting.
- Add a table test for exitCodeForSignal locking SIGINT→130, SIGTERM→143 (and the non-numeric fallback), guarding the mapping the fix relies on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

## Changes

2

- cmd/entire

- Mmain.go+12/-6

- Amain_test.go+39

```
88 unmodified lines

89
90
91
92
93
94
95
96
97
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108

88 unmodified lines

var silent *cli.SilentError

switch {
	case errors.Is(err, context.Canceled):
		// Aborted (a signal cancelled the root context). Don't dump the
		// raw transport/keyring cancellation string ("...: context
		// canceled", "read access token: signal: interrupt") as if it
		// were a failure — die quietly by re-raising the signal that
		// triggered it (see dieFromSignal) so an enclosing
	case errors.Is(err, context.Canceled) && caughtSignal.Load() != nil:
		// A signal cancelled the root context (our handler fired). Don't
		// dump the raw transport/keyring cancellation string ("...:
		// context canceled", "read access token: signal: interrupt") as
		// if it were a failure — die quietly by re-raising the signal
		// that triggered it (see dieFromSignal) so an enclosing
		// `while ...; do entire; done` loop actually breaks on a single
		// Ctrl-C, and a SIGTERM shutdown still exits 143.
		//
		// We gate on the handler having fired rather than on the error
		// type alone: a context.Canceled that arose without a signal
		// (e.g. an internally-cancelled sub-context) is a genuine error
		// and must fall through to normal reporting, not masquerade as a
		// user abort (which would also wrongly break an enclosing loop).
		cancel()
		dieFromSignal(terminatingSignal())
	case errors.As(err, &silent):
```

Mcmd/entire/main.go+12/-6

```
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

package main

import (
	"os"
	"syscall"
	"testing"
)

// nonNumericSignal is an os.Signal that isn't a syscall.Signal, exercising
// exitCodeForSignal's fallback branch.
type nonNumericSignal struct{}

func (nonNumericSignal) String() string { return "non-numeric" }
func (nonNumericSignal) Signal() {}

// TestExitCodeForSignal locks the conventional 128+signum mapping the
// Ctrl-C/SIGTERM fix relies on, so a future "simplification" back to a
// hardcoded 130 can't silently regress SIGTERM's 143.
func TestExitCodeForSignal(t *testing.T) {
	t.Parallel()

tests := []struct {
	name string
	sig  os.Signal
	want int
}{
	{"SIGINT", os.Interrupt, 130},
	{"SIGTERM", syscall.SIGTERM, 143},
	{"non-numeric signal falls back to 130", nonNumericSignal{}, 130},
}
for _, tc := range tests {
	t.Run(tc.name, func(t *testing.T) {
		t.Parallel()
		if got := exitCodeForSignal(tc.sig); got != tc.want {
			t.Errorf("exitCodeForSignal(%v) = %d, want %d", tc.sig, got, tc.want)
		}
	})
}
}
```

Acmd/entire/main_test.go+39
