<?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>Forem: Genie</title>
    <description>The latest articles on Forem by Genie (@geniej).</description>
    <link>https://forem.com/geniej</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%2F3880925%2F00a25c7a-5f71-494c-b80e-66d3cd3975e2.jpeg</url>
      <title>Forem: Genie</title>
      <link>https://forem.com/geniej</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/geniej"/>
    <language>en</language>
    <item>
      <title>ctxwatch — I Built the Missing Context-Saturation Daemon for Claude Code in 4 Hours</title>
      <dc:creator>Genie</dc:creator>
      <pubDate>Thu, 16 Apr 2026 16:44:13 +0000</pubDate>
      <link>https://forem.com/geniej/ctxwatch-i-built-the-missing-context-saturation-daemon-for-claude-code-in-4-hours-4l5i</link>
      <guid>https://forem.com/geniej/ctxwatch-i-built-the-missing-context-saturation-daemon-for-claude-code-in-4-hours-4l5i</guid>
      <description>&lt;p&gt;Six tools measure whether your Claude Code &lt;strong&gt;wallet&lt;/strong&gt; is empty. Zero measure whether your &lt;strong&gt;brain&lt;/strong&gt; is full. This is the story of the 600-line Python daemon that fixes that gap, built in one afternoon from a GitHub issue that had been open for exactly 24 hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Alarm vs. The Smoke Detector
&lt;/h2&gt;

&lt;p&gt;Yesterday a user named rmcoppersmith opened &lt;a href="https://github.com/anthropics/claude-code/issues/49226" rel="noopener noreferrer"&gt;anthropics/claude-code#49226&lt;/a&gt;. The framing is so sharp it's almost marketing copy:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;PreCompact hook fires when compaction happens, but it's &lt;strong&gt;too late for thoughtful memory writing&lt;/strong&gt; (the alarm, not the smoke detector). Tool call counting is a crude proxy that doesn't account for varying response sizes. Manual &lt;code&gt;/context&lt;/code&gt; command is &lt;strong&gt;not machine-parseable&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Three complaints, one thesis: &lt;strong&gt;we need a continuous signal, not a terminal alarm.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;If you use Claude Code for hours at a time, you know this pain. You think everything is fine. You lose yourself in the flow. Then compaction fires — and suddenly the model has forgotten half the session. The hook you wrote to save important context? It ran, yes. But it ran after the fire, not when the smoke started.&lt;/p&gt;

&lt;p&gt;Every existing monitor on the Claude Code side measures cost or quota. &lt;code&gt;ccusage&lt;/code&gt;, &lt;code&gt;claude-monitor&lt;/code&gt;, the six-or-so others I found while scanning this morning — they all answer the question "is my wallet empty?" None answer "is my brain full?"&lt;/p&gt;

&lt;p&gt;Those are completely different axes.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Actually in the Transcript
&lt;/h2&gt;

&lt;p&gt;Before building, I wanted to know what signal was already sitting on disk. If you peek at &lt;code&gt;~/.claude/projects/&amp;lt;project&amp;gt;/&amp;lt;session&amp;gt;.jsonl&lt;/code&gt;, each assistant turn has a &lt;code&gt;usage&lt;/code&gt; block like this:&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;"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;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"timestamp"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-04-17T00:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&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;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_read_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_creation_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;50000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;500&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;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 sum of those four fields is approximately the number of tokens that were visible to the model on that turn. That's your current saturation. The window size comes from the model name — 200K default, 1M for 1M-context subscribers.&lt;/p&gt;

&lt;p&gt;There's a subtle trap here: &lt;strong&gt;Claude Code transcripts don't record the &lt;code&gt;[1m]&lt;/code&gt; suffix even for 1M users.&lt;/strong&gt; The model field just says &lt;code&gt;claude-opus-4-7&lt;/code&gt;. So if you naively treat that as 200K, a 1M subscriber with 200K used shows as 100% — a nonsense reading. I'll come back to how I handled this.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tool
&lt;/h2&gt;

&lt;p&gt;I called it &lt;code&gt;ctxwatch&lt;/code&gt;. One Python file, stdlib only, six subcommands.&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="nv"&gt;$ &lt;/span&gt;ctxwatch once
transcript: c6af100b-a2e7-4f2f-ba1a-cd9b0503c71d.jsonl
&lt;span class="o"&gt;[&lt;/span&gt;████░░░░░░░░░░░░░░░░]  20.4%   204,214 / 1,000,000  —  &lt;span class="nv"&gt;turns&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;69  OK
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The daemon mode (&lt;code&gt;ctxwatch watch&lt;/code&gt;) tails the most recent transcript and prints a new bar each time the assistant responds. If you prefer JSON for statuslines:&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="nv"&gt;$ &lt;/span&gt;ctxwatch json
&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;"ts"&lt;/span&gt;:&lt;span class="s2"&gt;"2026-04-17T00:41:05Z"&lt;/span&gt;,&lt;span class="s2"&gt;"tokens"&lt;/span&gt;:204214,&lt;span class="s2"&gt;"window"&lt;/span&gt;:1000000,&lt;span class="s2"&gt;"pct"&lt;/span&gt;:0.2042,&lt;span class="s2"&gt;"turns"&lt;/span&gt;:69,&lt;span class="s2"&gt;"model"&lt;/span&gt;:&lt;span class="s2"&gt;"claude-opus-4-7"&lt;/span&gt;,...&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And the piece rmcoppersmith explicitly asked for — a &lt;strong&gt;Stop hook&lt;/strong&gt; that fires at a threshold of your choosing:&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="nv"&gt;$ &lt;/span&gt;ctxwatch hook &lt;span class="nt"&gt;--threshold&lt;/span&gt; 0.50 &lt;span class="nt"&gt;--on-exceed&lt;/span&gt; &lt;span class="s1"&gt;'your-memory-write-script.sh'&lt;/span&gt;
&lt;span class="o"&gt;{&lt;/span&gt;
  &lt;span class="s2"&gt;"hooks"&lt;/span&gt;: &lt;span class="o"&gt;{&lt;/span&gt;
    &lt;span class="s2"&gt;"Stop"&lt;/span&gt;: &lt;span class="o"&gt;[{&lt;/span&gt;
      &lt;span class="s2"&gt;"matcher"&lt;/span&gt;: &lt;span class="s2"&gt;""&lt;/span&gt;,
      &lt;span class="s2"&gt;"hooks"&lt;/span&gt;: &lt;span class="o"&gt;[{&lt;/span&gt;
        &lt;span class="s2"&gt;"type"&lt;/span&gt;: &lt;span class="s2"&gt;"command"&lt;/span&gt;,
        &lt;span class="s2"&gt;"command"&lt;/span&gt;: &lt;span class="s2"&gt;"ctxwatch check --threshold=0.5 &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;--on-exceed=your-memory-write-script.sh&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
      &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="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Drop that block into &lt;code&gt;~/.claude/settings.json&lt;/code&gt;, merge with your existing hooks, done. The &lt;code&gt;check&lt;/code&gt; subcommand does the saturation math and exits with code 1 (plus runs your on-exceed command) when you cross the threshold. No escaping gymnastics, no embedded Python one-liners.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two Design Decisions Worth Sharing
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Auto-detect 1M users instead of asking them
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;[1m]&lt;/code&gt; suffix problem is a calibration landmine. The clean fix would be to make users pass &lt;code&gt;--window 1m&lt;/code&gt; every time. The nice fix is to detect it.&lt;/p&gt;

&lt;p&gt;I noticed something obvious: if I see a single turn in the transcript with more than 200K total tokens, the user is mathematically guaranteed to be on a 1M window. 200K can't fit in 200K. So I added a small pre-scan: if observed max exceeds the default, bump to 1M and tag the source as &lt;code&gt;auto:observed&amp;gt;200K&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;resolve_window&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;override&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;observed_max&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;override&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;override&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;override&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[1m]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[1m] suffix&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="n"&gt;base&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;]&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;MODEL_WINDOWS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_WINDOW&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;observed_max&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_WINDOW&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;auto:&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s"&gt;+observed&amp;gt;200K&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;DEFAULT_WINDOW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;default&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Manual override still works (&lt;code&gt;--window 1m&lt;/code&gt;, &lt;code&gt;--window 200k&lt;/code&gt;, or raw tokens). But 95% of users never touch it.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. &lt;code&gt;parse_usage&lt;/code&gt; never raises
&lt;/h3&gt;

&lt;p&gt;A single corrupted JSONL line — partial write, disk full, schema drift — used to kill my whole scan. My code-review caught it late in the day: the &lt;code&gt;int()&lt;/code&gt; calls were outside the try/except. A non-numeric &lt;code&gt;input_tokens&lt;/code&gt; field (improbable but not impossible) would propagate &lt;code&gt;ValueError&lt;/code&gt; through every code path.&lt;/p&gt;

&lt;p&gt;Fix: wrap everything, return &lt;code&gt;None&lt;/code&gt; on any failure.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;parse_usage&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;assistant&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
        &lt;span class="c1"&gt;# ... int coercion, field access, etc ...
&lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;TurnUsage&lt;/span&gt;&lt;span class="p"&gt;(...)&lt;/span&gt;
    &lt;span class="nf"&gt;except &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JSONDecodeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;AttributeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Small change. Would have bitten me within days of real-world use.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's Next (v0.2)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Multi-project dashboard&lt;/strong&gt; — aggregate across all your Claude Code projects&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Hook template library&lt;/strong&gt; — more patterns than just Stop; common memory-write recipes&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Historical trend&lt;/strong&gt; — "you crossed 80% saturation 12 times this week"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For now: v0.1 ships, today. If you write hooks, build agent memory, or just want to know how close your next session is to compaction — try it and tell me where it's wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Repo&lt;/strong&gt;: &lt;a href="https://github.com/Genie-J/ctxwatch" rel="noopener noreferrer"&gt;https://github.com/Genie-J/ctxwatch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue that inspired this&lt;/strong&gt;: &lt;a href="https://github.com/anthropics/claude-code/issues/49226" rel="noopener noreferrer"&gt;anthropics/claude-code#49226&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sibling project&lt;/strong&gt;: &lt;a href="https://github.com/Genie-J/cc-healthcheck" rel="noopener noreferrer"&gt;cc-healthcheck&lt;/a&gt; — static snapshot of what's &lt;em&gt;in&lt;/em&gt; your context right now&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Install:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pipx &lt;span class="nb"&gt;install &lt;/span&gt;git+https://github.com/Genie-J/ctxwatch
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or clone and run — it's one file. MIT.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built as part of &lt;a href="https://github.com/Genie-J" rel="noopener noreferrer"&gt;OPC Team&lt;/a&gt;, a self-directed experiment in solo-dev AI infrastructure. Calibration feedback via GitHub issues is the primary signal I'm watching for.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>claudecode</category>
      <category>ai</category>
      <category>python</category>
      <category>cli</category>
    </item>
    <item>
      <title>What's eating your Claude Code context window? I wrote a 500-line Python script to find out</title>
      <dc:creator>Genie</dc:creator>
      <pubDate>Thu, 16 Apr 2026 03:16:29 +0000</pubDate>
      <link>https://forem.com/geniej/whats-eating-your-claude-code-context-window-i-wrote-a-500-line-python-script-to-find-out-3oma</link>
      <guid>https://forem.com/geniej/whats-eating-your-claude-code-context-window-i-wrote-a-500-line-python-script-to-find-out-3oma</guid>
      <description>&lt;p&gt;If you use Claude Code seriously — Max plan, 50+ skills, a &lt;code&gt;CLAUDE.md&lt;/code&gt; that's grown organically over months — you've probably hit this moment:&lt;/p&gt;

&lt;p&gt;You run &lt;code&gt;claude /context&lt;/code&gt; and it says your system prompt is sitting at &lt;strong&gt;14% of your context window before you've typed anything&lt;/strong&gt;. And &lt;code&gt;claude /cost&lt;/code&gt; tells you today's spend but doesn't say &lt;em&gt;what&lt;/em&gt; inside your setup is expensive.&lt;/p&gt;

&lt;p&gt;Tokens are real money. You can't optimize what you can't see. So I wrote &lt;a href="https://github.com/Genie-J/cc-healthcheck" rel="noopener noreferrer"&gt;&lt;code&gt;cc-healthcheck&lt;/code&gt;&lt;/a&gt; — a single Python file, zero dependencies, zero network, that reads &lt;code&gt;~/.claude/&lt;/code&gt; locally and answers three questions:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;What auto-loads into every session?&lt;/strong&gt; (&lt;code&gt;CLAUDE.md&lt;/code&gt; + every &lt;code&gt;@&lt;/code&gt;-reference + &lt;code&gt;rules/*.md&lt;/code&gt; + every skill frontmatter)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are my hooks broken?&lt;/strong&gt; (pipe-corruption bugs, missing timeouts, case-sensitivity traps)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Where did the last session's tokens actually go?&lt;/strong&gt; (per-model totals, cache hit ratio, system-reminder injection count)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Sample output on my own machine:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;━━━ cc-healthcheck v0.1.0 ━━━

[1] Auto-Load Budget
    CLAUDE.md chain:       12.0K  (420 lines across 4 file(s))
    rules/*.md (11 files):  7.9K
    skills frontmatter (76): 3.6K  (full bodies: 102.5K — loaded on invocation)
    ───────────────────────────────
    Total auto-loaded:     23.4K  (2.34% of 1M)
    Status: ✅ HEALTHY (soft limit: 100.0K, hard: 200.0K)

[2] Hooks (20 total across 6 events)
    Issues (5):
      ⚠️  [SessionStart] inline '|' without quoting — known Claude Code #1132 corruption risk
      ⚠️  [PreToolUse/Write] no timeout set — hook can hang indefinitely
      ...

[3] Latest Session X-Ray
    Size: 1.11 MB, 365 records (147 assistant turns)
    Cumulative API tokens: 29.4M  (cache_read 90.6% — cache working)
    ⚠️  system-reminder injections: 13 occurrences
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  How it actually works
&lt;/h2&gt;

&lt;p&gt;The code is ~500 lines of Python stdlib. No &lt;code&gt;tiktoken&lt;/code&gt;, no &lt;code&gt;requests&lt;/code&gt;, no external anything — just &lt;code&gt;json&lt;/code&gt;, &lt;code&gt;pathlib&lt;/code&gt;, &lt;code&gt;re&lt;/code&gt;, &lt;code&gt;argparse&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Counting tokens without tiktoken
&lt;/h3&gt;

&lt;p&gt;For a health-check tool, exact tokenization is overkill. I use &lt;code&gt;len(text) / 4.0&lt;/code&gt; as the standard English-prose approximation (OpenAI/Anthropic both document this ratio). For JSON/code it drifts to ~3.5, but order-of-magnitude is what matters when you're asking "is my &lt;code&gt;CLAUDE.md&lt;/code&gt; eating 3K or 30K?"&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;est_tokens&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you need exact numbers, pipe the &lt;code&gt;--json&lt;/code&gt; output into a real tokenizer. I'd rather keep the tool install-free than 10% more accurate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Following &lt;code&gt;@&lt;/code&gt; references
&lt;/h3&gt;

&lt;p&gt;Claude Code's &lt;code&gt;CLAUDE.md&lt;/code&gt; supports &lt;code&gt;@~/path/to/file.md&lt;/code&gt; at the start of a line to include other files in the auto-loaded context. To count the whole tree:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;AT_REF_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^@(~[^\s]+|[^\s]+)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MULTILINE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;root&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;seen&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exists&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;
    &lt;span class="n"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;read_text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;utf-8&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;replace&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# count tokens, record path
&lt;/span&gt;    &lt;span class="bp"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;AT_REF_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;finditer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolve_at_ref&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nf"&gt;include&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;via&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;@ from &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;seen&lt;/code&gt; set prevents infinite loops if two files &lt;code&gt;@&lt;/code&gt;-reference each other (I've seen this happen in real configs).&lt;/p&gt;

&lt;h3&gt;
  
  
  Linting hooks for known bugs
&lt;/h3&gt;

&lt;p&gt;Claude Code hooks are a JSON structure in &lt;code&gt;~/.claude/settings.json&lt;/code&gt;. Three recurring issues:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Inline &lt;code&gt;|&lt;/code&gt; without quoting&lt;/strong&gt; — tracked as &lt;a href="https://github.com/anthropics/claude-code/issues/1132" rel="noopener noreferrer"&gt;anthropics/claude-code#1132&lt;/a&gt;, marked "not planned" for fix. The command string gets split on &lt;code&gt;|&lt;/code&gt; before shell parses it, and your hook silently mangles.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No &lt;code&gt;timeout&lt;/code&gt; field&lt;/strong&gt; — hooks can hang indefinitely, freezing your Claude session.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lowercase matcher with capitalized tool name&lt;/strong&gt; — matchers are case-sensitive but docs are ambiguous. &lt;code&gt;"edit"&lt;/code&gt; won't match &lt;code&gt;Edit&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The linter flags all three:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;'"'&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="sh"&gt;"'"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;issues&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;severity&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;warn&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;msg&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;inline &lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;|&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt; without quoting — known #1132 corruption risk&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  X-raying the JSONL session
&lt;/h3&gt;

&lt;p&gt;Claude Code writes every session to &lt;code&gt;~/.claude/projects/&amp;lt;id&amp;gt;/&amp;lt;uuid&amp;gt;.jsonl&lt;/code&gt;. Each assistant turn has this shape:&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;"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;"assistant"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"isSidechain"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"message"&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;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"claude-opus-4-6"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"usage"&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;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;27&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_creation_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;59469&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"cache_read_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11530&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;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;Sum those fields across all &lt;code&gt;type === "assistant"&lt;/code&gt; records (including &lt;code&gt;isSidechain: true&lt;/code&gt; subagent calls, which is the bit that &lt;code&gt;/cost&lt;/code&gt; misses) and you have the real API spend for that session.&lt;/p&gt;

&lt;p&gt;A bonus finding: counting &lt;code&gt;&amp;lt;system-reminder&amp;gt;&lt;/code&gt; occurrences in the raw JSONL is a useful metric. On Claude Code 2.1.x, the skill-trigger list gets re-broadcast inside a system-reminder on many user turns. Those blocks are inside the cached prefix so you're only billed once per 5-minute cache window, but they still &lt;em&gt;count&lt;/em&gt; against the context window on every turn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why bother?
&lt;/h2&gt;

&lt;p&gt;Two recent open issues on the Claude Code repo describe the &lt;em&gt;same&lt;/em&gt; symptom:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/46339" rel="noopener noreferrer"&gt;#46339&lt;/a&gt; — "System prompt token consumption increased ~40-50% between v2.1.92 and v2.1.100 with zero changes to user configuration"&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://github.com/anthropics/claude-code/issues/46917" rel="noopener noreferrer"&gt;#46917&lt;/a&gt; — "v2.1.100 sends 978 fewer bytes than v2.1.98 but is billed 20,196 MORE tokens"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Both reporters had to set up HTTP proxies or manual diffs to investigate. &lt;code&gt;cc-healthcheck&lt;/code&gt; won't solve the server-side inflation (only Anthropic can), but it lets you &lt;strong&gt;separate the two pools&lt;/strong&gt;: is it your config that grew, or the platform? Without that, it's all vibes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;

&lt;p&gt;Zero-install — run straight from GitHub:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; https://raw.githubusercontent.com/Genie-J/cc-healthcheck/main/cc_healthcheck.py | python3 -
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or clone + run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Genie-J/cc-healthcheck
python3 cc-healthcheck/cc_healthcheck.py
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Flags:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;cc-healthcheck              &lt;span class="c"&gt;# text report&lt;/span&gt;
cc-healthcheck &lt;span class="nt"&gt;--json&lt;/span&gt;       &lt;span class="c"&gt;# JSON for CI&lt;/span&gt;
cc-healthcheck &lt;span class="nt"&gt;--verbose&lt;/span&gt;    &lt;span class="c"&gt;# per-file breakdown&lt;/span&gt;
cc-healthcheck &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Repo
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/Genie-J/cc-healthcheck" rel="noopener noreferrer"&gt;github.com/Genie-J/cc-healthcheck&lt;/a&gt; — MIT. Issues welcome, especially reconciliation cases where cc-healthcheck numbers don't match what &lt;code&gt;/cost&lt;/code&gt; or your Anthropic billing shows.&lt;/p&gt;

&lt;p&gt;If you like this flavor (small single-file local tools), I also wrote &lt;a href="https://github.com/Genie-J/burncheck" rel="noopener noreferrer"&gt;BurnCheck&lt;/a&gt; — same philosophy, different problem (predicting whether your weekly Opus cap is about to hit mid-task).&lt;/p&gt;

&lt;p&gt;Keep your context tight. Your wallet will thank you.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>claudecode</category>
      <category>python</category>
      <category>devtools</category>
    </item>
    <item>
      <title>How I built a privacy-first Claude Code burn-rate analyzer in a single HTML file</title>
      <dc:creator>Genie</dc:creator>
      <pubDate>Wed, 15 Apr 2026 17:00:55 +0000</pubDate>
      <link>https://forem.com/geniej/how-i-built-a-privacy-first-claude-code-burn-rate-analyzer-in-a-single-html-file-48o7</link>
      <guid>https://forem.com/geniej/how-i-built-a-privacy-first-claude-code-burn-rate-analyzer-in-a-single-html-file-48o7</guid>
      <description>&lt;p&gt;If you're a Claude Code power user on a Max $20 / $100 / $200 plan, you've probably hit a weekly limit mid-task at least once. Anthropic doesn't publish exact caps, the UI doesn't warn you beforehand, and existing tools just show current usage — they don't predict.&lt;/p&gt;

&lt;p&gt;So I built &lt;a href="https://genie-j.github.io/burncheck/" rel="noopener noreferrer"&gt;&lt;strong&gt;BurnCheck&lt;/strong&gt;&lt;/a&gt;: a single-page webapp that reads your &lt;code&gt;~/.claude/projects/&lt;/code&gt; folder locally and shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Projected weekly cost (based on last 7 days)&lt;/li&gt;
&lt;li&gt;Whether you'll cross Max $20 / $100 / $200 caps before Sunday&lt;/li&gt;
&lt;li&gt;Sessions at risk of hitting the 5-hour interrupt&lt;/li&gt;
&lt;li&gt;Concrete model-swap recommendations ("these 14 Opus calls could've run on Sonnet → save $23/week")&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Everything runs in your browser.&lt;/strong&gt; Zero upload, zero account, zero tracking. Open DevTools Network tab — there are no outbound requests after the page loads. The whole thing is one 27 KB HTML file on GitHub Pages.&lt;/p&gt;




&lt;h2&gt;
  
  
  The design constraint that shaped everything
&lt;/h2&gt;

&lt;p&gt;Claude Code logs contain every prompt you've ever written — source code, API keys occasionally (though you shouldn't, but people do), business ideas, private thoughts. Any tool that asks you to &lt;em&gt;upload&lt;/em&gt; those logs to analyze them is dead on arrival for the target audience (privacy-conscious devs). So:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Constraint:&lt;/strong&gt; nothing can leave the user's machine.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Consequence:&lt;/strong&gt; no backend, no serverless, no database, no analytics. Just HTML + JS using the browser's &lt;code&gt;File API&lt;/code&gt; to read local files on user-gesture click.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// That's it. The whole "upload" flow.&lt;/span&gt;
&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;folderInput&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;change&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Array&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;e&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.jsonl&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;records&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;files&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;line&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;line&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;assistant&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;?.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;ts&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;timestamp&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nf"&gt;render&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;analyze&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;records&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user picks their &lt;code&gt;~/.claude/projects/&lt;/code&gt; folder in the native file picker. Chrome's File API gives the page read access to every &lt;code&gt;.jsonl&lt;/code&gt; inside — and nothing more. No network, no &lt;code&gt;fetch()&lt;/code&gt;, no hidden beacon.&lt;/p&gt;




&lt;h2&gt;
  
  
  Cost calculation
&lt;/h2&gt;

&lt;p&gt;Each &lt;code&gt;type: assistant&lt;/code&gt; record in the JSONL has a &lt;code&gt;usage&lt;/code&gt; object:&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;"input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cache_creation_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;59469&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"cache_read_input_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;11530&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"output_tokens"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;27&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;Per-model pricing (community-published rates, ±30% uncertain since Anthropic doesn't publish a stable schedule):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;PRICING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;opus&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;   &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;75.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;sonnet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mf"&gt;3.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;15.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;haiku&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;in&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cw&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.10&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;costOf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;priceOf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;model&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;input_tokens&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
       &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;output_tokens&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;
       &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache_creation_input_tokens&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cw&lt;/span&gt;
       &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;u&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cache_read_input_tokens&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e6&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cr&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Surprising finding from my own logs:&lt;/strong&gt; 99% of my cost came from cache reads. Without cache prompt optimization, long sessions become very expensive very fast. A single multi-hour Claude Code session with a growing context can easily cross $50.&lt;/p&gt;




&lt;h2&gt;
  
  
  Weekly cap heuristic
&lt;/h2&gt;

&lt;p&gt;Anthropic's Max plans have opaque weekly caps enforced somewhere between "suggested" and "hard throttle." Community reports suggest:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Plan&lt;/th&gt;
&lt;th&gt;Approx weekly $-equivalent&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Max $20&lt;/td&gt;
&lt;td&gt;~$25&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max $100&lt;/td&gt;
&lt;td&gt;~$140&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Max $200&lt;/td&gt;
&lt;td&gt;~$320&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These aren't official and shift. BurnCheck lets users override them from their own throttle experience — the values persist in &lt;code&gt;localStorage&lt;/code&gt; so the forecast improves as users calibrate.&lt;/p&gt;

&lt;p&gt;At current burn rate × 7, we compute &lt;code&gt;ratio = projected_week / cap&lt;/code&gt;. If &amp;gt;100%: "OVER by X%". If 80–100%: "tight". If &amp;lt;80%: "comfortable". It's not a rocket science prediction — it's a trust-the-user-can-read-a-bar-chart move. The value isn't the math, it's &lt;strong&gt;surfacing information Anthropic's UI never shows you.&lt;/strong&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The shareable card (viral loop)
&lt;/h2&gt;

&lt;p&gt;The most fun part to build. A &lt;code&gt;&amp;lt;canvas&amp;gt;&lt;/code&gt; element that renders your weekly burn as a 1200×630 PNG (Twitter/X OG card dimensions):&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dlmtm37i07ve17hi9v9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F2dlmtm37i07ve17hi9v9.png" alt="BurnCheck share card — warm cream background with big hero $ number and BURNED THIS WEEK label in deep tangerine" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Users download + post it. Each share becomes free distribution. My alpha tester (my partner) noticed this instantly — "people love to flex how much they spent on AI this week" — and it turned out to be the most engagement-generating feature by a large margin.&lt;/p&gt;

&lt;p&gt;Canvas drawing is old-school pleasant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#FBF3E8&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// linen paper&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillRect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;630&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;font&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;700 260px -apple-system, sans-serif&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;fillStyle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#2B1810&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;  &lt;span class="c1"&gt;// warm near-black&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;textAlign&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;center&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;ctx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;fillText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`$&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;totalCost&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;600&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;360&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No html2canvas, no external libs. 50 lines of draw calls.&lt;/p&gt;




&lt;h2&gt;
  
  
  Seven design iterations in one day
&lt;/h2&gt;

&lt;p&gt;I wrote the first version in 2 hours. Then I spent the next 8 hours &lt;strong&gt;making it look worse&lt;/strong&gt;, three times, before landing back near v0.1.&lt;/p&gt;

&lt;p&gt;Why? Because I kept trying to ape Cal.com, Clay.com, and Linear.app's design systems instead of just shipping a clean utility. Each time I'd read their DESIGN.md-style regulations, take the DNA ("oat background", "hard-edge offset shadows", "multi-layer ring shadows"), and apply it mechanically — "use hard shadow at least 3 places." Result: cramped dashboards that visually screamed "I am pretending to be a marketing site."&lt;/p&gt;

&lt;p&gt;My alpha tester's feedback each round:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;v0.4 (Cal-inspired grayscale): "too cold"&lt;/li&gt;
&lt;li&gt;v0.5 (Clay-inspired warm + hard shadows): "ugly, zero breathing room"&lt;/li&gt;
&lt;li&gt;v0.6 (Linear-inspired breathing-first): "worse, not as good as the first version"&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Only v0.6 had the right diagnosis — subtract, don't add. But the right &lt;em&gt;aesthetic&lt;/em&gt; was v0.1 all along: orange accent + clean off-white background + system fonts + 1px borders. Boring. Functional. Invisible design.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: design system DNA is grammar, not recipe. Apply it selectively based on whether the product is marketing-site-shaped (Clay, Linear home page) or utility-dashboard-shaped (what BurnCheck actually is). Force-fitting a marketing aesthetic onto a dashboard produces cramped ugliness at worst, cargo-cult polish at best.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;npm CLI&lt;/strong&gt; — &lt;code&gt;npx github:Genie-J/burncheck&lt;/code&gt; runs the same analysis in your terminal, colored output, zero deps (shipped today)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pro tier&lt;/strong&gt; (planned) — auto-monitoring daemon, daily email alerts, hosted dashboard. $9/mo, founding users $5/mo for life. &lt;a href="https://github.com/Genie-J/burncheck" rel="noopener noreferrer"&gt;Watch the repo&lt;/a&gt; to be notified.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Leaderboard&lt;/strong&gt; — opt-in "biggest burn of the week" flex board. Privacy-preserving (only aggregated stats posted). Network effect around the share-card loop.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Web: &lt;a href="https://genie-j.github.io/burncheck/" rel="noopener noreferrer"&gt;https://genie-j.github.io/burncheck/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;CLI: &lt;code&gt;npx github:Genie-J/burncheck&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Source: &lt;a href="https://github.com/Genie-J/burncheck" rel="noopener noreferrer"&gt;https://github.com/Genie-J/burncheck&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built over a single day. MIT licensed. Not affiliated with Anthropic.&lt;/p&gt;

&lt;p&gt;If it saves you from a Thursday-afternoon limit cutoff, open an issue and tell me how. That's the only thanks the project needs.&lt;/p&gt;

</description>
      <category>claude</category>
      <category>claudecode</category>
      <category>ai</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
