Go version

go version go1.25.0 linux/amd64

Output of go env in your module/workspace:

AR='ar'
CC='gcc'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='g++'
GCCGO='gccgo'
GO111MODULE=''
GOAMD64='v1'
GOARCH='amd64'
GOAUTH='netrc'
GOBIN=''
GOCACHE='/home/agis/.cache/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/home/agis/.config/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2541113073=/tmp/go-build -gno-record-gcc-switches'
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMOD='/home/agis/dev/x/go.mod'
GOMODCACHE='/home/agis/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/home/agis/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/home/agis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.linux-amd64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/home/agis/.config/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/home/agis/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.25.0.linux-amd64/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.25.0'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Brief

The way go tool traps and forwards signals might cause tools that stay in the foreground to misbehave.

Note: I'm not sure if this is considered a bug or a "work as intended" situation. If there's intention of changing the behavior, I'd be happy to take a stab at it.

Description

go tool traps signals and forwards them to the spawned tool: https://github.com/golang/go/blob/cdd8cf4988c7c0f2bb8eb795f74c4f803c63a70d/src/cmd/go/internal/tool/tool.go#L374-L383

However, if the spawned tool is a process that's in the foreground of the TTY, hitting Ctrl-C in the terminal will cause 2 SIGINT signals to be delivered to the spawned tool: one from the kernel (the tool is in the same process group ID as go tool) and one from go tool itself.

This is problematic for tools that assign special meaning to a second SIGINT. overmind (a process supervisor) is an example of this. The second signal causes it to forcefully kill all its processes.

Reproduction

// main.go
package main

func main() {}
// go.mod
module foo

go 1.25

toolchain go1.25.0

tool github.com/agis/gotool-sig

require github.com/agis/gotool-sig v0.0.0-20250826124522-e922fff2b3fb // indirect

Run go tool gotool-sig 1 and then hit Ctrl-C in the terminal. You'll get the following output:

$ go tool gotool-sig 1
PID: 176199, PPID: 176162, PGID: 176162
Signal monitor started with buffer size 1...
^CReceived signal: interrupt (2)
Received signal: interrupt (2)

You'll notice that 2 interrupt signals were delivered to the tool, instead of one.

What did you see happen?

N/A (see above)

What did you expect to see?

Only 1 SIGINT should be delivered.