◉CLAUDE LABJP
●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…●2.1.310 — Monitor tool now works on Bedrock, Vertex and Foundry, and gateway failures are fixed (10/23)●2.1.307 — Fixes MCP failing to reconnect after sleep or a keychain lock (10/21). 2.1.306 also fixed the false claude -p login message●SONNET 4.5 — Retires on the Claude API on 11/30, 34 days left. Migrate to Sonnet 5.5●WINDOWS — Reports say hooks break silently only on Japanese-locale Windows; check the character encoding first●API — In API version 2026-10-21, passing temperature, top_p or top_k returns a 400 error●NEW — I Put a Ten-Line Promise File at the To…
Articles/Claude Code
⟐ Claude Code/2026-10-27Intermediate

Counting How Often Claude Code Ships, Straight from npm's Publish History

How many Claude Code versions ship per week, and how many hours apart? A 37-line Python script reads npm's publish history to count weekly releases and the median gap, including the empty-week trap and why a Tuesday run makes this week look quiet.

Claude Code275npm3release cadencePython26versioning

Before I wrote "Claude Code ships five or six versions a week" into my update notes, I decided to count once. A number written from feel has a way of quietly bending later decisions.

As an indie developer who uses Claude Code between app releases, I treat its update rhythm as a planning number. So I wrote a small script that counts the publish history. All it needs is python3 and a network path to the npm registry. Below are the steps in order, and the two traps I hit when I actually ran it.

Today's numbers first

The thing I most want to pass on: the median and the maximum gap describe the cadence better than an average does. This is what the script printed at midday today (a run at 03:xx UTC).

ISO weekReleases
2026-W394
2026-W406
2026-W417
2026-W425
2026-W437
2026-W44 (as of Tuesday)1

Across the last 60 gaps, the median was 23.2 hours and the maximum was 103.4 hours. In plain terms: about one release a day, and even the longest pause was a little over four days. My "five or six a week" guess sits inside the 5 to 7 range of the complete weeks W40 through W43.

Step 1: pull the publish times from the registry

The npm registry returns JSON when you request the package path. Its time key holds one timestamp per version.

def fetch(pkg):
    url = f"https://registry.npmjs.org/{pkg}"
    with urllib.request.urlopen(url, timeout=30) as r:
        return json.load(r)

Scoped names such as @anthropic-ai/claude-code work as-is in the URL. For a package with over 500 versions the document is large — 1,564,015 bytes in today's fetch — so I would not poll it every minute.

Step 2: drop what you do not want to count

time mixes two non-version keys, created and modified, in with the versions. Prereleases such as 2.2.0-beta.1 appear too. I filter out both, then sort by timestamp.

def stable_releases(doc):
    out = []
    for ver, stamp in doc["time"].items():
        if ver in ("created", "modified") or "-" in ver:
            continue
        out.append((ver, datetime.fromisoformat(stamp.replace("Z", "+00:00"))))
    return sorted(out, key=lambda x: x[1])

The word stable in that name means "no prereleases", not the stable channel. It is confusing, so rename it when you reuse the code. For counting how far the stable channel itself lags, I wrote a separate piece on the stable channel gap.

Step 3: count by week, take the median gap

I bucket by ISO week (%G-W%V). Mixing %Y with %W can misplace days around New Year.

per_week = Counter(t.strftime("%G-W%V") for _, t in rel)
gaps = [(b[1] - a[1]).total_seconds() / 3600 for a, b in zip(rel, rel[1:])][-RECENT:]
print(f"median {statistics.median(gaps):.1f} h / max {max(gaps):.1f} h")

zip(rel, rel[1:]) pairs each release with the next one, and the last RECENT gaps are kept. The median beats the mean here because one long pause over a weekend drags a mean upward.

The whole script

Here is everything in 37 lines. Run python3 cadence.py for the default package, or add arguments, as in python3 cadence.py lodash 20.

#!/usr/bin/env python3
"""Count weekly publishes and the median/max gap between publishes from npm history."""
import json, sys, statistics, urllib.request
from collections import Counter
from datetime import datetime, timedelta, timezone
 
PKG = sys.argv[1] if len(sys.argv) > 1 else "@anthropic-ai/claude-code"
RECENT = int(sys.argv[2]) if len(sys.argv) > 2 else 60
 
def fetch(pkg):
    url = f"https://registry.npmjs.org/{pkg}"
    with urllib.request.urlopen(url, timeout=30) as r:
        return json.load(r)
 
def stable_releases(doc):
    out = []
    for ver, stamp in doc["time"].items():
        if ver in ("created", "modified") or "-" in ver:  # skip prereleases (x.y.z-beta)
            continue
        out.append((ver, datetime.fromisoformat(stamp.replace("Z", "+00:00"))))
    return sorted(out, key=lambda x: x[1])
 
def main():
    rel = stable_releases(fetch(PKG))
    per_week = Counter(t.strftime("%G-W%V") for _, t in rel)
    print(f"package={PKG} total={len(rel)}")
    now = datetime.now(timezone.utc)
    for back in range(5, -1, -1):  # walk back week by week so empty weeks are not dropped
        week = (now - timedelta(weeks=back)).strftime("%G-W%V")
        print(f"  {week}: {per_week.get(week, 0)} releases")
    gaps = [(b[1] - a[1]).total_seconds() / 3600 for a, b in zip(rel, rel[1:])][-RECENT:]
    print(f"last {len(gaps)} gaps: median {statistics.median(gaps):.1f} h / max {max(gaps):.1f} h")
    idle = (now - rel[-1][1]).total_seconds() / 3600
    print(f"latest {rel[-1][0]} was published {idle:.1f} h ago")
 
if __name__ == "__main__":
    main()

Two traps I hit while running it

Empty weeks vanish silently

My first version printed the last six keys of the Counter. It looked fine for Claude Code, which publishes every week. Then I tried lodash, and weeks from 2019 and 2020 showed up as "recent". A week with zero releases has no key in a Counter, so it disappears the moment you list the keys.

The fix was to walk back from today one week at a time and read each count with per_week.get(week, 0). With that version, lodash prints six weeks of zeros, and you can also see that its last publish (4.18.1) was about 5,000 hours ago.

A Tuesday run makes this week look like one release

The second trap is about reading the output. W44 shows 1 not because publishing slowed, but because the week was barely a day old. The latest publish was in the evening of October 26 (UTC), 7.4 hours before the run.

My rule now: compare only complete weeks (W39 to W43) and treat the week of the run separately. Since adding that rule, I no longer jump to "this week is slow."

One thing to try next

Save cadence.py and run it once with no arguments. Checking whether the median matches your sense that "a new version appears daily" is enough to set a sensible rhythm for following updates.

For my own indie-development tools, I plan to decide between updating right after a release or waiting a few days by this median rather than by feel. Once you have counted, you have a reason for the number of days you wait.

Share

Thank You for Reading

Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⟐ Claude Code2026-10-15
The Hook That Was Meant to Guard My Japanese Folder Never Blocked Anything — Python Was Reading stdin as cp932
A PreToolUse hook guarding folders like 経理 or 議事録 silently lets edits through when Python's stdin defaults to cp932. How I reproduced it, the three-line fix, and one command to check your own hooks.
⟐ Claude Code2026-09-29
Two Default-Model Changes Haven't Reached Claude Code's Stable Channel Yet
This morning, Claude Code's stable channel sat 6 releases and 10 days behind latest. A small standard-library Python script that counts the gap in real, published releases instead of version arithmetic, and lists what hasn't arrived yet with a headline for each.
⟐ Claude Code2026-10-24
The Settings File I Kept Saying Yes To Had a Rule That Let Any Python Through — Auditing Allow Rules After the 2.1.309 Changelog
Claude Code 2.1.309 is full of permission-check and sandbox fixes. Here is a script that audits the allow rules in settings.local.json, and how to fix a prefix wildcard on an interpreter.