chenhg5/cc-connect · warning

taskkill failed

Error message

taskkill failed: %w: %s

What it means

signalProcessGroup (agent/claudecode/proc_windows.go:50) stops a Claude Code process tree on Windows by first running 'taskkill /T' to signal the group. If taskkill itself failed (and the failure is not the benign 'no running instance' case), it falls back to cmd.Process.Signal(sig); when that fallback also fails it wraps both as 'taskkill failed: %w: %s' (the taskkill error plus captured output). It indicates the process could not be gracefully signaled.

Solutions

  1. If the error's second cause is os.ErrProcessDone or 'access is denied' after exit, treat the stop as successful — the process is already gone.
  2. Retry Stop once after a short delay; transient races between exit and signal are common.
  3. Check captured taskkill output (the %s part) for 'access is denied' and run cc-connect with sufficient privileges or as the process owner.
  4. Verify taskkill.exe is available (it is a standard Windows binary); if removed by hardening policies, use the Unix-style build or a fallback kill path.

Example fix

// before
err := session.Stop(ctx)
if err != nil { log.Error(err) }
// after
err := session.Stop(ctx)
if err != nil {
    var pe *os.ProcessState // or check message
    if errors.Is(err, os.ErrProcessDone) || strings.Contains(err.Error(), "No results") {
        log.Info("process already exited; stop treated as success")
        return nil
    }
    log.Error(err)
}
Defensive patterns

Strategy: retry

Try / catch

if err := session.Stop(ctx); err != nil {
    if errors.Is(err, os.ErrProcessDone) || strings.Contains(err.Error(), "already") {
        return nil // already exited; benign
    }
    time.Sleep(500 * time.Millisecond)
    if err2 := session.Stop(ctx); err2 != nil {
        log.Warn("stop failed; process may linger", "err", err2)
    }
}

Prevention

When it happens

Trigger: Calling Stop/signalProcessGroup on Windows where taskkill exits non-zero with output not matching 'there is no running instance', AND cmd.Process.Signal(sig) returns an error other than os.ErrProcessDone.

Common situations: Process already exited between the taskkill call and the Signal fallback (most common, benign); insufficient privileges to signal a process owned by another user; taskkill not on PATH or blocked by policy; process in a zombie/uninterruptible state.

Related errors


AI-assisted analysis of chenhg5/cc-connect@4000b2338a (2026-09-06). Data as JSON: /api/errors/e4d207afde78213b. Report an issue: GitHub.

Appendix: source

Thrown at agent/claudecode/proc_windows.go:50

// signalProcessGroup is a graceful best-effort equivalent of forceKillCmd
// on Windows: taskkill without /F asks the target to close cleanly. Falls
// back to cmd.Process.Signal if taskkill is unavailable.
func signalProcessGroup(cmd *exec.Cmd, sig syscall.Signal) error {
	if cmd == nil || cmd.Process == nil {
		return nil
	}
	killCmd := exec.Command("taskkill", "/T", "/PID", strconv.Itoa(cmd.Process.Pid))
	output, err := killCmd.CombinedOutput()
	if err == nil {
		return nil
	}
	if isTaskkillNotRunning(output) {
		return nil
	}
	if killErr := cmd.Process.Signal(sig); killErr == nil || errors.Is(killErr, os.ErrProcessDone) {
		return nil
	}
	return fmt.Errorf("taskkill failed: %w: %s", err, processKillOutput(output))
}

// forceKillCmd taskkill /T /F's the entire descendant tree rooted at cmd.
func forceKillCmd(cmd *exec.Cmd) error {
	if cmd == nil || cmd.Process == nil {
		return nil
	}
	killCmd := exec.Command("taskkill", "/T", "/F", "/PID", strconv.Itoa(cmd.Process.Pid))
	output, err := killCmd.CombinedOutput()
	if err == nil {
		return nil
	}
	if isTaskkillNotRunning(output) {
		return nil
	}
	if killErr := cmd.Process.Kill(); killErr == nil || errors.Is(killErr, os.ErrProcessDone) {
		return nil
	} else {

View on GitHub (pinned to 4000b2338a)