<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Karthikeyan Natarajan</title>
    <description>The latest articles on DEV Community by Karthikeyan Natarajan (@karthikeyan_natarajan_1cb).</description>
    <link>https://dev.to/karthikeyan_natarajan_1cb</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1798983%2Fcc833f6e-8e9a-450e-8344-c33c45864a6c.jpg</url>
      <title>DEV Community: Karthikeyan Natarajan</title>
      <link>https://dev.to/karthikeyan_natarajan_1cb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/karthikeyan_natarajan_1cb"/>
    <language>en</language>
    <item>
      <title>I Built a Shell Wrapper That Makes Claude Code Auto-Resume After Rate Limits</title>
      <dc:creator>Karthikeyan Natarajan</dc:creator>
      <pubDate>Mon, 13 Apr 2026 14:36:36 +0000</pubDate>
      <link>https://dev.to/karthikeyan_natarajan_1cb/i-built-a-shell-wrapper-that-makes-claude-code-auto-resume-after-rate-limits-2lje</link>
      <guid>https://dev.to/karthikeyan_natarajan_1cb/i-built-a-shell-wrapper-that-makes-claude-code-auto-resume-after-rate-limits-2lje</guid>
      <description>&lt;p&gt;If you use &lt;strong&gt;Claude Code&lt;/strong&gt; for long agentic sessions — code generation, refactoring, automated research — you have almost certainly hit this screen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You're out of extra usage · resets 7:30pm (Asia/Calcutta)

  &amp;gt; continue  &amp;gt; exit without saving
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Claude's rate limits reset on a rolling 5-hour and 7-day window. When either window fills, the CLI drops you into an exit menu. If you pick &lt;em&gt;continue&lt;/em&gt;, it asks you the same question again. Eventually you just have to quit, wait, and come back later.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The problem:&lt;/strong&gt; "come back later" is vague. You don't know if "later" is 12 minutes or 4 hours. You have to manually resume the session with the right UUID. If you forget, context is lost.&lt;/p&gt;

&lt;p&gt;I got tired of babysitting this, so I built &lt;strong&gt;Smart Resume&lt;/strong&gt; — a 350-line bash/zsh wrapper that handles the whole lifecycle automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  What It Does (30-second version)
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;  ╭────────────────────────────────────────────────────────────────────╮
  │  Smart Resume for Claude Code  ·  Karthikeyan N  ·  MIT License   │
  ╰────────────────────────────────────────────────────────────────────╯

  ⚡ Rate limit hit
  ──────────────────────────────────────────────────────────────────────
  Session  "rl-2026-04-12-projects-myapp"
  Resets   00:30:00 IST  (2026-04-13)
  Waking   00:31:00 IST  (+60s buffer)
  ──────────────────────────────────────────────────────────────────────
  Press Ctrl-C to cancel

    Waiting until reset.  Remaining: 4 min 23s

  ╭──────────────────────────────────────────────────╮
  │  ✓ Resuming  "rl-2026-04-12-projects-myapp"      │
  ╰──────────────────────────────────────────────────╯
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You type &lt;code&gt;claude&lt;/code&gt; as usual. When a rate limit hits, instead of leaving you stranded at a menu, the wrapper:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Intercepts Claude's exit&lt;/li&gt;
&lt;li&gt;Parses the exact reset timestamp from Claude's session JSONL&lt;/li&gt;
&lt;li&gt;Displays a countdown in a single updating terminal line&lt;/li&gt;
&lt;li&gt;Sleeps until the reset (plus a 60-second buffer)&lt;/li&gt;
&lt;li&gt;Resumes the same session automatically with &lt;code&gt;claude --resume &amp;lt;uuid&amp;gt;&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If the resumed session hits &lt;em&gt;another&lt;/em&gt; rate limit, the whole cycle repeats. You can leave it running overnight in a tmux session and come back to a fully-resumed Claude context in the morning.&lt;/p&gt;




&lt;h2&gt;
  
  
  Architecture: Three Components
&lt;/h2&gt;

&lt;p&gt;The system has three pieces that work together.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. &lt;code&gt;statusline.sh&lt;/code&gt; — the sensor
&lt;/h3&gt;

&lt;p&gt;Claude Code supports a &lt;code&gt;statusLine&lt;/code&gt; hook in &lt;code&gt;~/.claude/settings.json&lt;/code&gt; — a command that Claude calls after every response, receiving a JSON payload that includes rate limit usage percentages and reset timestamps.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"statusLine"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"/home/karthik/.claude/statusline.sh"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;statusline.sh&lt;/code&gt; script reads this payload via &lt;code&gt;jq&lt;/code&gt; and renders a custom terminal status bar. But more importantly for Smart Resume, when usage hits &lt;strong&gt;90%&lt;/strong&gt;, it writes a flag file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# From statusline.sh&lt;/span&gt;
&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rl_5h_int&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 90 &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rl_7d_int&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nt"&gt;-ge&lt;/span&gt; 90 &lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
    &lt;/span&gt;&lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'5h_pct=%s\n5h_reset=%s\n7d_pct=%s\n7d_reset=%s\n'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rl_5h_int&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rl_5h_rst&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
        &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rl_7d_int&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;rl_7d_rst&lt;/span&gt;&lt;span class="k"&gt;:-&lt;/span&gt;&lt;span class="nv"&gt;0&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$rl_warn_flag&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The flag file at &lt;code&gt;~/.claude/.rl_warn&lt;/code&gt; stores pre-computed Unix epochs for the reset time. When the wrapper wakes up after Claude exits, it can read the reset epoch directly — no timestamp parsing needed.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;claude-smart-resume.sh&lt;/code&gt; — the watcher and scheduler
&lt;/h3&gt;

&lt;p&gt;This is the main wrapper. It lives at &lt;code&gt;~/.claude/claude-smart-resume.sh&lt;/code&gt; and is installed as a shell alias:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;alias &lt;/span&gt;&lt;span class="nv"&gt;claude&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$HOME&lt;/span&gt;&lt;span class="s2"&gt;/.claude/claude-smart-resume.sh"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the alias shadows the &lt;code&gt;claude&lt;/code&gt; name, every invocation — including shorthand aliases like &lt;code&gt;cc&lt;/code&gt; or &lt;code&gt;ccam&lt;/code&gt; — transparently goes through the wrapper. The real binary is called by its absolute path to avoid recursion.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The flag file — &lt;code&gt;~/.claude/.rl_warn&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;A simple key=value file that acts as the inter-process communication channel between the statusline sensor and the resume wrapper. Classic Unix filesystem-as-message-bus.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Tricky Parts
&lt;/h2&gt;

&lt;p&gt;Building this involved several non-obvious problems. Here are the ones I found most interesting.&lt;/p&gt;

&lt;h3&gt;
  
  
  Running Claude in the Foreground — for real
&lt;/h3&gt;

&lt;p&gt;The most important design constraint: Claude must run &lt;strong&gt;as a direct foreground child&lt;/strong&gt; of the wrapper script. This sounds obvious but it rules out several approaches.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not &lt;code&gt;(claude &amp;amp;); fg&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When the script runs as a child of an interactive shell (as it does via an alias), the parent shell owns the terminal session. Calling &lt;code&gt;fg&lt;/code&gt; or &lt;code&gt;tcsetpgrp&lt;/code&gt; from a child process that isn't the session leader fails silently or throws an error — only the session leader can reassign the foreground process group.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not a subshell with &lt;code&gt;exec&lt;/code&gt;?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;code&gt;exec&lt;/code&gt; replaces the current process image, which means the wrapper ceases to exist and can't do anything after Claude exits.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The solution:&lt;/strong&gt; run Claude directly in the foreground as a simple child call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_run_claude&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="c"&gt;# ... start watcher in background ...&lt;/span&gt;
  &lt;span class="nb"&gt;trap&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt; INT    &lt;span class="c"&gt;# absorb Ctrl-C — don't let it kill the wrapper&lt;/span&gt;
  &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$CLAUDE_BIN&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nb"&gt;trap&lt;/span&gt; - INT
  &lt;span class="c"&gt;# ... cleanup watcher ...&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;By calling the binary directly (not via subshell or exec), Claude inherits the terminal naturally. The trap ensures that Ctrl-C reaches Claude but doesn't kill the wrapper script — the wrapper only exits when Claude itself exits.&lt;/p&gt;

&lt;h3&gt;
  
  
  Finding Claude's PID Without Polling the Process Table
&lt;/h3&gt;

&lt;p&gt;The background watcher needs to send &lt;code&gt;SIGINT&lt;/code&gt; to Claude when it detects a rate-limit line in the session JSONL. But at the moment the watcher starts, Claude hasn't launched yet — there's a tiny race window.&lt;/p&gt;

&lt;p&gt;The naive approach is &lt;code&gt;pgrep -f claude&lt;/code&gt;, but that's fragile (matches any process with "claude" in its name) and races with Claude's startup time.&lt;/p&gt;

&lt;p&gt;The clean approach: read Claude's PID from &lt;code&gt;/proc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# In the background watcher subshell:&lt;/span&gt;
&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; raw &amp;lt; &lt;span class="s2"&gt;"/proc/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;my_pid&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/task/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;my_pid&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/children"&lt;/span&gt; 2&amp;gt;/dev/null
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/&amp;lt;pid&amp;gt;/children&lt;/code&gt; lists the immediate child PIDs of a given process. Because the watcher knows the wrapper's PID (&lt;code&gt;$$&lt;/code&gt;), and Claude is spawned as a direct child of the wrapper, this gives a clean, race-free PID with no grepping the process table.&lt;/p&gt;

&lt;p&gt;The watcher polls this file in a tight loop (50 ms intervals) until Claude appears, then hands the PID off to the JSONL poller.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;claude_pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;''&lt;/span&gt; &lt;span class="nv"&gt;i&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;0
&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; i++ &amp;lt; 200 &lt;span class="o"&gt;))&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$claude_pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do
  &lt;/span&gt;&lt;span class="nb"&gt;read&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; raw &amp;lt; &lt;span class="s2"&gt;"/proc/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;my_pid&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/task/&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;my_pid&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/children"&lt;/span&gt; 2&amp;gt;/dev/null &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nv"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;pgrep &lt;span class="nt"&gt;-d&lt;/span&gt;&lt;span class="s1"&gt;' '&lt;/span&gt; &lt;span class="nt"&gt;-P&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$my_pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nb"&gt;true
  &lt;/span&gt;&lt;span class="k"&gt;for &lt;/span&gt;pid &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nv"&gt;$raw&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$watcher_self&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="k"&gt;continue&lt;/span&gt;  &lt;span class="c"&gt;# skip the watcher itself&lt;/span&gt;
    &lt;span class="nv"&gt;claude_pid&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$pid&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nb"&gt;break
  &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
  &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="nt"&gt;-z&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$claude_pid&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sleep &lt;/span&gt;0.05
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There's a fallback to &lt;code&gt;pgrep -P&lt;/code&gt; for systems where &lt;code&gt;/proc&lt;/code&gt; layout differs (or on macOS, which doesn't have the same &lt;code&gt;/proc&lt;/code&gt; structure).&lt;/p&gt;

&lt;h3&gt;
  
  
  Parsing Timestamps That Come in Multiple Formats
&lt;/h3&gt;

&lt;p&gt;Claude's rate-limit message can express the reset time in several forms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;7:30pm (Asia/Calcutta)&lt;/code&gt; — time only, today (or tomorrow if it's already past)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Apr 26 7:30pm (America/New_York)&lt;/code&gt; — date + time, no year&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Apr 26, 2026 7:30pm (UTC)&lt;/code&gt; — full date with year&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;GNU &lt;code&gt;date -d&lt;/code&gt; handles all of these when passed the right timezone:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;parse_reset_epoch&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;reset_time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nv"&gt;reset_tz&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;reset_epoch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nv"&gt;TZ&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$reset_tz&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="nb"&gt;date&lt;/span&gt; &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$reset_time&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; +%s 2&amp;gt;/dev/null&lt;span class="si"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;return &lt;/span&gt;1
  &lt;span class="nv"&gt;now_epoch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt; +%s&lt;span class="si"&gt;)&lt;/span&gt;

  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;((&lt;/span&gt; reset_epoch &amp;lt;&lt;span class="o"&gt;=&lt;/span&gt; now_epoch &lt;span class="o"&gt;))&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then&lt;/span&gt;
    &lt;span class="c"&gt;# Only roll over to "tomorrow" for time-only strings.&lt;/span&gt;
    &lt;span class="c"&gt;# A full date string that's in the past means something went wrong — bail.&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;[[&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="k"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;reset_time&lt;/span&gt;&lt;span class="p"&gt;,,&lt;/span&gt;&lt;span class="k"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;~ ^[0-9]+:[0-9]+[apm]+&lt;span class="nv"&gt;$ &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
      &lt;/span&gt;&lt;span class="nv"&gt;reset_epoch&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$((&lt;/span&gt; reset_epoch &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="m"&gt;86400&lt;/span&gt; &lt;span class="k"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else
      return &lt;/span&gt;1
    &lt;span class="k"&gt;fi
  fi
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$reset_epoch&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;+86400&lt;/code&gt; rollover only applies to bare time strings like &lt;code&gt;7:30pm&lt;/code&gt;. A full date string (&lt;code&gt;Apr 26 7:30pm&lt;/code&gt;) already resolves to the correct future epoch — adding a day would overshoot by 24 hours. This distinction is a CLAUDE.md rule in the repo because it's easy to break.&lt;/p&gt;

&lt;p&gt;On macOS, BSD &lt;code&gt;date&lt;/code&gt; doesn't support &lt;code&gt;-d&lt;/code&gt;. The macOS variant of the script shells out to &lt;code&gt;python3&lt;/code&gt; (always available on macOS 12.3+) for epoch parsing instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  Never Use &lt;code&gt;strings&lt;/code&gt; on JSONL Files
&lt;/h3&gt;

&lt;p&gt;Claude's session files are JSONL — one JSON object per line. To extract the session name or the rate-limit reset time, the obvious temptation is &lt;code&gt;strings &amp;lt;file&amp;gt; | grep "resets"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Don't. &lt;code&gt;strings&lt;/code&gt; is a binary analysis tool that extracts printable ASCII runs by byte. On multi-byte UTF-8 content (which JSONL often contains), it can split a single JSON object across multiple lines, or merge adjacent objects, causing silent misses.&lt;/p&gt;

&lt;p&gt;The correct approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get session name from JSONL&lt;/span&gt;
&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-F&lt;/span&gt; &lt;span class="s1"&gt;'"type":"custom-title"'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$session_file&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;tail&lt;/span&gt; &lt;span class="nt"&gt;-1&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-oP&lt;/span&gt; &lt;span class="s1"&gt;'"customTitle":"\K[^"]+'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;grep -F&lt;/code&gt; treats the pattern as a literal string (no regex interpretation), which is both correct and faster for fixed-string matching on JSON.&lt;/p&gt;

&lt;h3&gt;
  
  
  All Output Goes to stderr
&lt;/h3&gt;

&lt;p&gt;This one is subtle. Claude Code's &lt;code&gt;--print&lt;/code&gt; flag makes the CLI emit its response to stdout for piping into other tools. If the wrapper writes any diagnostic output to stdout, it silently corrupts the pipe.&lt;/p&gt;

&lt;p&gt;Every &lt;code&gt;printf&lt;/code&gt; that produces user-visible output in the wrapper ends with &lt;code&gt;&amp;gt;&amp;amp;2&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;_bold&lt;span class="o"&gt;()&lt;/span&gt;   &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\e[1m%s\e[0m'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
_yellow&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\e[33m%s\e[0m'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$*&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
_nl&lt;span class="o"&gt;()&lt;/span&gt;     &lt;span class="o"&gt;{&lt;/span&gt; &lt;span class="nb"&gt;printf&lt;/span&gt; &lt;span class="s1"&gt;'\n'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&amp;amp;2&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;All of the countdown, banners, and status messages write to stderr. Stdout is reserved exclusively for Claude's actual output. The watcher subshell is doubly-isolated:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="o"&gt;(&lt;/span&gt;
  &lt;span class="nb"&gt;exec&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt;/dev/null 2&amp;gt;/dev/null   &lt;span class="c"&gt;# silence everything in the watcher&lt;/span&gt;
  &lt;span class="c"&gt;# ... polling logic ...&lt;/span&gt;
&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; /dev/null 2&amp;gt;/dev/null &amp;amp;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The redirect appears both inside (on &lt;code&gt;exec&lt;/code&gt;) and outside (on the subshell itself) as belt-and-suspenders, because some shells inherit file descriptors through subshell boundaries in non-obvious ways.&lt;/p&gt;

&lt;h3&gt;
  
  
  Avoiding Zsh's &lt;code&gt;local&lt;/code&gt; Quirk at Script Top-Level
&lt;/h3&gt;

&lt;p&gt;In zsh, a bare &lt;code&gt;local varname&lt;/code&gt; declaration at the script's top level (outside any function) echoes the variable's current value on each evaluation. Inside a &lt;code&gt;while true&lt;/code&gt; loop, this means the variable gets printed to the terminal on every iteration.&lt;/p&gt;

&lt;p&gt;The fix is simple: wrap &lt;code&gt;main()&lt;/code&gt; in a function, so all &lt;code&gt;local&lt;/code&gt; declarations are properly function-scoped:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;main&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;resume_id&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;   &lt;span class="c"&gt;# safe — inside a function now&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="nb"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;do&lt;/span&gt;
    &lt;span class="c"&gt;# ...&lt;/span&gt;
  &lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
main &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works correctly in both bash and zsh. The CLAUDE.md rule reads: &lt;em&gt;"Wrapping in a function prevents that spurious output and works correctly in both bash and zsh."&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Idempotent Installer
&lt;/h2&gt;

&lt;p&gt;The installer (&lt;code&gt;install.sh&lt;/code&gt;) does six things: checks dependencies, detects the &lt;code&gt;claude&lt;/code&gt; binary path, copies scripts to &lt;code&gt;~/.claude/&lt;/code&gt;, patches the binary path into the wrapper, offers to add the shell alias, and registers the &lt;code&gt;statusLine&lt;/code&gt; hook in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Every step is idempotent — running the installer twice is safe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Binary detection rejects the wrapper itself (by comparing paths) so it doesn't recurse&lt;/li&gt;
&lt;li&gt;Alias injection checks for the wrapper path before appending&lt;/li&gt;
&lt;li&gt;Settings patching reads the existing value and skips if it's already correct&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The installer also never runs &lt;code&gt;sudo&lt;/code&gt;. If a required package is missing, it prints the exact install command and exits — the user runs the command, then re-runs &lt;code&gt;./install.sh&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Test Suite
&lt;/h2&gt;

&lt;p&gt;The project has 43 unit tests in &lt;code&gt;src/test-smart-resume.zsh&lt;/code&gt;. Rather than running the scripts end-to-end (which would require a real Claude binary), the tests source just the function definitions from each script:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;source_functions&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="nb"&gt;local &lt;/span&gt;&lt;span class="nv"&gt;script&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
  &lt;span class="nv"&gt;CLAUDE_BIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"/bin/true"&lt;/span&gt;
  &lt;span class="nb"&gt;source&lt;/span&gt; &amp;lt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="s1"&gt;'/^resume_id=/{exit} {print}'&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$script&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt; 2&amp;gt;/dev/null
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;awk&lt;/code&gt; command stops reading at the line &lt;code&gt;resume_id=&lt;/code&gt; — the first line of the main execution loop. Everything before that is pure function definitions, which can be safely sourced and tested in isolation.&lt;/p&gt;

&lt;p&gt;Tests cover:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Session name extraction (&lt;code&gt;get_session_name&lt;/code&gt;) — including the &lt;code&gt;strings&lt;/code&gt;-vs-&lt;code&gt;grep&lt;/code&gt; correctness case&lt;/li&gt;
&lt;li&gt;Reset time parsing for all timestamp formats — time-only, date+time, full datetime&lt;/li&gt;
&lt;li&gt;The epoch rollover logic — verifying &lt;code&gt;+86400&lt;/code&gt; only applies to time-only strings&lt;/li&gt;
&lt;li&gt;JSONL line-baseline logic — confirming the post-resume loop never re-matches old entries&lt;/li&gt;
&lt;li&gt;Flag file parsing — both 5h and 7d paths, including tie-breaking when both are near limits&lt;/li&gt;
&lt;li&gt;WSL function equivalence vs the Linux baseline&lt;/li&gt;
&lt;li&gt;macOS &lt;code&gt;sed -E&lt;/code&gt; parser and &lt;code&gt;python3&lt;/code&gt; epoch parser&lt;/li&gt;
&lt;li&gt;Installer dependency checking with fake PATH stubs&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Tests 40–43 are Linux/WSL-only (require &lt;code&gt;/proc&lt;/code&gt;).&lt;/p&gt;




&lt;h2&gt;
  
  
  Installation
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/karthiknitt/smart_resume.git
&lt;span class="nb"&gt;cd &lt;/span&gt;smart_resume
./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The installer handles everything. After it finishes, the cloned repo directory is no longer needed — the scripts live in &lt;code&gt;~/.claude/&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Recommended: Run in tmux
&lt;/h3&gt;

&lt;p&gt;Because Smart Resume sleeps for potentially hours between a rate limit hit and the resume, the process needs to stay alive the whole time. Running inside a tmux session means you can detach, close your laptop, reconnect later, and find the session already resumed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;tmux new-session &lt;span class="nt"&gt;-s&lt;/span&gt; claude
claude                          &lt;span class="c"&gt;# wrapper takes over on RL hit&lt;/span&gt;
&lt;span class="c"&gt;# Ctrl-b d to detach; reattach with: tmux attach -t claude&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Platforms
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Platform&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Linux&lt;/td&gt;
&lt;td&gt;Available — v0.1+&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Windows (WSL)&lt;/td&gt;
&lt;td&gt;Available — v0.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;macOS&lt;/td&gt;
&lt;td&gt;Available — v0.3&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The WSL version is identical to Linux (WSL runs a full Linux kernel). The macOS version substitutes BSD-compatible tooling: &lt;code&gt;ls -t&lt;/code&gt; instead of &lt;code&gt;find -printf&lt;/code&gt;, &lt;code&gt;python3&lt;/code&gt; instead of GNU &lt;code&gt;date -d&lt;/code&gt;, &lt;code&gt;sed -E&lt;/code&gt; instead of &lt;code&gt;grep -oP&lt;/code&gt;, and &lt;code&gt;pgrep -P&lt;/code&gt; instead of &lt;code&gt;/proc/&amp;lt;pid&amp;gt;/children&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  Wrapping Up
&lt;/h2&gt;

&lt;p&gt;This project started as a frustration fix — I was losing hours to manual rate-limit management. It turned into a deeper dive into Unix process management, terminal I/O, and the quirks of writing portable shell code that runs correctly in both bash and zsh.&lt;/p&gt;

&lt;p&gt;The key lessons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The filesystem is a perfectly good IPC mechanism for low-frequency signaling.&lt;/strong&gt; The flag file approach is simpler and more debuggable than a pipe or socket would be.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;/proc/&amp;lt;pid&amp;gt;/task/&amp;lt;pid&amp;gt;/children&lt;/code&gt; is underrated.&lt;/strong&gt; It gives you a child PID cleanly without grepping the entire process table.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;stdout purity matters more than you think.&lt;/strong&gt; Adding &lt;code&gt;&amp;gt;&amp;amp;2&lt;/code&gt; to every diagnostic printf is tedious, but it's the only way to stay composable with pipes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shell portability bugs are usually subtle.&lt;/strong&gt; The zsh &lt;code&gt;local&lt;/code&gt;-at-top-level quirk, the &lt;code&gt;strings&lt;/code&gt;-vs-&lt;code&gt;grep&lt;/code&gt; difference, the epoch rollover logic — none of these are obvious from a quick read of the code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is MIT-licensed and open for contributions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/karthiknitt/smart_resume" rel="noopener noreferrer"&gt;karthiknitt/smart_resume&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Smart Resume is not affiliated with or endorsed by Anthropic. Claude Code is a product of Anthropic, PBC.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>shell</category>
      <category>productivity</category>
    </item>
  </channel>
</rss>
