charmbracelet/crush · error

failed to remove stale server socket %q: %v

Error message

failed to remove stale server socket %q: %v

What it means

During ensureServer, when dialing the server socket fails with a 'stale socket' error, the client attempts to delete the leftover socket file at hostURL.Host via os.Remove. If the removal fails for any reason other than the file already being gone (fs.ErrNotExist), the error is wrapped as 'failed to remove stale server socket' and aborts the connection flow.

Source

Thrown at internal/cmd/root.go:533

		switch {
		case statErr == nil:
			// Probe the socket explicitly before the version-check
			// path. A stale unix socket file (the previous server
			// exited without cleaning up) would otherwise make
			// restartIfStale spin on a non-responsive endpoint; here
			// we detect it with a short DialTimeout and remove the
			// orphaned file so the normal spawn path can run.
			if hostURL.Scheme == "unix" {
				conn, dialErr := net.DialTimeout( //nolint:noctx
					hostURL.Scheme, hostURL.Host, 200*time.Millisecond,
				)
				if dialErr == nil {
					conn.Close()
				} else if server.IsStaleSocketErr(dialErr) {
					slog.Warn("Stale socket detected, removing",
						"path", hostURL.Host, "error", dialErr)
					if err := os.Remove(hostURL.Host); err != nil && !errors.Is(err, fs.ErrNotExist) {
						return fmt.Errorf("failed to remove stale server socket %q: %v", hostURL.Host, err)
					}
					needsStart = true
					break
				}
			}
			restarted, err := restartIfStale(cmd, hostURL)
			if err != nil {
				slog.Warn("Failed to check server version", "error", err)
			}
			needsStart = restarted || err != nil
		case errors.Is(statErr, fs.ErrNotExist):
			needsStart = true
		default:
			slog.Warn("Unexpected error stat'ing server socket, attempting cleanup",
				"path", hostURL.Host, "error", statErr)
			if err := os.Remove(hostURL.Host); err != nil && !errors.Is(err, fs.ErrNotExist) {
				return fmt.Errorf("failed to remove stale server socket %q: %v", hostURL.Host, err)
			}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Check permissions/ownership of the socket file and its parent directory and remove it manually: rm <socket-path> (use sudo if ownership differs).
  2. Verify the path in the error is a socket file, not a directory; if it is a directory, inspect what created it before deleting.
  3. Ensure the cache directory (config.GlobalCacheDir derived) is on a writable filesystem, not a read-only mount.
  4. Run crush as the same user that started the original server to avoid ownership mismatches.

Example fix

// before
if err := os.Remove(hostURL.Host); err != nil && !errors.Is(err, fs.ErrNotExist) {
	return fmt.Errorf("failed to remove stale server socket %q: %v", hostURL.Host, err)
}
// after
if err := os.Remove(hostURL.Host); err != nil && !errors.Is(err, fs.ErrNotExist) {
	slog.Warn("Could not remove stale socket, forcing fresh start", "path", hostURL.Host, "error", err)
	// fall through to needsStart instead of hard-failing the client
}
needsStart = true
Defensive patterns

Strategy: validation

Validate before calling

info, err := os.Lstat(socketPath)
if err == nil && info.Mode()&os.ModeSocket == 0 {
	// not a socket: refuse or clean up before ensureServer runs
	os.RemoveAll(socketPath)
}

Prevention

When it happens

Trigger: A dial to the unix socket fails with server.IsStaleSocketErr, then os.Remove(socketPath) returns a non-ErrNotExist error — e.g. permission denied on the socket file or its parent directory, the path is a non-empty directory instead of a socket, or the filesystem is read-only.

Common situations: A previous crush server crashed leaving a socket owned by a different user (e.g. after sudo use or a container user mismatch); the socket path sits on a read-only mount; another process recreated the path as a directory; stale sockets in a shared cache dir with mixed ownership.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/74c031051bb8a447. Report an issue: GitHub.