Last active
May 18, 2026 07:24
-
-
Save brunos3d/66abd16661cfcfd4b5a943b4baf96f24 to your computer and use it in GitHub Desktop.
Claude Code socket keepalive fix — workaround for 'socket connection was closed unexpectedly' (issue #60133)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # claude-keepalive-fix.sh | |
| # Workaround for: "API Error: The socket connection was closed unexpectedly" | |
| # GitHub issue: https://github.com/anthropics/claude-code/issues/60133 | |
| # | |
| # This script applies OS-level TCP keepalive fixes that compensate for | |
| # the Claude Code binary (Bun 1.3.14) not setting SO_KEEPALIVE on its | |
| # TCP sockets, combined with Anthropic's server-side keepalive flags | |
| # being set to 0ms via GrowthBook feature flags. | |
| # | |
| # Supports: Linux, macOS | |
| # Requires: gcc or clang, sudo (for sysctl phase only) | |
| # ============================================================================= | |
| set -uo pipefail | |
| # ── colours ────────────────────────────────────────────────────────────────── | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' | |
| info() { echo -e "${BLUE}[INFO]${NC} $*"; } | |
| success() { echo -e "${GREEN}[OK]${NC} $*"; } | |
| warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } | |
| error() { echo -e "${RED}[ERR]${NC} $*"; } | |
| step() { echo -e "\n${BOLD}${BLUE}▶ $*${NC}"; } | |
| dim() { echo -e "${DIM} $*${NC}"; } | |
| ask() { | |
| local prompt="$1" | |
| echo -e "\n${BOLD}${YELLOW}?${NC} ${BOLD}${prompt}${NC} ${DIM}[Y/n]${NC} " | |
| read -r -n1 reply 2>/dev/null || read -r reply | |
| echo | |
| [[ "$reply" =~ ^[Yy]$ ]] || [[ -z "$reply" ]] | |
| } | |
| # ── detect OS ──────────────────────────────────────────────────────────────── | |
| OS="$(uname -s)" | |
| ARCH="$(uname -m)" | |
| if [[ "$OS" == "Darwin" ]]; then | |
| PLATFORM="macOS" | |
| LIB_EXT="dylib" | |
| LIB_FLAG="-dynamiclib" | |
| PRELOAD_VAR="DYLD_INSERT_LIBRARIES" | |
| CC="clang" | |
| elif [[ "$OS" == "Linux" ]]; then | |
| PLATFORM="Linux" | |
| LIB_EXT="so" | |
| LIB_FLAG="-shared -fPIC" | |
| PRELOAD_VAR="LD_PRELOAD" | |
| CC="gcc" | |
| else | |
| error "Unsupported OS: $OS. Only Linux and macOS are supported." | |
| exit 1 | |
| fi | |
| # ── detect shell config ─────────────────────────────────────────────────────── | |
| detect_shell_config() { | |
| local shell_name | |
| shell_name="$(basename "${SHELL:-bash}")" | |
| if [[ "$shell_name" == "zsh" ]]; then | |
| echo "${ZDOTDIR:-$HOME}/.zshrc" | |
| elif [[ "$shell_name" == "bash" ]]; then | |
| if [[ "$PLATFORM" == "macOS" ]]; then | |
| echo "$HOME/.bash_profile" | |
| else | |
| echo "$HOME/.bashrc" | |
| fi | |
| else | |
| echo "$HOME/.profile" | |
| fi | |
| } | |
| SHELL_CONFIG="$(detect_shell_config)" | |
| LIB_DIR="$HOME/.local/lib" | |
| LIB_FILE="$LIB_DIR/libkeepalive.$LIB_EXT" | |
| MARKER="# claude-keepalive-fix — managed block" | |
| # ── header ─────────────────────────────────────────────────────────────────── | |
| echo | |
| echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${BOLD}║ Claude Code — Socket Keepalive Fix v1.0 ║${NC}" | |
| echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}" | |
| echo | |
| echo -e " Platform : ${BOLD}$PLATFORM${NC} ($ARCH)" | |
| echo -e " Shell cfg: ${BOLD}$SHELL_CONFIG${NC}" | |
| echo -e " Library : ${BOLD}$LIB_FILE${NC}" | |
| echo | |
| echo -e " This script applies ${BOLD}4 independent fixes${NC}. Each one is optional" | |
| echo -e " and will ask for confirmation before making any change." | |
| echo -e " To revert everything, run: ${BOLD}claude-keepalive-uninstall.sh${NC}" | |
| echo | |
| echo -e " Issue ref: ${DIM}https://github.com/anthropics/claude-code/issues/60133${NC}" | |
| echo | |
| echo -e "${YELLOW} ⚠ Read each step before confirming. Some steps require sudo.${NC}" | |
| echo | |
| # ── phase 1: environment variables ─────────────────────────────────────────── | |
| step "Phase 1 of 4 — Environment variables" | |
| echo | |
| echo -e " Adds the following exports to ${BOLD}$SHELL_CONFIG${NC}:" | |
| echo | |
| echo -e " ${DIM}CLAUDE_CODE_REMOTE_SEND_KEEPALIVES=true${NC}" | |
| dim " Enables application-level session keepalives in Claude Code" | |
| echo -e " ${DIM}BUN_CONFIG_HTTP_IDLE_TIMEOUT=300${NC}" | |
| dim " Extends Bun's HTTP connection pool idle timeout to 5 minutes" | |
| echo -e " ${DIM}BUN_CONFIG_HTTP_RETRY_COUNT=3${NC}" | |
| dim " Enables automatic HTTP retry on connection failure" | |
| echo -e " ${DIM}CLAUDE_STREAM_IDLE_TIMEOUT_MS=120000${NC}" | |
| dim " Extends the stream idle watchdog timeout to 2 minutes" | |
| echo -e " ${DIM}NODE_OPTIONS=--dns-result-order=ipv4first${NC}" | |
| dim " Prefers IPv4 over IPv6 for DNS results (more stable NAT path)" | |
| echo | |
| if ask "Apply environment variable fixes to $SHELL_CONFIG?"; then | |
| # Check if already applied | |
| if grep -q "$MARKER" "$SHELL_CONFIG" 2>/dev/null; then | |
| warn "Env vars block already present in $SHELL_CONFIG — skipping." | |
| else | |
| cat >> "$SHELL_CONFIG" << ENVBLOCK | |
| $MARKER — begin | |
| export CLAUDE_CODE_REMOTE_SEND_KEEPALIVES=true | |
| export BUN_CONFIG_HTTP_IDLE_TIMEOUT=300 | |
| export BUN_CONFIG_HTTP_RETRY_COUNT=3 | |
| export CLAUDE_STREAM_IDLE_TIMEOUT_MS=120000 | |
| export NODE_OPTIONS="--dns-result-order=ipv4first" | |
| $MARKER — end | |
| ENVBLOCK | |
| success "Environment variables written to $SHELL_CONFIG" | |
| fi | |
| else | |
| warn "Skipped — env vars not applied." | |
| fi | |
| # ── phase 2: SO_KEEPALIVE shim ─────────────────────────────────────────────── | |
| step "Phase 2 of 4 — SO_KEEPALIVE LD_PRELOAD shim" | |
| echo | |
| echo -e " Compiles a small C library (${BOLD}~15KB${NC}) that intercepts every" | |
| echo -e " TCP socket() call and forces ${BOLD}SO_KEEPALIVE=1${NC} before returning it." | |
| echo | |
| echo -e " Without this, the OS kernel never sends keepalive probe packets" | |
| echo -e " on Claude's idle connections, even with tcp_keepalive_time set." | |
| echo | |
| echo -e " The library is installed to: ${BOLD}$LIB_FILE${NC}" | |
| echo -e " ${PRELOAD_VAR} is added to your shell config so Claude picks it up." | |
| echo | |
| echo -e " ${DIM}Requires: $CC${NC}" | |
| echo | |
| if ! command -v "$CC" &>/dev/null; then | |
| if [[ "$PLATFORM" == "macOS" ]] && command -v clang &>/dev/null; then | |
| CC="clang" | |
| else | |
| warn "$CC not found. Install build tools and re-run this phase." | |
| warn " macOS: xcode-select --install" | |
| warn " Debian/Ubuntu: sudo apt install gcc" | |
| warn " Arch: sudo pacman -S gcc" | |
| warn "Skipping Phase 2." | |
| CC="" | |
| fi | |
| fi | |
| if [[ -n "${CC:-}" ]]; then | |
| if ask "Build and install the SO_KEEPALIVE shim?"; then | |
| mkdir -p "$LIB_DIR" | |
| # Write the C source | |
| cat > /tmp/claude_keepalive.c << 'CSRC' | |
| #define _GNU_SOURCE | |
| #include <stddef.h> | |
| #include <sys/socket.h> | |
| #include <dlfcn.h> | |
| /* | |
| * Intercepts socket() and forces SO_KEEPALIVE on every SOCK_STREAM fd. | |
| * Installed via LD_PRELOAD (Linux) / DYLD_INSERT_LIBRARIES (macOS). | |
| * See: https://github.com/anthropics/claude-code/issues/60133 | |
| */ | |
| int socket(int domain, int type, int protocol) { | |
| static int (*orig_socket)(int, int, int) = NULL; | |
| if (!orig_socket) | |
| orig_socket = dlsym(RTLD_NEXT, "socket"); | |
| int fd = orig_socket(domain, type, protocol); | |
| if (fd >= 0 && (type & 0xf) == SOCK_STREAM) { | |
| int opt = 1; | |
| setsockopt(fd, SOL_SOCKET, SO_KEEPALIVE, &opt, sizeof(opt)); | |
| } | |
| return fd; | |
| } | |
| CSRC | |
| # Compile | |
| if [[ "$PLATFORM" == "macOS" ]]; then | |
| $CC -dynamiclib -O2 -o "$LIB_FILE" /tmp/claude_keepalive.c 2>&1 | |
| else | |
| $CC -shared -fPIC -O2 -o "$LIB_FILE" /tmp/claude_keepalive.c -ldl 2>&1 | |
| fi | |
| if [[ -f "$LIB_FILE" ]]; then | |
| success "Library built: $LIB_FILE ($(du -sh "$LIB_FILE" | cut -f1))" | |
| # Add PRELOAD to shell config (inside the managed block if it exists, else append) | |
| if grep -q "$MARKER" "$SHELL_CONFIG" 2>/dev/null; then | |
| # Insert before the end marker | |
| if [[ "$PLATFORM" == "macOS" ]]; then | |
| sed -i '' "/$MARKER — end/i\\ | |
| export ${PRELOAD_VAR}=\"\$HOME/.local/lib/libkeepalive.${LIB_EXT}\${${PRELOAD_VAR}:+:\$${PRELOAD_VAR}}\" | |
| " "$SHELL_CONFIG" | |
| else | |
| sed -i "/$MARKER — end/i export ${PRELOAD_VAR}=\"\$HOME/.local/lib/libkeepalive.${LIB_EXT}\${${PRELOAD_VAR}:+:\$${PRELOAD_VAR}}\"" "$SHELL_CONFIG" | |
| fi | |
| else | |
| cat >> "$SHELL_CONFIG" << PRELOADBLOCK | |
| $MARKER — begin | |
| export ${PRELOAD_VAR}="\$HOME/.local/lib/libkeepalive.${LIB_EXT}\${${PRELOAD_VAR}:+:\$${PRELOAD_VAR}}" | |
| $MARKER — end | |
| PRELOADBLOCK | |
| fi | |
| success "${PRELOAD_VAR} added to $SHELL_CONFIG" | |
| else | |
| error "Compilation failed. Check that $CC is properly installed." | |
| fi | |
| rm -f /tmp/claude_keepalive.c | |
| else | |
| warn "Skipped — SO_KEEPALIVE shim not installed." | |
| fi | |
| fi | |
| # ── phase 3: sysctl tcp_keepalive_time ─────────────────────────────────────── | |
| step "Phase 3 of 4 — TCP keepalive probe interval (requires sudo)" | |
| echo | |
| if [[ "$PLATFORM" == "Linux" ]]; then | |
| CURRENT_KA="$(cat /proc/sys/net/ipv4/tcp_keepalive_time 2>/dev/null || echo unknown)" | |
| echo -e " Current ${BOLD}net.ipv4.tcp_keepalive_time${NC} = $CURRENT_KA seconds" | |
| echo | |
| echo -e " This controls how long a TCP connection can be idle before the" | |
| echo -e " kernel starts sending keepalive probe packets." | |
| echo -e " The default is ${BOLD}120s${NC}. This script sets it to ${BOLD}60s${NC}." | |
| echo | |
| echo -e " Only takes effect alongside Phase 2 (SO_KEEPALIVE shim)." | |
| echo -e " ${DIM}Requires: sudo${NC}" | |
| echo | |
| if ask "Set tcp_keepalive_time=60 (requires sudo)?"; then | |
| sudo sysctl -w net.ipv4.tcp_keepalive_time=60 | |
| success "tcp_keepalive_time set to 60 seconds (active now)" | |
| else | |
| warn "Skipped — tcp_keepalive_time not changed." | |
| fi | |
| elif [[ "$PLATFORM" == "macOS" ]]; then | |
| CURRENT_MSS="$(sysctl -n net.inet.tcp.mssdflt 2>/dev/null || echo unknown)" | |
| CURRENT_BH="$(sysctl -n net.inet.tcp.blackhole 2>/dev/null || echo unknown)" | |
| echo -e " Current ${BOLD}net.inet.tcp.mssdflt${NC} = $CURRENT_MSS (should be 1440, not 512)" | |
| echo -e " Current ${BOLD}net.inet.tcp.blackhole${NC} = $CURRENT_BH (should be 0)" | |
| echo | |
| echo -e " macOS has non-standard TCP defaults that worsen socket drops:" | |
| echo -e " ${BOLD}mssdflt=512${NC} fragments packets (VPNs like Tailscale set this)" | |
| echo -e " ${BOLD}blackhole=1${NC} silently drops packets instead of sending RST" | |
| echo | |
| echo -e " This step corrects both values. ${DIM}Requires: sudo${NC}" | |
| echo | |
| if ask "Fix macOS TCP defaults (requires sudo)?"; then | |
| sudo sysctl -w net.inet.tcp.mssdflt=1440 | |
| sudo sysctl -w net.inet.tcp.blackhole=0 | |
| sudo sysctl -w net.inet.tcp.delayed_ack=0 | |
| success "macOS TCP sysctl values corrected" | |
| else | |
| warn "Skipped — macOS TCP defaults not changed." | |
| fi | |
| fi | |
| # ── phase 4: persist sysctl across reboots ─────────────────────────────────── | |
| step "Phase 4 of 4 — Persist sysctl settings across reboots (requires sudo)" | |
| echo | |
| if [[ "$PLATFORM" == "Linux" ]]; then | |
| SYSCTL_FILE="/etc/sysctl.d/99-claude-keepalive.conf" | |
| echo -e " Writes ${BOLD}$SYSCTL_FILE${NC} so the keepalive time" | |
| echo -e " survives reboots. Without this, Phase 3 resets on next boot." | |
| echo -e " ${DIM}Requires: sudo${NC}" | |
| echo | |
| if ask "Persist tcp_keepalive_time=60 to $SYSCTL_FILE?"; then | |
| echo "net.ipv4.tcp_keepalive_time=60" | sudo tee "$SYSCTL_FILE" > /dev/null | |
| success "Persisted to $SYSCTL_FILE" | |
| else | |
| warn "Skipped — sysctl will reset on next reboot." | |
| fi | |
| elif [[ "$PLATFORM" == "macOS" ]]; then | |
| PLIST="/Library/LaunchDaemons/com.claude.tcp-tuning.plist" | |
| echo -e " Creates a LaunchDaemon plist ${BOLD}$PLIST${NC}" | |
| echo -e " to re-apply the TCP fixes on every boot. ${DIM}Requires: sudo${NC}" | |
| echo | |
| if ask "Create LaunchDaemon to persist macOS TCP fixes?"; then | |
| sudo tee "$PLIST" > /dev/null << 'PLIST' | |
| <?xml version="1.0" encoding="UTF-8"?> | |
| <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" | |
| "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
| <plist version="1.0"> | |
| <dict> | |
| <key>Label</key> <string>com.claude.tcp-tuning</string> | |
| <key>ProgramArguments</key> | |
| <array> | |
| <string>/usr/sbin/sysctl</string> | |
| <string>net.inet.tcp.mssdflt=1440</string> | |
| <string>net.inet.tcp.blackhole=0</string> | |
| <string>net.inet.tcp.delayed_ack=0</string> | |
| </array> | |
| <key>RunAtLoad</key> <true/> | |
| </dict> | |
| </plist> | |
| PLIST | |
| sudo launchctl load "$PLIST" 2>/dev/null || true | |
| success "LaunchDaemon installed: $PLIST" | |
| else | |
| warn "Skipped — sysctl will reset on next reboot." | |
| fi | |
| fi | |
| # ── done ───────────────────────────────────────────────────────────────────── | |
| echo | |
| echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${BOLD}${GREEN}║ All phases complete ║${NC}" | |
| echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" | |
| echo | |
| echo -e " ${BOLD}To activate in the current terminal:${NC}" | |
| echo -e " ${DIM}source $SHELL_CONFIG${NC}" | |
| echo | |
| echo -e " ${BOLD}To verify the fix is working:${NC}" | |
| echo -e " ${DIM}ss -tnop state established '( dport = :443 )' | grep claude${NC}" | |
| echo -e " ${DIM}→ You should see timer:(keepalive,...) on all claude connections${NC}" | |
| echo | |
| echo -e " ${BOLD}To revert all changes:${NC}" | |
| echo -e " ${DIM}bash claude-keepalive-uninstall.sh${NC}" | |
| echo | |
| echo -e " ${DIM}Issue: https://github.com/anthropics/claude-code/issues/60133${NC}" | |
| echo |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # ============================================================================= | |
| # claude-keepalive-uninstall.sh | |
| # Reverts all changes made by claude-keepalive-fix.sh | |
| # | |
| # Removes: | |
| # - Managed env var / LD_PRELOAD block from your shell config | |
| # - ~/.local/lib/libkeepalive.{so,dylib} | |
| # - /etc/sysctl.d/99-claude-keepalive.conf (Linux) | |
| # - /Library/LaunchDaemons/com.claude.tcp-tuning.plist (macOS) | |
| # | |
| # The live sysctl value (tcp_keepalive_time / net.inet.tcp.*) is reset | |
| # to OS defaults; it will also revert automatically on next reboot once | |
| # the persist file is removed. | |
| # ============================================================================= | |
| set -uo pipefail | |
| RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m' | |
| BLUE='\033[0;34m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' | |
| info() { echo -e "${BLUE}[INFO]${NC} $*"; } | |
| success() { echo -e "${GREEN}[OK]${NC} $*"; } | |
| warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } | |
| error() { echo -e "${RED}[ERR]${NC} $*"; } | |
| step() { echo -e "\n${BOLD}${BLUE}▶ $*${NC}"; } | |
| # ── detect OS ──────────────────────────────────────────────────────────────── | |
| OS="$(uname -s)" | |
| if [[ "$OS" == "Darwin" ]]; then | |
| PLATFORM="macOS" | |
| LIB_EXT="dylib" | |
| elif [[ "$OS" == "Linux" ]]; then | |
| PLATFORM="Linux" | |
| LIB_EXT="so" | |
| else | |
| error "Unsupported OS: $OS" | |
| exit 1 | |
| fi | |
| # ── detect shell config ─────────────────────────────────────────────────────── | |
| detect_shell_config() { | |
| local shell_name | |
| shell_name="$(basename "${SHELL:-bash}")" | |
| if [[ "$shell_name" == "zsh" ]]; then | |
| echo "${ZDOTDIR:-$HOME}/.zshrc" | |
| elif [[ "$shell_name" == "bash" ]]; then | |
| if [[ "$PLATFORM" == "macOS" ]]; then | |
| echo "$HOME/.bash_profile" | |
| else | |
| echo "$HOME/.bashrc" | |
| fi | |
| else | |
| echo "$HOME/.profile" | |
| fi | |
| } | |
| SHELL_CONFIG="$(detect_shell_config)" | |
| LIB_FILE="$HOME/.local/lib/libkeepalive.$LIB_EXT" | |
| MARKER="# claude-keepalive-fix — managed block" | |
| # ── header ─────────────────────────────────────────────────────────────────── | |
| echo | |
| echo -e "${BOLD}╔══════════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${BOLD}║ Claude Code — Socket Keepalive Uninstall v1.0 ║${NC}" | |
| echo -e "${BOLD}╚══════════════════════════════════════════════════════════════╝${NC}" | |
| echo | |
| echo -e " Platform : ${BOLD}$PLATFORM${NC}" | |
| echo -e " Shell cfg: ${BOLD}$SHELL_CONFIG${NC}" | |
| echo -e " Library : ${BOLD}$LIB_FILE${NC}" | |
| echo | |
| echo -e " This script reverts all changes made by ${BOLD}claude-keepalive-fix.sh${NC}." | |
| echo -e " Each step will ask for confirmation before making any change." | |
| echo | |
| echo -e "${YELLOW} ⚠ After uninstalling, open a new terminal (or source your shell config)${NC}" | |
| echo -e "${YELLOW} to ensure the environment variables are cleared from your session.${NC}" | |
| echo | |
| # ── step 1: remove managed block from shell config ─────────────────────────── | |
| step "Step 1 of 4 — Remove managed block from $SHELL_CONFIG" | |
| echo | |
| if grep -q "$MARKER" "$SHELL_CONFIG" 2>/dev/null; then | |
| echo -e " Found managed block between markers:" | |
| echo -e " ${DIM}$MARKER — begin${NC}" | |
| echo -e " ${DIM}$MARKER — end${NC}" | |
| echo | |
| echo -e " Lines to be removed:" | |
| awk "/^${MARKER} — begin$/,/^${MARKER} — end$/" "$SHELL_CONFIG" | \ | |
| while IFS= read -r line; do echo -e " ${DIM}$line${NC}"; done | |
| echo | |
| echo -e " Remove this block from $SHELL_CONFIG?" | |
| echo -e " ${DIM}[Y/n]${NC} " | |
| read -r -n1 reply 2>/dev/null || read -r reply; echo | |
| if [[ "$reply" =~ ^[Yy]$ ]] || [[ -z "$reply" ]]; then | |
| if [[ "$PLATFORM" == "macOS" ]]; then | |
| sed -i '' "/^${MARKER} — begin$/,/^${MARKER} — end$/d" "$SHELL_CONFIG" | |
| else | |
| sed -i "/^${MARKER} — begin$/,/^${MARKER} — end$/d" "$SHELL_CONFIG" | |
| fi | |
| # Remove any blank lines left by the deletion (up to 2 consecutive) | |
| if [[ "$PLATFORM" == "macOS" ]]; then | |
| sed -i '' '/^$/N;/^\n$/d' "$SHELL_CONFIG" 2>/dev/null || true | |
| else | |
| sed -i '/^$/N;/^\n$/d' "$SHELL_CONFIG" 2>/dev/null || true | |
| fi | |
| success "Managed block removed from $SHELL_CONFIG" | |
| else | |
| warn "Skipped — shell config not modified." | |
| fi | |
| else | |
| info "No managed block found in $SHELL_CONFIG — nothing to remove." | |
| fi | |
| # ── step 2: remove the shared library ──────────────────────────────────────── | |
| step "Step 2 of 4 — Remove SO_KEEPALIVE shim library" | |
| echo | |
| if [[ -f "$LIB_FILE" ]]; then | |
| echo -e " File: ${BOLD}$LIB_FILE${NC} ($(du -sh "$LIB_FILE" | cut -f1))" | |
| echo | |
| echo -e " Delete this file?" | |
| echo -e " ${DIM}[Y/n]${NC} " | |
| read -r -n1 reply 2>/dev/null || read -r reply; echo | |
| if [[ "$reply" =~ ^[Yy]$ ]] || [[ -z "$reply" ]]; then | |
| rm -f "$LIB_FILE" | |
| success "Removed $LIB_FILE" | |
| else | |
| warn "Skipped — library not removed." | |
| fi | |
| else | |
| info "$LIB_FILE not found — nothing to remove." | |
| fi | |
| # ── step 3: remove sysctl persist file & reset live value ──────────────────── | |
| step "Step 3 of 4 — Remove sysctl persist file (requires sudo)" | |
| echo | |
| if [[ "$PLATFORM" == "Linux" ]]; then | |
| SYSCTL_FILE="/etc/sysctl.d/99-claude-keepalive.conf" | |
| if [[ -f "$SYSCTL_FILE" ]]; then | |
| echo -e " File: ${BOLD}$SYSCTL_FILE${NC}" | |
| echo -e " Content: $(cat "$SYSCTL_FILE")" | |
| echo | |
| echo -e " Also reset ${BOLD}net.ipv4.tcp_keepalive_time${NC} to Linux default (${BOLD}7200${NC} seconds) live." | |
| echo -e " ${DIM}Requires: sudo${NC}" | |
| echo | |
| echo -e " Remove file and reset sysctl?" | |
| echo -e " ${DIM}[Y/n]${NC} " | |
| read -r -n1 reply 2>/dev/null || read -r reply; echo | |
| if [[ "$reply" =~ ^[Yy]$ ]] || [[ -z "$reply" ]]; then | |
| sudo rm -f "$SYSCTL_FILE" | |
| sudo sysctl -w net.ipv4.tcp_keepalive_time=7200 | |
| success "Removed $SYSCTL_FILE and reset tcp_keepalive_time=7200" | |
| else | |
| warn "Skipped — sysctl persist file not removed." | |
| fi | |
| else | |
| info "$SYSCTL_FILE not found — nothing to remove." | |
| fi | |
| elif [[ "$PLATFORM" == "macOS" ]]; then | |
| PLIST="/Library/LaunchDaemons/com.claude.tcp-tuning.plist" | |
| if [[ -f "$PLIST" ]]; then | |
| echo -e " File: ${BOLD}$PLIST${NC}" | |
| echo | |
| echo -e " Also reset macOS TCP sysctls to system defaults:" | |
| echo -e " ${DIM}net.inet.tcp.mssdflt=512 net.inet.tcp.blackhole=0 net.inet.tcp.delayed_ack=3${NC}" | |
| echo -e " ${DIM}Requires: sudo${NC}" | |
| echo | |
| echo -e " Unload and remove LaunchDaemon, reset sysctl?" | |
| echo -e " ${DIM}[Y/n]${NC} " | |
| read -r -n1 reply 2>/dev/null || read -r reply; echo | |
| if [[ "$reply" =~ ^[Yy]$ ]] || [[ -z "$reply" ]]; then | |
| sudo launchctl unload "$PLIST" 2>/dev/null || true | |
| sudo rm -f "$PLIST" | |
| sudo sysctl -w net.inet.tcp.mssdflt=512 | |
| sudo sysctl -w net.inet.tcp.blackhole=0 | |
| sudo sysctl -w net.inet.tcp.delayed_ack=3 | |
| success "LaunchDaemon removed and macOS TCP sysctls reset" | |
| else | |
| warn "Skipped — LaunchDaemon not removed." | |
| fi | |
| else | |
| info "$PLIST not found — nothing to remove." | |
| fi | |
| fi | |
| # ── step 4: verify ──────────────────────────────────────────────────────────── | |
| step "Step 4 of 4 — Verification" | |
| echo | |
| echo -e " Checking for any remaining traces..." | |
| echo | |
| FOUND=0 | |
| if grep -q "CLAUDE_CODE_REMOTE_SEND_KEEPALIVES\|libkeepalive\|BUN_CONFIG_HTTP_IDLE_TIMEOUT\|CLAUDE_STREAM_IDLE_TIMEOUT" "$SHELL_CONFIG" 2>/dev/null; then | |
| warn "Residual claude-keepalive entries still found in $SHELL_CONFIG" | |
| warn "You may need to manually review and clean up $SHELL_CONFIG" | |
| FOUND=1 | |
| fi | |
| if [[ -f "$LIB_FILE" ]]; then | |
| warn "Library still exists: $LIB_FILE" | |
| FOUND=1 | |
| fi | |
| if [[ "$PLATFORM" == "Linux" ]] && [[ -f "/etc/sysctl.d/99-claude-keepalive.conf" ]]; then | |
| warn "Sysctl persist file still exists: /etc/sysctl.d/99-claude-keepalive.conf" | |
| FOUND=1 | |
| fi | |
| if [[ "$PLATFORM" == "macOS" ]] && [[ -f "/Library/LaunchDaemons/com.claude.tcp-tuning.plist" ]]; then | |
| warn "LaunchDaemon still exists: /Library/LaunchDaemons/com.claude.tcp-tuning.plist" | |
| FOUND=1 | |
| fi | |
| if [[ "$FOUND" -eq 0 ]]; then | |
| success "No traces found — uninstall appears complete." | |
| fi | |
| # ── done ───────────────────────────────────────────────────────────────────── | |
| echo | |
| echo -e "${BOLD}${GREEN}╔══════════════════════════════════════════════════════════════╗${NC}" | |
| echo -e "${BOLD}${GREEN}║ Uninstall complete ║${NC}" | |
| echo -e "${BOLD}${GREEN}╚══════════════════════════════════════════════════════════════╝${NC}" | |
| echo | |
| echo -e " ${BOLD}To activate changes in the current terminal:${NC}" | |
| echo -e " ${DIM}source $SHELL_CONFIG${NC}" | |
| echo | |
| echo -e " ${BOLD}To verify no keepalive timers remain on claude sockets:${NC}" | |
| echo -e " ${DIM}ss -tnop state established '( dport = :443 )' | grep claude${NC}" | |
| echo -e " ${DIM}→ Connections should show no timer:(keepalive,...) entry${NC}" | |
| echo | |
| echo -e " ${DIM}Issue: https://github.com/anthropics/claude-code/issues/60133${NC}" | |
| echo |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment