{"slug": "show-hn-cckeep-claude-code-s-remote-session-gives-up-reconnecting-in-31s", "title": "Show HN: Cckeep – Claude Code's remote session gives up reconnecting in 31s", "summary": "Cckeep, a new open-source tool by developer Kamihork, prevents Claude Code's Remote Control sessions from silently disconnecting by re-arming them within 31 seconds, addressing a known issue where sessions give up reconnecting after 5 attempts with exponential backoff. The tool runs as a background job via launchd on macOS or systemd on Linux, checking every 15 seconds, and requires Claude Code to run inside tmux. It also works around a separate bug (anthropics/claude-code#34255) where sessions wedge in reconnecting mode, which has been open since March 2026 with 99 upvotes and no fix.", "body_md": "**Keeps Claude Code Remote Control from silently going dead.**\n\nRemote Control retries for about 31 seconds and then gives up for good.\n\n`cckeep`\n\nnotices, and re-arms the session — without touching one that's busy.\n\n[Remote Control](https://code.claude.com/docs/en/remote-control) lets you drive a local Claude Code session from your phone or from claude.ai. It reconnects on its own when the link drops — for **5 attempts with 1/2/4/8/16-second backoff**. That is a **31-second** budget. Close your laptop lid, switch Wi-Fi, ride an elevator, and the budget is gone. The connection closes and never comes back.\n\nThere is a second failure too: the session wedges in `/rc reconnecting`\n\nand sits there forever. That one is [anthropics/claude-code#34255](https://github.com/anthropics/claude-code/issues/34255) — open since March 2026, 99 👍, no fix.\n\nEither way you find out the same way: you reach for your phone, and the session is gone. The documented recovery is to walk back to your desk and type `/remote-control`\n\n.\n\n```\nnpm install -g cckeep\ncckeep enable\n```\n\n`npm install`\n\nonly puts the CLI on your `PATH`\n\n; `cckeep enable`\n\nis the step that registers a background job — launchd on macOS, a systemd user timer on Linux — that checks every 15 seconds and re-arms whatever went dead.\n\nInstall it globally rather than running `npx cckeep enable`\n\n. The scheduled job runs cckeep from wherever it was installed, and npx's cache is throwaway: a job pointing into it keeps working until the cache is cleared and then stops, silently — the one failure a watchdog must not have. `cckeep enable`\n\nrefuses to schedule from an npx path for that reason.\n\nTo look before installing anything, `npx`\n\nis fine — neither of these changes a thing:\n\n```\nnpx cckeep            # what it sees right now\nnpx cckeep doctor     # check tmux, panes, and the scheduler\n```\n\n** command not found: cckeep right after installing?** You are on a shim-based\nversion manager.\n\n`nodenv`\n\nand `rbenv`\n\n-style setups need a rehash before a newly\ninstalled binary appears on `PATH`\n\n, and `nvm`\n\nneeds a new shell:\n\n```\nnodenv rehash     # nodenv\nasdf reshim nodejs # asdf\n# nvm: just open a new shell\n```\n\nOne requirement: **Claude Code has to be running inside tmux.** A session started in a bare terminal cannot be reached from another process, so there is nothing any tool can do for it. See [Running Claude Code in tmux](#running-claude-code-in-tmux).\n\nIf cckeep saved you a walk back to your desk, a ⭐ helps other Remote Control users find it.\n\nThis is the whole design problem. A watchdog that types into your terminal on a timer is a liability unless it is certain the moment is safe. Every one of these is enforced, and [tested](/kamihork/cckeep/blob/main/test):\n\n**Never on top of what you typed.** Enter submits whatever is in the composer, so an unsent draft would go out with the command glued onto it. If anything is in the box — or the box cannot be found at all — nothing is sent. The idle check cannot cover this: a draft sitting in the box is perfectly still.**Never during a turn.** The pane is captured three times. A running turn animates a spinner and a token counter, so identical captures mean nothing is happening. Three rather than two, at an interval that is deliberately not round, because any animation whose period divides the interval would otherwise alias into identical frames.**Never into a dialog.** Permission prompts turn Enter into a selection. A selection marker counts anywhere on screen; the plain English phrasings only count near the composer, since Claude Code writes sentences like \"Do you want me to run the tests as well?\" in ordinary replies.**Never into the panel you opened.**`/remote-control`\n\nopens a status panel with a QR code. cckeep only presses Enter there when it opened the panel itself, and only if the panel is still up when it goes to press it.**Never a session that only mentioned it.** The indicators are read from the last few rows, never from the transcript, so a session discussing`/rc active`\n\nis not mistaken for a connected one. A pane that has never been seen connected is only ever acted on when Claude Code itself prints that the link died.**Never in a tight loop, and never twice at once.** One action per pane per 5 minutes, and a lock so that`cckeep watch`\n\nrunning alongside the scheduled job cannot interleave two passes into one garbled prompt.**Never against a wall.** Replies like “Remote Control requires a claude.ai subscription” mean reconnecting is impossible here — an auth or plan problem the command cannot fix — so the pane is dropped until the link is seen healthy again. And for failure modes it cannot recognise, at most 3 re-arms per outage; after that it stops and waits to see the link healthy rather than retyping into your transcript forever.**Re-checked at the last moment.** The decision is made from one capture, then re-verified after the wait — reconnected in between, dialog appeared, something typed? Nothing is sent, and the pane keeps the progress it had made rather than starting its wait over.\n\n`--dry-run`\n\nprints what it would do and sends nothing.\n\n| State on screen | What it means | What cckeep does |\n|---|---|---|\n`/rc active` , or a truncated `/rc` |\nconnected | remembers the pane, nothing else |\n`/rc reconnecting` |\ninside the 31-second budget | waits — this usually resolves |\n`/rc reconnecting` , 2 minutes on |\nwedged (\n|\n\n`Remote Control disconnected`\n\n```\ncckeep                 # status: one line per Claude Code pane\ncckeep watch           # run in the foreground instead of scheduling\ncckeep once            # a single pass — what the scheduler runs\ncckeep enable          # start checking in the background\ncckeep disable         # stop checking\ncckeep doctor          # tmux, panes, scheduler, paths\ncckeep logs            # what it has done\n```\n\nOptions: `--dry-run`\n\n, `--json`\n\n, `--interval <s>`\n\n, `--lang en|ja`\n\n(auto-detected from `LANG`\n\n).\n\n`install`\n\nand `uninstall`\n\nstill work as aliases for `enable`\n\nand `disable`\n\n.\n\ncckeep reads and types into tmux panes. That is the only channel a separate process has into a live Claude Code session — and it is why the session must be started inside tmux. Restarting the process is not an alternative: it would end the conversation, which is exactly what you are trying to save.\n\nThe smallest change is a shell function that wraps interactive launches only, so `claude update`\n\n, `claude doctor`\n\nand `claude -p`\n\nstill behave normally:\n\n```\ncc() {\n  local a\n  for a in \"$@\"; do\n    case \"$a\" in\n      -p|--print|-v|--version|-h|--help|--bg|--background|--output-format) command claude \"$@\"; return ;;\n      agents|auth|doctor|install|mcp|plugin|project|setup-token|update|upgrade|remote-control|rc|config)\n        command claude \"$@\"; return ;;\n      -*) ;;\n      *) break ;;\n    esac\n  done\n  local session=\"claude-$(basename \"$PWD\")-$(printf '%s' \"$PWD\" | cksum | cut -d' ' -f1)\"\n  if [ -n \"$TMUX\" ]; then command claude \"$@\"; return; fi\n  if ! tmux has-session -t \"=$session\" 2>/dev/null; then\n    tmux new-session -s \"$session\" -c \"$PWD\" claude \"$@\"; return\n  fi\n\n  # The tmux session outlives the claude process inside it, so attaching can\n  # drop you at a bare shell with the conversation gone. Bring it back.\n  local pane_cmd cmd\n  pane_cmd=$(tmux list-panes -t \"=$session:\" -F '#{pane_current_command}' 2>/dev/null | head -1)\n  case \"$pane_cmd\" in\n    zsh|bash|sh|fish)\n      cmd=\"claude\"\n      case \" $* \" in\n        *\" -c \"*|*\" --continue \"*|*\" -r \"*|*\" --resume \"*|*\" --session-id \"*) ;;\n        *) cmd=\"$cmd --continue\" ;;\n      esac\n      [ $# -gt 0 ] && cmd=\"$cmd $*\"\n      tmux send-keys -t \"=$session:\" \"$cmd\" C-m ;;\n  esac\n  tmux attach-session -t \"=$session\"\n}\n```\n\n`cc -c`\n\nand `cc --continue`\n\nwork as you'd expect — the flag is passed straight\nthrough to Claude Code. The block above covers the case the flag can't: a tmux\nsession whose Claude Code process has already exited.\n\nClaude Code also needs two lines in `~/.tmux.conf`\n\n, or Shift+Enter and desktop notifications break inside tmux ([official guidance](https://code.claude.com/docs/en/terminal-config#configure-tmux)):\n\n```\nset -g allow-passthrough on\nset -s extended-keys on\nset -as terminal-features 'xterm*:extkeys'\n```\n\nCtrl+B needs no fix: Claude Code detects tmux and rebinds its own shortcut to `Ctrl+B Ctrl+B`\n\n.\n\nDefaults are tuned so you never notice it. Override in `~/.cckeep/config.json`\n\n, by environment variable, or per-run flag — later wins. A malformed config is a hard error rather than a silent half-load.\n\n```\n{\n  \"interval\": 15,\n  \"cooldown\": 300,\n  \"stuckLimit\": 8,\n  \"missLimit\": 4,\n  \"settle\": 2000,\n  \"paneCommand\": \"claude\"\n}\n```\n\n`interval`\n\n— seconds between passes (also what`enable`\n\nschedules)`cooldown`\n\n— seconds before the same pane may be acted on again`stuckLimit`\n\n— checks in`reconnecting`\n\nbefore the bridge is treated as wedged`missLimit`\n\n— checks with no indicator before re-arming a pane that had one`maxRearms`\n\n— re-arms allowed per outage before it gives up until the link is seen healthy again`settle`\n\n— milliseconds between the two captures of the idle check; raise it on a slow machine`paneCommand`\n\n— foreground process name that marks a pane as Claude Code`tmuxSocket`\n\n— socket name or path, if your tmux runs on something other than the default server (`tmux -L name`\n\n/`-S path`\n\n). Empty means the default`tmuxBinary`\n\n— absolute path to tmux, if yours lives somewhere the usual lookup misses\n\nEvery key has an env twin: `CCKEEP_INTERVAL`\n\n, `CCKEEP_COOLDOWN`\n\n, `CCKEEP_STUCK_LIMIT`\n\n, `CCKEEP_MISS_LIMIT`\n\n, `CCKEEP_MAX_REARMS`\n\n, `CCKEEP_SETTLE`\n\n, `CCKEEP_PANE_COMMAND`\n\n, `CCKEEP_TMUX_SOCKET`\n\n, `CCKEEP_TMUX`\n\n. Whatever is set when you run `cckeep enable`\n\nis written into the scheduled job, so a socket set only in your shell does not quietly go missing from the background run. `CCKEEP_HOME`\n\nmoves state, config and log off `~/.cckeep`\n\n.\n\ncckeep re-arms the connection. It does **not** raise Claude Code's retry budget — that is a constant inside a closed-source binary, and only Anthropic can change it. If [#34255](https://github.com/anthropics/claude-code/issues/34255) is fixed, this tool becomes unnecessary, which is the right outcome. Until then, a 👍 there is worth more than a star here.\n\nOut of reach by design:\n\n**Sessions outside tmux**— no channel to type into** The VS Code extension**— not a terminal TUI; tmux cannot wrap it** Server mode**(`claude remote-control`\n\n) — that one is a process you own, so supervise it with launchd/systemd directly, or a`while true`\n\nloop**Outages past ~10 minutes**— Claude Code exits the session itself; there is nothing left to re-arm\n\ncckeep reads the visible text of your tmux panes to decide whether a pane is connected. That text is your conversation. Therefore:\n\n- everything stays on your machine; there is no network code in this package\n- no telemetry, no account, no phone-home\n- pane text is matched against a handful of indicator strings and thrown away — only pane labels and verdicts reach the log\n- the log lives at\n`~/.cckeep/cckeep.log`\n\nand rolls over at 512 KB, keeping one previous generation;`cckeep logs`\n\nprints the recent lines\n\nClaude Code paints a Remote Control indicator in its footer: `/rc active`\n\nwhen connected, `/rc reconnecting`\n\nwhile retrying, and a `Remote Control disconnected`\n\nnotification when it gives up. cckeep finds the panes running Claude Code, reads those indicators out of `tmux capture-pane`\n\n, and keeps a small per-pane counter in `~/.cckeep/state.json`\n\n.\n\nFinding those panes takes one extra step: Claude Code rewrites its own process title, so tmux reports such a pane as `2.1.220`\n\nrather than `claude`\n\n. Matching the name tmux reports therefore finds nothing on a real machine. cckeep checks the process table as well, and treats a pane as Claude Code's when the pane's process — or anything it spawned — is actually `claude`\n\n.\n\nThe indicator is right-aligned, so a custom status line or a narrow pane squeezes it down to a bare `/rc`\n\nwith the word cut off. cckeep treats that as connected: the indicator only renders while a link exists, and reading it as connected merely records the pane and waits.\n\nWhere it looks matters as much as what it looks for. The state indicators are read from the last dozen lines only, because the words themselves turn up in ordinary conversation — a session where you happen to discuss `/rc active`\n\nwould otherwise read as connected. Dialog and status-panel detection deliberately scans the whole pane instead: a false positive there costs one skipped pass, while a miss costs a keystroke in the wrong place.\n\nThe decision layer (`src/detect.js`\n\n) is a pure function of screen text plus prior state, which is why the safety rules can be tested exhaustively without a terminal. The runner (`src/run.js`\n\n) does the I/O: the idle check, the last-moment re-check, and the keystrokes.\n\nNone of this is a published API — the indicator strings are UI text and can change. When they do, cckeep stops acting rather than acting wrongly: a pane it cannot read looks \"never connected\", and panes it has never seen connected are never touched.\n\n```\ngit clone https://github.com/kamihork/cckeep.git && cd cckeep\nnpm test                       # 36 tests, no network, no tmux required\nnode bin/cckeep.js doctor\n```\n\nThe test suite fakes tmux, so it runs anywhere. Contributions welcome — especially indicator strings from Claude Code versions or terminals where detection misses. See [CONTRIBUTING.md](/kamihork/cckeep/blob/main/CONTRIBUTING.md).", "url": "https://wpnews.pro/news/show-hn-cckeep-claude-code-s-remote-session-gives-up-reconnecting-in-31s", "canonical_source": "https://github.com/kamihork/cckeep", "published_at": "2026-08-04 05:56:11+00:00", "updated_at": "2026-08-04 06:22:37.465452+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-infrastructure"], "entities": ["Cckeep", "Kamihork", "Claude Code", "Anthropic", "tmux", "launchd", "systemd", "anthropics/claude-code#34255"], "alternates": {"html": "https://wpnews.pro/news/show-hn-cckeep-claude-code-s-remote-session-gives-up-reconnecting-in-31s", "markdown": "https://wpnews.pro/news/show-hn-cckeep-claude-code-s-remote-session-gives-up-reconnecting-in-31s.md", "text": "https://wpnews.pro/news/show-hn-cckeep-claude-code-s-remote-session-gives-up-reconnecting-in-31s.txt", "jsonld": "https://wpnews.pro/news/show-hn-cckeep-claude-code-s-remote-session-gives-up-reconnecting-in-31s.jsonld"}}