gastownhall/beads · error
procid: process %d still matches token after fatal signal an
Error message
procid: process %d still matches token after fatal signal and %s re-check
What it means
After sending a fatal signal (SIGKILL/SIGTERM) on darwin, Handle.Signal polls Verify for up to fallbackSignalConfirmTimeout (2s) to confirm the process actually died. If the process still matches the token after the timeout, the library reports that the kill did not take effect within the confirmation window.
Source
Thrown at internal/procid/procid_darwin.go:97
}
if !match {
return fmt.Errorf("procid: process %d no longer matches token", h.pid)
}
return nil
}
func (h *Handle) confirmFatalSignal() error {
deadline := time.Now().Add(fallbackSignalConfirmTimeout)
for {
match, err := Verify(h.pid, h.token)
if err != nil {
return err
}
if !match {
return nil
}
if time.Now().After(deadline) {
return fmt.Errorf(
"procid: process %d still matches token after fatal signal and %s re-check",
h.pid,
fallbackSignalConfirmTimeout,
)
}
time.Sleep(10 * time.Millisecond)
}
}
func isFatalSignal(sig syscall.Signal) bool {
return sig == syscall.SIGKILL || sig == syscall.SIGTERM
}
func (h *Handle) Kill() error { return h.Signal(syscall.SIGKILL) }
func (h *Handle) Close() error { return nil }
// IsProcessGone reports whether err means the referenced process no longerView on GitHub (pinned to 71377f2769)
Solutions
- Send SIGKILL instead of SIGTERM if the target may trap SIGTERM (SIGKILL cannot be handled).
- Wait and re-check: poll Verify yourself after returning, since the process may exit just after the 2s window.
- Increase tolerance in your caller: retry the kill after this error before treating it as a hard failure.
- Investigate the target for stuck states (D-state, blocked handlers) if it survives SIGKILL for more than 2 seconds.
Example fix
// before
if err := handle.Kill(); err != nil {
return fmt.Errorf("hard failure: %w", err)
}
// after
if err := handle.Kill(); err != nil {
if strings.Contains(err.Error(), "still matches token") {
time.Sleep(500 * time.Millisecond)
_ = handle.Kill() // SIGKILL retry; then treat as gone if token changed
}
} Defensive patterns
Strategy: retry
Validate before calling
// No pre-call validation possible; target may trap SIGTERM. Prefer SIGKILL if survival is unacceptable: // h.Kill() instead of h.Signal(syscall.SIGTERM)
Try / catch
if err := h.Kill(); err != nil {
if strings.Contains(err.Error(), "still matches token") {
time.Sleep(2 * time.Second)
return h.Kill() // one retry; process may have needed longer to exit
}
return err
} Prevention
- Use SIGKILL for targets that must die and cannot be trusted to honor SIGTERM
- Set SIGTERM handlers in your own workers to exit well within 2 seconds
- After this error, poll procid.Verify before declaring the kill failed
- Watch for D-state/stuck processes if SIGKILL also times out
When it happens
Trigger: Handle.Signal(SIGKILL)/Kill() (or SIGTERM) where the process survives the full 2-second confirmation loop while still matching its token — e.g. SIGTERM trapped/ignored by the target, or the system is heavily loaded so exit is delayed.
Common situations: Applications installing a SIGTERM handler that blocks shutdown; processes stuck in uninterruptible work that delay reaping; heavily loaded CI machines; sending SIGTERM (treated as fatal here) to a process that handles it gracefully and takes longer than 2s to exit.
Related errors
- procid: unsupported signal %v
- procid: signal %d: %w
- procid: process %d still matches token after fatal signal an
- timeout (%s) waiting for spawn marker %s; wait for the in-pr
- confirm verified pid %d stopped: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/6fd10f699dbc950c.
Report an issue: GitHub.