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.