npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

claude-keep-going

v0.14.0

Published

Automatically retry Claude Code sessions on subscription rate limits and sustained API overload (529/500/503) with exponential backoff

Readme

claude-keep-going

Keep unattended Claude Code sessions going through usage limits and API errors.

When Claude Code shows "5-hour limit reached - resets 3pm", this tool waits for the reset and sends "continue". You come back to find your work done.

No dependencies, and the claude command works the same as before.

npm version License: MIT Node.js >= 18

This is a fork of cheapestinference/claude-auto-retry, renamed because it is growing past retries. Upstream has open PRs waiting since July 2026, so changes land here instead.


The Problem

You're in the middle of a complex task with Claude Code. After a while, you see:

You've hit your limit · resets 3pm (Europe/Dublin)

Claude stops. You have to wait hours, come back, and type "continue". If you're running long tasks overnight or while AFK, this kills your productivity.

The Solution

npm i -g claude-keep-going
claude-keep-going install

That's it. Type claude as you always do. When the rate limit hits, the tool:

  1. Detects the rate limit message in the terminal
  2. Parses the reset time (timezone-aware)
  3. Waits until the limit resets + 60s margin
  4. Verifies Claude is still the foreground process
  5. Sends "continue" automatically

You come back to find your task completed.

How it Works

You type "claude"
       │
       ▼
  Shell function (injected in .bashrc/.zshrc)
       │
       ├─ Already in tmux? ──▶ Start background monitor
       │                        Launch claude with full TUI
       │
       └─ Not in tmux? ──▶ Create tmux session transparently
                             Launch claude + monitor inside
                             Attach (looks the same to you)

  MONITOR (background, ~0% CPU):
       │
       ├─ Polls tmux pane every 5 seconds
       ├─ Detects rate limit text
       ├─ Parses reset time from message
       ├─ Waits until reset + safety margin
       ├─ Verifies Claude is still the foreground process
       └─ Sends "continue" via tmux send-keys

Why tmux?

When you disconnect (SSH drops, close terminal, laptop sleeps), tmux keeps running. The monitor keeps waiting. When you reconnect with tmux attach, you find Claude working on your task. This is the key advantage over wrapper scripts.

Features

  • Zero workflow change — same claude command, same TUI, same everything
  • Works with and without tmux — auto-creates tmux session if you're not already in one
  • Auto-installs tmux if missing (apt, dnf, brew, pacman, apk)
  • Timezone-aware — parses reset times with full IANA timezone support (including half-hour offsets)
  • DST-safe — iterative offset correction handles daylight saving transitions
  • Safe send-keys — verifies Claude is still the foreground process before injecting text
  • Self-healing coverage — reconcile re-arms monitors for any live claude session that lost one; an optional timer (systemd --user on Linux, launchd on macOS) runs it automatically (details)
  • Overload backoff — detects sustained API overload (429/500/502/503/504/529) and retries on a configurable exponential backoff with jitter and a cumulative-wait cap, distinct from the usage-reset path (details)
  • Safeguard retry — auto-continues past an AUP-safeguard false-positive (often transient), capped at a few tries so a sticky flag can't loop (details)
  • Interrupted-stream resume — picks the work back up when a laptop suspend or a dropped connection truncates a response mid-turn and leaves the session parked at an idle prompt (details)
  • Near-limit wrap-up nudge — when Claude Code winds the turn down at ~95% of the 5-hour window ("Approaching your 5-hour usage limit — Claude will wrap up the current step") and parks the session at an idle prompt with no limit banner, sends one continue so the work runs on to the real limit, where the usage wait takes over (details)
  • tmux status bar indicator — see at a glance whether a pane is being monitored, waiting on a reset, backing off from overload, or has given up (details)
  • --print mode support — buffers output, retries cleanly for piped/scripted usage
  • Configurable — retry count, wait margin, custom patterns, retry message
  • Config validation — bad config values fall back to safe defaults instead of crashing
  • Zero dependencies — pure Node.js, no node_modules

Messages Detected (verbatim)

The tool acts on these real-world Claude Code renders — if you landed here after pasting one of these errors into a search engine or an AI assistant: yes, this tool automates the wait-and-retry for all of them.

Usage / session limits — waits until the printed reset, then continues

| Render | Example | |--------|---------| | N-hour limit | 5-hour limit reached - resets 3pm (UTC) | | Session limit | You've hit your session limit · resets 2am (Europe/Zurich) | | Weekly limit | You've hit your weekly limit · resets Oct 9, 10am | | Usage limit | Claude usage limit reached. Resets at 2pm | | Out of extra usage | You're out of extra usage · resets 3pm | | Try again | Please try again in 5 hours | | Hit your limit | You've hit your limit · resets 3pm (Europe/Dublin) | | Rate limit | Rate limit hit. Resets at 4pm | | Live-limit companion hint | /usage-credits to finish what you're working on. |

The /rate-limit-options menu — driven to "Stop and wait", never "Upgrade"

What do you want to do?
❯ 1. Upgrade your plan
  2. Stop and wait for limit to reset (3pm)

Handled across any menu layout (the option order varies by Claude Code version); the tool locates the cursor and the "Stop and wait" option, and refuses to press Enter if the layout is unreadable.

API overload / transient errors — exponential backoff with jitter

| Render | Example | |--------|---------| | Terminal API error (colon form) | API Error: 529 {"type":"error","error":{"type":"overloaded_error","message":"Overloaded"}} | | 5xx family | API Error: 500 / 502 / 503 / 504 … (including bodyless renders like 503 no healthy upstream) | | API-level 429 | API Error: Server is temporarily limiting requests (not your usage limit) · Rate limited |

Safeguard false positives — bounded immediate re-send

API Error: <model>'s safeguards flagged this message (https://www.anthropic.com/legal/aup).
They may flag safe, normal content as well. … Claude Code can't respond to this request with <model>.

Interrupted streams — one resume, then stop

API Error: Your computer went to sleep mid-response. The response above may be incomplete.
API Error: Connection lost mid-response. The response above may be incomplete.
API Error: The response stopped arriving. The response above may be incomplete.
API Error: Server error mid-response. The response above may be incomplete.

…and the three "before a response was produced. Try again." variants (suspend, stall, connection). All seven come from the same stream finalizer and leave the same wreckage: a truncated turn at an idle prompt.

Near-limit wrap-up — one nudge, then the usage wait

⏺ Approaching your 5-hour usage limit — Claude will wrap up the current step.

Not an error: Claude Code prints this at ~95% of the window and tells the model to checkpoint. The model finishes the step, lists what's left and ends the turn — no banner, idle prompt, nothing resumes it. One continue picks the work back up.

Custom patterns can be added via config for future message format changes.

Detection is chrome-aware: it looks at the live bottom of the pane, but first skips past Claude Code's UI furniture — the input box, footer, key hints, the todo/task widget, the status spinner, and the /usage-credits hint. So a genuine limit banner still registers even when a tall task list or background-agent status pushes it well above the prompt, while a banner merely quoted in scrollback (with real output below it) does not trigger a retry.

Your own customPatterns are the exception: they are matched against the raw last-N lines (not the chrome-skipped view), so a pattern keyed on footer text — a usage percentage, a model name — keeps firing. You own the false-positive tradeoff for your regexes; the built-in detection keeps the chrome-aware discipline.

Configuration

Optional. Create ~/.config/claude-keep-going/config.json (or under $XDG_CONFIG_HOME if you set it). A system-wide file at /etc/xdg/claude-keep-going/config.json (each $XDG_CONFIG_DIRS entry) is read first, and your file overrides it key by key, including keys inside blocks like overload.

{
  "maxRetries": 5,
  "pollIntervalSeconds": 5,
  "marginSeconds": 60,
  "fallbackWaitHours": 5,
  "retryMessage": "Continue where you left off. The previous attempt was rate limited.",
  "customPatterns": ["my custom pattern"]
}

| Option | Default | Description | |--------|---------|-------------| | maxRetries | 5 | Max retry attempts per rate-limit event | | pollIntervalSeconds | 5 | How often to check the terminal (seconds) | | marginSeconds | 60 | Extra wait after reset time (seconds) | | fallbackWaitHours | 5 | Wait time if reset time can't be parsed | | retryMessage | "Continue where..." | Message sent to Claude on retry | | customPatterns | [] | Additional regex patterns to detect rate limits |

All fields optional. Invalid values fall back to defaults automatically.

Launch wrapper

Set CLAUDE_KEEP_GOING_LAUNCH_WRAPPER to a prefix command and it's prepended to each interactive session — useful for keeping a machine awake while Claude works, or any other per-process wrapper:

# macOS: don't sleep while a session runs
export CLAUDE_KEEP_GOING_LAUNCH_WRAPPER="caffeinate -i"

Generic (not macOS-specific — e.g. nice, chrt … work too). Unset or blank spawns claude directly, unchanged.

Naming the session

The auto-created session is named claude-keep-going-<pid>-<timestamp> by default — unique, but tmux ls after a few launches is a wall of timestamps with nothing to say which checkout each one belongs to. Name it instead:

claude --tmux-session api          # this launch
tmux attach -t '=api'              # …and back to it later

export CLAUDE_KEEP_GOING_SESSION_NAME=api   # every launch from this shell (direnv, etc.)

--tmux-session is consumed by the launcher and never reaches claude; the flag wins over the env var. Notes:

  • . and : are rewritten to _ — tmux reserves them as target separators, so --tmux-session my.api creates my_api (the launcher tells you when it does this).
  • A name already in use is an error, not a second session: the launcher prints the tmux attach command for the existing one and exits non-zero. Unnamed launches keep their generated name and never collide.
  • Attaching uses an exact-name target (-t '=api'), so a session whose name is a prefix of another's (api vs api-worker) still resolves to the right one.
  • Inside an existing tmux session, or in --print mode, no session is created and the flag does nothing — the launcher says so rather than ignoring it silently.

Session lifetime

When claude exits cleanly inside the auto-created tmux session, the session now ends with it — tmux reaps it, nothing lingers. When the launcher exits non-zero (a crash), the pane falls through to your login shell so the scrollback survives for inspection. Two opt-outs:

# Always keep a shell in the pane after claude exits (the pre-0.7 behavior)
export CLAUDE_KEEP_GOING_KEEP_SHELL=1

# Never create a tmux session (e.g. you're inside Zellij/screen and don't want nesting).
# Note: the monitor needs a tmux pane to watch, so this disables auto-retry for the run.
export CLAUDE_KEEP_GOING_NO_TMUX=1

Environment forwarding

Your full shell environment reaches claude inside the tmux session via a 0600 snapshot file under $XDG_RUNTIME_DIR/claude-keep-going/tmp/ that only the launcher reads (and deletes immediately). Nothing about your environment — names or values — ever appears on a tmux command line, so secrets can't surface in /proc/<pid>/cmdline.

After a usage limit resets

Reset time from the statusline

When the limit screen shows no reset time (the /rate-limit-options menu often doesn't), the wait falls back to fallbackWaitHours. With the statusline tap in place, the monitor reads which usage window is full and when it resets, and waits for that instead. A reset time on the screen still wins.

Claude Code's own auto-continue

Claude Code 2.1.287 can continue a session after a usage limit on its own (the autoContinueAtUsageLimit setting, not yet enabled for every account). It gives up if the machine slept through the reset. To avoid sending a second continue on top of its own, and to cover it when it doesn't act, the monitor lets it go first when the hooks are installed:

  • After the reset (plus marginSeconds) it waits native.graceSeconds more.
  • A new turn starting in that time means the session resumed, and nothing is sent.
  • Claude Code reporting that it gave up (quota_auto_resume_stale or quota_auto_resume_disabled) sends the continue at once.
  • If nothing happens, the continue goes out and the log says native-missed.
{ "native": { "usageLimit": "defer", "graceSeconds": 180 } }

"usageLimit": "ignore" sends right away, as if the native feature didn't exist. Without the hooks the monitor can't see what Claude Code did, so it sends right away too.

Network check

After a resume from suspend the reset has usually passed but the network isn't back yet. Before sending the continue, the monitor checks that api.anthropic.com:443 accepts a connection (or the proxy in HTTPS_PROXY), and holds while it doesn't, for up to maxWaitMinutes.

{ "networkCheck": { "enabled": true, "host": "api.anthropic.com", "port": 443, "maxWaitMinutes": 10 } }

Weekly-limit model fallback

Off by default. Some weekly limits apply to one model: "You've hit your Opus limit · resets Oct 9, 10am". Waiting means days without work, while other models still have room. With modelFallback.enabled, the monitor sends /model <fallback> and then the continue message, and once the limit has reset it sends /model <original> at the next idle prompt.

{ "modelFallback": { "enabled": true, "map": { "Opus": "sonnet" }, "switchBack": true } }

map keys are the model names Claude Code puts in the banner ("Opus", "Sonnet"); values are what /model accepts. Limits that cover every model ("session limit", "weekly limit") still wait. The original model comes from the statusline tap or the PostModelSwitch hook; without either, the monitor switches back to the lowercased banner name (/model opus).

Overload backoff

Separate from subscription rate limits, this fork also detects sustained API overload — Claude Code's own terminal API Error: <code> line for the retryable set (429 / 500 / 502 / 503 / 504 / 529, or an overloaded_error JSON body) — and retries on an exponential backoff instead of waiting for a usage reset. The two paths never collide; usage limits always take precedence.

Sustained only. Claude Code already retries transient 5xx/529 internally with its own backoff. This feature fires only when those internal retries are exhausted and a terminal error is left in the pane. It should rarely trigger.

Terminal vs. transient. Claude Code renders an in-progress retry as the parens form API Error (529 …) · Retrying in 5s · attempt 3/10, and the final exhausted error as the colon form API Error: 529 …. Detection requires the colon form and suppresses the · Retrying… / attempt n/m suffix, so the tool never interrupts Claude's own backoff.

Anchored, tail-only matching (why it won't fire on your code). Patterns are case-insensitive regexes matched against only the last 12 lines of the pane — never the full scrollback. They are anchored to Claude Code's API Error: <code> render, so a bare 503 in code you're editing (res.status(503)), a port number, a quoted log, or a status.claude.com link in a comment will not trip detection. The one residual: a live tail that literally contains API Error: 529 (e.g. editing this tool, or docs about Claude errors) will match — set "enabled": false while doing that. (Earlier versions matched bare status numbers across the whole capture, which injected spurious retries during ordinary web-dev sessions.) For a structured, ambiguity-free trigger see DESIGN-NOTES.md.

Configured under an overload block (shown with its defaults):

{
  "overload": {
    "enabled": true,
    "patterns": ["API Error:\\s*(429|500|502|503|504|529)\\b", "overloaded_error", "temporarily limiting requests"],
    "backoffSeconds": [30, 60, 120, 240, 300],
    "steadyStateSeconds": 300,
    "jitterPct": 15,
    "maxTotalWaitMinutes": 120,
    "retryMessage": "Continue where you left off.",
    "relaunchOnExit": false,
    "relaunchCommand": "claude --continue"
  }
}

| Option | Default | Description | |--------|---------|-------------| | enabled | true | Turn the overload path on/off | | patterns | (see above) | Case-insensitive regexes matching a terminal overload error in the pane tail (last 12 lines) | | backoffSeconds | [30,60,120,240,300] | Wait before each retry; index i for attempt i | | steadyStateSeconds | 300 | Wait once the backoffSeconds array is exhausted | | jitterPct | 15 | ±% jitter applied to every wait (clamped 0–100) | | maxTotalWaitMinutes | 120 | Cumulative-wait cap — give up loudly past this | | retryMessage | "Continue where you left off." | Sent to Claude on each retry | | relaunchOnExit | false | See the gating decision below | | relaunchCommand | "claude --continue" | Command used by relaunchOnExit |

The waits go 30 → 60 → 120 → 240 → 300 → 300 …, each with ±15% jitter, until the error clears (success) or the cumulative wait reaches maxTotalWaitMinutes (give up — the cap guards against hammering a genuinely-down endpoint or masking a real outage; check status.claude.com).

Event-driven detection (recommended — no scraping)

The scraper above is a heuristic over terminal output. For an exact, ambiguity-free trigger, install the StopFailure hook — Claude Code fires it precisely when a turn ends in an API error, with a typed error class:

claude-keep-going install-hook                  # into $CLAUDE_CONFIG_DIR or ~/.claude
claude-keep-going install-hook /path/to/config  # repeat per CLAUDE_CONFIG_DIR you use

This adds a StopFailure hook (matcher overloaded|server_error|rate_limit) that writes a pane-keyed marker the monitor consumes — no terminal scraping, so it cannot false-positive on code or scrollback. Sessions launched via the wrapper after installing the hook use it automatically; the first marker latches event mode and disables the scraper for that session. Sessions without the hook (or pre-install) fall back to the anchored scraper. Remove with uninstall-hook. See DESIGN-NOTES.md for the architecture.

Why does rate_limit go through the hook too? A rate_limit is the subscription session/usage limit — an hours-scale wait until a printed reset time, not a seconds-scale overload retry. The monitor routes it to a separate usage-wait path (never the overload backoff above), resolving the reset time from the live pane scrape or, if that missed it, the session's transcript. This closes a race in the scrape-only design: the limit notice is a one-shot transcript line, not a persistently-redrawn banner, so a poll that misses its brief on-screen window could previously strand a session with no retry for hours (#50).

Gating decision (alive-at-prompt vs exited-to-shell)

A transient API error in interactive Claude Code surfaces inline and leaves the process alive at its prompt — it does not exit to the shell. So the default, robust behavior reuses the existing usage-limit mechanism: only retry when the foreground process is claude/node and the session is idle, not working (the esc to interrupt footer is absent). Retrying mid-internal-retry would double-drive the session, so that case is deferred, never sent.

If a 500 ever does drop you to the shell, send-keys is correctly blocked by the foreground check (it never types into bash), and the tool logs overload-exited-to-shell rather than masking it. Auto-relaunch is off by default — blindly typing claude --continue into a shell the user may be using is worse than surfacing the stall. Set relaunchOnExit: true (and adjust relaunchCommand) only if you actually observe shell-exits on overload.

Hook signals and the statusline tap

Besides StopFailure, install-hook registers hooks that tell the monitor where a turn stands, so it doesn't have to read that off the screen:

| Hook | Marker | What the monitor learns | |---|---|---| | Stop | stop | The turn ended, how many background agents and tasks are still running, whether a scheduled wakeup is pending, and the end of the last message | | UserPromptSubmit | prompt | A turn started, and who started it | | Notification | notify | A permission prompt is open, or Claude Code's own auto-continue fired or gave up | | PostCompact | compact | A compaction finished | | PostModelSwitch | model | The model changed |

These hooks run async and print nothing, so they never slow a turn down or add text to it. Markers are keyed by tmux pane like the StopFailure ones. Sessions that reconcile adopted get them too, because the hook falls back to $TMUX_PANE.

Some numbers only exist in the JSON Claude Code gives its statusLine command: when the prompt cache expires, when each usage window resets, and how full the context is. To let the monitor see them, put statusline-tap in front of your statusline command:

{
  "statusLine": {
    "type": "command",
    "command": "claude-keep-going statusline-tap -- /path/to/your-statusline"
  }
}

The tap saves those fields for the pane and runs your command with the same input. With nothing after -- it prints nothing.

install-hook --dump also appends every raw hook payload to ~/.local/state/claude-keep-going/hook-dump.jsonl. Use it to check what your Claude Code version actually sends, then remove it with uninstall-hook --dump.

Idle compaction

Off by default. Claude can't run /compact itself, so a long unattended run tends to end with a message like "please run /compact now" that nobody reads until morning. By then the prompt cache has expired, and the first message re-sends the whole context cold.

With compact.enabled, the monitor sends /compact itself when all of these hold:

  • the last turn ended (Stop hook) and no new one started
  • no background agents or tasks are still running, and no scheduled wakeup is pending
  • no permission prompt is open
  • something asked for it (see trigger below)
  • the time window and the away check pass, if you set them
  • the prompt cache is still warm
  • Claude is in the foreground and the input box is empty

It needs the hooks from install-hook. Timing against the cache needs the statusline tap; without it the monitor waits a fixed number of minutes after the turn.

{
  "compact": {
    "enabled": true,
    "trigger": "both",
    "window": { "start": "01:00", "end": "07:00" },
    "awayMinutes": 30,
    "minContextPercent": 40,
    "settle": { "mode": "before-expiry", "marginSeconds": 60 },
    "focus": "",
    "matchLastMessage": false
  }
}

| Key | Default | Meaning | |---|---|---| | enabled | false | Turn the feature on. | | trigger | "request" | request: only when the session asked (below). policy: whenever the context is at least minContextPercent full. both: either. | | window | null | Local-time window, may wrap past midnight. null means any time. | | awayMinutes | null | Only when no attached tmux client has had input for this long. No client attached counts as away. | | minContextPercent | 40 | Threshold for the policy trigger. | | minContextTokens | null | Also require this many tokens in the prompt for the policy trigger. | | settle.mode | "before-expiry" | before-expiry fires marginSeconds before the cache expires. fixed fires minutes after the turn ends: 4 suits the 5-minute cache, 55 the 1-hour one. | | focus | "" | Text appended to /compact, telling the summary what to keep. | | matchLastMessage | false | Also act when the last message mentions /compact, for sessions that ask a person instead of using request. | | minIntervalMinutes | 30 | Never compact twice within this long. |

The session asks with a command it can run through its Bash tool:

claude-keep-going request compact [what the summary should keep]

A line like this in CLAUDE.md tells it to:

When a long task is done and the context is large, run
`claude-keep-going request compact <what to keep>` instead of asking me to run /compact.

If the cache has already gone cold, the monitor skips the compaction: at that point it would cost a full cold read on top of the next message's. Every decision is logged ("Compacting at 03:41", "Sent /compact", "Compaction finished").

Safeguard retry

A third failure mode, separate from usage limits and 5xx overloads: the model's safeguards flag your message and Claude Code can't respond. It renders like:

● API Error: Fable 5's safeguards flagged this message (…/legal/aup). They may flag
  safe, normal content as well. … Claude Code can't respond to this request with Fable 5.
  Double press esc to edit your last message, or try a different model with /model.

These flags are often false positives (the message says so) and semi-random, so an immediate re-send frequently clears them. When the tool sees this render at an idle prompt, it sends a short retry message (continue by default), waits a few seconds, and repeats — but only up to maxRetries times, then gives up loudly (logged) rather than looping. A sticky flag means the content/model combination is genuinely blocked; switch models with /model or rephrase.

Detection is tail-anchored (last 12 pane lines) like the overload path, and a match additionally requires the API Error render line nearby — so the phrases appearing in scrollback or in a conversation about safeguards won't trigger it.

Configured under a safeguard block (defaults shown):

{
  "safeguard": {
    "enabled": true,
    "patterns": ["safeguards flagged this message", "can't respond to this request with", "legal/aup"],
    "maxRetries": 3,
    "retryDelaySeconds": 8,
    "retryMessage": "continue"
  }
}

| Option | Default | Description | |--------|---------|-------------| | enabled | true | Turn the safeguard-retry path on/off | | patterns | (see above) | Case-insensitive regexes marking the safeguard render (matched in the pane tail, near an API Error line) | | maxRetries | 3 | Re-send attempts before giving up — kept small; retrying a sticky flag won't help | | retryDelaySeconds | 8 | Wait between re-sends | | retryMessage | "continue" | Message sent to nudge past the flag |

Usage limits always take precedence; the safeguard path only acts when Claude is idle (no esc to interrupt footer) and the foreground process is claude/node.

Interrupted-stream resume

Close your laptop lid while Claude is mid-answer and the work does not survive. Claude Code wraps the response body in a byte watchdog; when the bytes stop arriving it aborts the stream and finalizes whatever had already been printed, naming the cause:

⏺ API Error: Your computer went to sleep mid-response. The response above may be
  incomplete.

The turn is then over. The prompt returns idle and nothing resumes it, so the session sits there — potentially for hours — with a half-finished answer on screen. Claude Code retries by itself only while the response is still thinking-only; once real content has been yielded it declines to retry, which is exactly when this render appears. That is the gap this closes: seeing it at an idle prompt, the tool sends continue, bounded at maxRetries because a machine that just woke may not have its network back yet.

The same finalizer emits the render for a dropped connection, a stalled stream and a mid-response server error. All are the same truncated-turn state with the same remedy, so all are matched — sleeping is just the cause we can name.

Detection is anchored on the shape of the line, not its vocabulary: a real render begins with API Error:, behind at most Claude's message glyph. The "an API Error line nearby" rule the overload and safeguard paths use is deliberately not enough here — these sentences are ordinary English, so a session merely explaining them quotes the whole render, anchor included, mid-sentence. Prose carries it mid-line and your own typed line carries ❯, so neither trips it.

Configured under a streamInterrupted block (defaults shown):

{
  "streamInterrupted": {
    "enabled": true,
    "patterns": ["went to sleep mid-response", "Connection lost mid-response", "…"],
    "maxRetries": 2,
    "retryDelaySeconds": 5,
    "retryMessage": "continue"
  }
}

| Option | Default | Description | |--------|---------|-------------| | enabled | true | Turn the interrupted-stream path on/off | | patterns | (all 7 renders) | Case-insensitive regexes, matched only against a line that begins with the API Error: render | | maxRetries | 2 | Resume attempts before giving up loudly | | retryDelaySeconds | 5 | Wait before resuming — lets the network settle after a wake | | retryMessage | "continue" | Message sent to pick the work back up |

Usage limits take precedence, and like the safeguard path this only acts when Claude is idle and the foreground process is claude/node.

Near-limit wrap-up nudge

At roughly 95% of the 5-hour window, Claude Code injects a checkpoint instruction into the model's context — finish the current step, then list up to three short bullets of the most impactful remaining work, don't start subagents or long-running work — and prints:

⏺ Approaching your 5-hour usage limit — Claude will wrap up the current step.

The model does exactly that and ends the turn. There is no limit banner (the limit has not been hit), the prompt returns idle, and nothing resumes the session — so an overnight run winds down at 95% and stays parked long after the window has reset. This is a server-side Claude Code behavior with no user-facing switch (it is feature-flagged, not a setting), so the tool handles the render instead.

Seeing the notice at an idle prompt, the tool sends one continue. The session then either finishes its work or runs into the real limit, where the usage wait takes over as usual. Unlike the retry families above this is not a bounded machine: the nudge renders as a user row under the notice, and a notice with a user row below it — yours or ours — belongs to a turn that has already been answered, so it is never nudged twice. The maxRetries cap only bounds the pathological case where the nudge never renders.

Detection is anchored on the shape of the line, like the interrupted-stream render: the notice begins its line, behind at most Claude's message glyph, so prose quoting it and your own typed copy don't trip it.

Configured under a nearLimitWrapUp block (defaults shown):

{
  "nearLimitWrapUp": {
    "enabled": true,
    "maxRetries": 3,
    "retryMessage": "continue"
  }
}

Set "enabled": false if you want the session to stop at the checkpoint.

tmux status bar indicator

The monitor writes a small JSON snapshot per pane on every poll tick, so you can tell at a glance — without checking logs — whether a pane is being watched, waiting out a usage-limit reset, backing off from overload or a safeguard flag, or has given up.

Add a segment to status-right (or status-left) in ~/.tmux.conf that shells out to the bundled reader script, passing the current pane id and the server's socket path:

set -g status-interval 5
set -g status-right "#(~/.local/lib/node_modules/claude-keep-going/bin/tmux-status.sh '#{pane_id}' '#{socket_path}') | %Y-%m-%d %H:%M"

Use an absolute path, not the bare command name. #() commands run inside the tmux server's own environment, not the environment of whichever shell you attached from — if the server was started before your shell rc added npm/nvm's bin directory to PATH (e.g. tmux auto-started at login, or by another program), the bare command name resolves to nothing and the segment stays permanently blank with no error anywhere. Find your actual install path with which claude-keep-going-tmux-status (run it in a normal shell, then hardcode that path in .tmux.conf) if it differs from the example above. If you use nvm and switch Node versions, re-check the path.

tmux substitutes #{pane_id} and #{socket_path} itself before running the command (these are tmux format variables, resolved at expansion time — not environment variables the script has to go looking for), so the segment always reflects whichever pane you're looking at, correctly scoped to the tmux server it belongs to. The socket_path argument matters if you ever run more than one tmux server on the same machine (e.g. tmux -L work, tmux -L personal, or two users' default servers on a shared host): pane ids like %2 are only unique within a server, so without it two different servers' %2 panes would render each other's status. Always pass it: the monitor keys each status file by the socket path it inherits from $TMUX, so a single-argument config looks under a shared default key the monitor never writes to, and the segment simply stays blank.

It prints:

| Pane state | Indicator | |------------|-----------| | Actively monitoring | 🟢KG | | Idle compaction is on and its window is open | 🟢KG 🗜 | | Idle compaction is on, window opens at 22:00 | 🟢KG 🗜22:00 | | Idle compaction scheduled, 3 minutes left | 🟢KG 🗜3m | | Waiting on a usage-limit reset | ⏳KG 1h30m | | Backing off from overload | 🟠KG 45s | | Retrying past a safeguard/AUP false-positive | 🛡KG 8s | | Given up — max retries/backoff cap reached; no further automatic action on this pane | 🔴KG | | No monitor for this pane, or the status file is stale (monitor process died without cleaning up) | (nothing) |

🔴KG overrides whatever the underlying status would otherwise render. Several give-up paths intentionally leave the monitor's internal status at whatever it was when it stopped acting (so the scraper/event logic doesn't re-detect its own stale error next tick) — without an explicit gaveUp flag in the snapshot, the status bar would keep showing a live 🟢/⏳/🟠 indicator for a monitor that will not act again on this pane until the underlying condition clears on its own.

Staleness is derived from each snapshot's own pollIntervalSeconds (age > 2× the monitor's configured poll interval) rather than a fixed constant, so a healthy monitor running with a longer pollIntervalSeconds doesn't have its segment blank out for a large fraction of every tick.

The script (bin/tmux-status.sh) is pure POSIX shell with no dependencies (no jq, no node), so it's cheap to run every few seconds from every attached client. status-interval defaults to 15s in tmux; dropping it to 5 (matching the monitor's default pollIntervalSeconds) keeps the overload countdown responsive.

In the Claude Code statusline

Claude Code runs its statusLine command inside the pane, with $TMUX and $TMUX_PANE set, so the badge can go there instead of the tmux bar:

badge=$(claude-keep-going status --pane "$TMUX_PANE")

It prints the same badge as tmux-status.sh, or nothing when the pane has no live monitor, and finds the status file the same way the monitor does. Pass --socket <path> to read a different tmux server than the one in $TMUX.

CLI Commands

claude-keep-going install          # Install shell wrapper + tmux, offer to migrate old files
claude-keep-going migrate [--yes]  # Move files from ~/.claude-auto-retry* to XDG paths
claude-keep-going uninstall        # Remove shell wrapper
claude-keep-going status           # Show monitor activity + last log entries
claude-keep-going logs             # Tail today's log file in real-time
claude-keep-going version          # Print version

# Event-driven overload detection (optional; see "Overload backoff")
claude-keep-going install-hook [dir]    # Install the hooks into a config dir (--dump: also record payloads)
claude-keep-going uninstall-hook [dir]  # Remove them (--dump: only the recording)
claude-keep-going statusline-tap -- <cmd...>  # statusLine wrapper, see "Hook signals"
claude-keep-going request compact [focus]     # For the model: compact once this session is idle

# Monitor coverage (see "Keeping monitors alive")
claude-keep-going reconcile        # Re-arm a monitor for every live claude pane not covered
claude-keep-going reconcile --dry-run   # Preview without arming
claude-keep-going install-timer    # Run reconcile every 5 min (systemd --user on Linux,
                                   # launchd LaunchAgent on macOS)
claude-keep-going uninstall-timer  # Remove the timer
claude-keep-going exclude-self     # Keep THIS session unmonitored (durable, self-expiring)

For AI Agents

If you are an AI agent (Claude Code, Codex, etc.) installing this for your user, the full setup is non-interactive:

npm install -g claude-keep-going
claude-keep-going install        # shell wrapper (+ tmux if missing)
claude-keep-going install-hook   # recommended: event-driven, scrape-free overload detection

Notes for agents:

  • The wrapper takes effect in new shells — have the user source their shell rc or open a new terminal. Sessions launched via the wrapper run inside tmux (required by the monitor).
  • Verify with claude-keep-going status (monitor activity) and claude-keep-going logs.
  • Configuration is optional and defaults are safe. To change it, write ~/.config/claude-keep-going/config.json (see Configuration); invalid values fall back to defaults instead of crashing.
  • If the user runs multiple CLAUDE_CONFIG_DIRs, repeat claude-keep-going install-hook <path> per dir.
  • Clean removal: claude-keep-going uninstall and claude-keep-going uninstall-hook.

Keeping monitors alive

Each claude you launch through the wrapper gets its own background monitor. Monitors are detached processes with no supervising service, so if one is killed — or a claude is started outside the wrapper — that session ends up unmonitored, and only new sessions get a monitor. Two commands restore and maintain full coverage:

  • reconcile re-arms a monitor for every live tmux pane running claude that isn't already covered. It maps each claude to its pane from live process state, keeps one monitor per pane (idempotent — safe to run anytime, and a single-instance lock stops an overlapping manual+timer run from double-arming), and handles tmux pane-id reuse. Print- mode sessions (claude -p) are skipped, and a claude that doesn't set its process title to claude (a bare node shebang) isn't detected — use the wrapper for those. Run it after a crash, or use --dry-run to see what it would do.
  • install-timer wires reconcile to a timer that runs every 5 minutes, so a monitor that dies is re-armed within one interval — coverage self-heals with no manual step. On Linux this is a systemd --user timer (enable loginctl enable-linger $USER once if you want it to run while logged out); on macOS it is a launchd LaunchAgent in ~/Library/LaunchAgents (LaunchAgents only run while you are logged in, which is fine — the tmux server it reconciles lives in your login session too).

Excluding a session. To keep a specific session unmonitored (e.g. one where you're pasting rate-limit text and don't want any auto-retry), run claude-keep-going exclude-self from inside it. This records the session's claude PID in $XDG_RUNTIME_DIR/claude-keep-going/reconcile-exclude; both reconcile and the timer skip it. Keying on the PID makes the entry self-expiring: dead PIDs are pruned when the file is read, so once that claude exits its entry is dropped and can never accidentally mute a later session (tmux reuses pane ids, so a hand-added %pane exclude could — pane ids are not pruned, since staleness can't be detected). Prefer the PID form; you can also hand-add a %pane id or a PID to that file.

Platform Support

Operating Systems

| OS | tmux auto-install | Status | |----|-------------------|--------| | Ubuntu / Debian | apt-get | Fully supported | | CentOS / RHEL / Fedora | dnf | Fully supported | | Rocky Linux / Amazon Linux | dnf | Fully supported | | macOS | brew | Fully supported | | Arch Linux | pacman | Fully supported | | Alpine | apk | Fully supported | | Windows | — | Not supported natively — the tool drives a tmux pane, which Windows does not have. Use WSL2 (Ubuntu), where it works as on Linux. A native backend via a tmux-compatible multiplexer is being discussed in #79. |

Requirements

  • Node.js >= 18
  • tmux >= 2.1 (auto-installed if missing)

Shell Support

| Shell | Status | |-------|--------| | bash | Full (auto-install to ~/.bashrc) | | zsh | Full (auto-install to ~/.zshrc) | | fish | Manual setup (instructions printed on install) |

NixOS / Nix

The repo is also a flake, for anyone who'd rather manage this declaratively than run npm i -g + install. It exposes:

  • packages.<system>.default: the package, built from source (no npm registry fetch).
  • overlays.default: adds claude-keep-going to pkgs.
  • nixosModules.default: a NixOS module, no home-manager required.
  • homeManagerModules.default: a home-manager module (Linux and Darwin).

Both modules install the package, wire up the shell wrapper (bash and zsh, same runtime-branching script described above), and manage the reconcile timer (systemd --user on Linux, a launchd agent under home-manager on Darwin) and the StopFailure hook declaratively. No imperative install/install-hook/install-timer step, and no shell-rc file to keep mutable for it.

NixOS system module:

{
  inputs.claude-keep-going.url = "github:someonewithpc/claude-keep-going";

  outputs = { self, nixpkgs, claude-keep-going, ... }: {
    nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
      modules = [
        claude-keep-going.nixosModules.default
        { programs.claude-keep-going.enable = true; }
      ];
    };
  };
}

home-manager module (works without a NixOS host, including on Darwin):

{
  imports = [ claude-keep-going.homeManagerModules.default ];
  programs.claude-keep-going.enable = true;
}

settings takes the same keys as the JSON config file. The NixOS module writes them to /etc/xdg/claude-keep-going/config.json, home-manager to ~/.config/claude-keep-going/config.json:

programs.claude-keep-going = {
  enable = true;
  settings = {
    maxRetries = 3;
    overload.maxTotalWaitMinutes = 60;
  };
};

See nix/common.nix for the full option list (package, settings, shellIntegration.{bash,zsh}, installHook, reconcileTimer.{enable,startupDelay,interval}).

--print Mode

For scripted/piped usage (claude -p "..." | jq), the tool:

  1. Buffers all output (nothing goes to stdout until done)
  2. If rate-limited: discards partial output, waits, re-executes with same args
  3. Consumer receives a single clean response
# This just works — retries transparently if rate-limited
claude -p "Generate a JSON schema" | jq .

Where files live

| What | Path | |---|---| | Config | $XDG_CONFIG_HOME/claude-keep-going/config.json, default ~/.config/... | | Logs | $XDG_STATE_HOME/claude-keep-going/logs/, default ~/.local/state/... | | Status files, StopFailure markers, reconcile lock and exclude list, env snapshots | $XDG_RUNTIME_DIR/claude-keep-going/ |

Without XDG_RUNTIME_DIR (macOS, some cron or su sessions) the runtime files go to $TMPDIR/claude-keep-going-<uid>/, which the tool refuses to use unless you own it.

The shell wrapper, the monitor, the hook, the reconcile timer and tmux-status.sh all have to agree on these paths. If you set a custom XDG_* value in your shell rc, export it to the systemd user manager as well (~/.config/environment.d/), or the timer looks in the default place.

Older versions kept everything under ~/.claude-auto-retry*. See Upgrading from claude-auto-retry.

Upgrading from claude-auto-retry

claude-keep-going is a fork of claude-auto-retry, and 0.8 also used the old file locations. claude-keep-going install finds them and asks before moving anything:

  • ~/.claude-auto-retry.json moves to ~/.config/claude-keep-going/config.json. If a new config already exists, the old one stays put and you merge it by hand.
  • Log files move to ~/.local/state/claude-keep-going/logs/. A log for the same day is joined, old lines first.
  • The rest of ~/.claude-auto-retry/ (status files, markers, lock) is deleted. If monitors from the older install are still running, it stays until they exit, and the next reconcile run deletes it.

Each file is copied and compared before the original is removed. Run claude-keep-going migrate to do this on its own, --yes to skip the question. Without a terminal and without --yes, nothing moves. The Nix modules run migrate --yes for you.

install also removes the old claude() block from your rc files, install-timer replaces the old reconcile timer, and install-hook replaces the old StopFailure entry.

Logging

Logs are written to ~/.local/state/claude-keep-going/logs/YYYY-MM-DD.log (under $XDG_STATE_HOME if set):

[2026-03-18 15:00:05] [INFO] Monitor started for pane %3 (claude PID: 12345)
[2026-03-18 15:32:10] [INFO] Rate limit detected: "5-hour limit reached - resets 3pm". Waiting 3547s...
[2026-03-18 16:01:10] [INFO] Sent retry message (attempt 1)

Logs rotate daily. Files older than 7 days are cleaned automatically.

Uninstall

claude-keep-going uninstall
npm uninstall -g claude-keep-going

This removes the shell function from your rc files. tmux is left installed.

Known Limitations

  1. Retry message context — The retry message is sent as plain text. If Claude was mid-confirmation or in a special input state, it may not interpret it as a continuation. You can customize the message via config.

  2. Node version lock — The launcher path is resolved at install time. If you switch Node versions with nvm, re-run claude-keep-going install.

  3. tmux required — The tool needs tmux to monitor terminal output and inject keystrokes. It auto-installs if missing, but requires sudo for system package managers.

Contributing

Contributions are welcome! Here's how to get started:

Development Setup

git clone https://github.com/someonewithpc/claude-keep-going.git
cd claude-keep-going
npm test            # Run all 128 tests
npm link            # Install locally for testing

Project Structure

claude-keep-going/
├── bin/cli.js              # CLI: install, hook, reconcile, timer, status, logs, ...
├── src/
│   ├── patterns.js         # Rate limit + overload detection + ANSI stripping
│   ├── time-parser.js      # Reset time parsing with timezone support
│   ├── config.js           # Config loading + validation
│   ├── logger.js           # File-based logging with rotation
│   ├── tmux.js             # tmux command wrappers (execFile-based)
│   ├── monitor.js          # Core monitoring loop + retry logic (usage + overload paths)
│   ├── events.js           # StopFailure hook event channel (scrape-free overload trigger)
│   ├── reconcile.js        # Re-arm monitors for all live claude panes + exclusion
│   ├── launcher.js         # Process orchestration + signal forwarding
│   └── wrapper.sh          # Shell function template
├── systemd/                # systemd --user units for the reconcile timer (Linux)
├── launchd/                # LaunchAgent plist for the reconcile timer (macOS)
├── test/                   # tests across the src modules
├── package.json
├── LICENSE
└── README.md

Architecture Decisions

  • Zero dependencies — only Node.js built-ins. Reduces supply chain risk and install size.
  • execFile over exec — all child process calls use array-based args to prevent shell injection.
  • stdio: 'inherit' — Claude gets the real TTY for full TUI support. The monitor reads pane content independently via tmux capture-pane.
  • Iterative DST correction — timezone offset is computed via 3-iteration convergence loop, not a single-shot formula that breaks at DST boundaries.
  • Config validation — invalid user config values fall back to safe defaults instead of producing NaN/undefined behavior.

Running Tests

npm test                              # All tests
node --test test/patterns.test.js     # Single file
node --test --watch test/             # Watch mode

Submitting Changes

  1. Fork the repo
  2. Create a feature branch (git checkout -b feat/my-feature)
  3. Write tests first (TDD)
  4. Make your changes
  5. Ensure all tests pass (npm test)
  6. Submit a Pull Request

Areas for Contribution

  • New rate limit patterns — If you see a Claude Code rate limit message that isn't detected, open an issue with the exact text.
  • Fish shell support — Auto-install for fish shell (currently manual).
  • Windows support — WSL works, but native Windows would need a different approach.
  • Notification integration — Desktop/Slack notification when rate limit detected or when Claude resumes.

Related Projects

FAQ

Q: Does this work with Claude Max/Pro/Team? A: Yes. It works with any Anthropic subscription that has usage-based rate limits.

Q: Does it work outside of tmux? A: Yes. If you're not in tmux, it creates a tmux session transparently. You won't notice a difference.

Q: What if I continue manually before the retry fires? A: The monitor checks if the rate limit is still visible before sending keys. If you already continued, it resets and keeps watching.

Q: What if Claude exits while the monitor is waiting? A: The monitor checks the Claude process every 30 seconds during the wait. If Claude exits, the monitor shuts down cleanly.

Q: Does it consume a lot of resources? A: No. tmux capture-pane is extremely lightweight. The monitor uses ~0% CPU at a 5-second polling interval.

Q: Can it accidentally type into the wrong program? A: The monitor verifies the foreground process is node or claude before sending keys. If you've switched to vim, bash, or anything else, it skips the retry.

License

MIT — see LICENSE for details.


Originally written by CheapestInference. Fork maintained by Hugo Sales.