
Star History Chart
- 29.9k repo stars
- Updated July 27, 2026
- davila7/claude-code-templates
A Claude Code skill that adds a self-hosted, auto-refreshing 'stargazers over time' SVG chart to a GitHub README, replacing third-party star-history services that now fail with 'Requires authentication'.
About
star-history-chart is a Claude Code skill from davila7/claude-code-templates that puts a 'Stargazers over time' chart in a GitHub README without relying on any third-party chart service. GitHub now restricts the /stargazers endpoint to a repo's own admins and collaborators, which broke star-history.com's free tier, starchart.cc and every similar live-image service with a 'Requires authentication' error. The skill's fix is structural: a bundled Python script fetches stargazer timestamps with an authenticated token and renders a clean, light/dark-adaptive SVG committed to the repo, and a bundled GitHub Actions workflow regenerates docs/star-history.svg on a weekly cron (plus manual trigger) using the repo's built-in GITHUB_TOKEN - so there are no secrets to configure and nothing external to break. Point the README at the local SVG once and the chart stays live forever.
- Self-hosted SVG chart - no star-history.com or starchart.cc dependency left to break
- Weekly GitHub Action refresh using the repo's own GITHUB_TOKEN, zero secrets to configure
- Light/dark theme-aware SVG rendered from real stargazer timestamps
- Fixes the 'Requires authentication' breakage after GitHub restricted the stargazers endpoint
- Bundled Python generator script plus ready-made workflow YAML
Star History Chart by the numbers
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davila7/claude-code-templates --skill star-history-chartAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| repo stars | ★ 29.9k |
|---|---|
| Last updated | July 27, 2026 |
| Repository | davila7/claude-code-templates ↗ |
What it does
Fix a broken star-history chart in a README by generating a self-hosted SVG that a weekly GitHub Action refreshes with the repo's own GITHUB_TOKEN.
Who is it for?
Open-source maintainers who want a star-history chart in their README that keeps rendering forever, without depending on star-history.com or starchart.cc.
What you get
A theme-aware docs/star-history.svg committed in-repo and refreshed weekly by a GitHub Action using the repo's own GITHUB_TOKEN - no secrets, no external service.
- scripts/generate_star_history.py
- .github/workflows/star-history.yml weekly cron
- README section pointing at docs/star-history.svg
Files
Star History Chart (self-hosted, never breaks)
Add a "Stargazers over time" chart that renders from a static SVG committed to the repo and refreshes itself weekly — no external chart service, no broken images.
Why this exists
GitHub now restricts the /stargazers endpoint to a repository's own admins and collaborators. Unauthenticated requests return {"message":"Requires authentication"}, which breaks every third-party live-chart service (star-history.com free tier, starchart.cc, etc.) for all repos. The only reliable fix is to generate the chart yourself with an authenticated token and commit a static image. Inside GitHub Actions, the repo's own GITHUB_TOKEN can read its own stargazers, so the whole thing runs with zero secrets to configure.
What this skill sets up
1. scripts/generate_star_history.py — fetches stargazers (authenticated), renders a clean, light/dark-adaptive SVG. 2. .github/workflows/star-history.yml — weekly cron + manual trigger that regenerates and commits docs/star-history.svg. 3. A README section pointing at the local SVG.
Workflow
Step 1: Copy the script and workflow into the repo
mkdir -p scripts .github/workflows docs
cp skills/git/star-history-chart/scripts/generate_star_history.py scripts/generate_star_history.py
cp skills/git/star-history-chart/assets/star-history.yml .github/workflows/star-history.ymlThe script needs therequestspackage:pip install requests.
It resolves the repo fromSTAR_HISTORY_REPO, thenGITHUB_REPOSITORY
(set automatically in Actions), then the origin git remote — so no editsare required for it to work in a different repo.
Step 2: Generate the SVG once, locally
Use a token that can read the repo's stargazers (as owner/collaborator). The GitHub CLI provides one:
GITHUB_TOKEN=$(gh auth token) python scripts/generate_star_history.pyThis writes docs/star-history.svg. For a repo with many thousands of stars the first run paginates the whole stargazer list and can take a couple of minutes.
Verify it rendered (optional, macOS): qlmanage -t -s 800 -o . docs/star-history.svg
Step 3: Add it to the README
Add or replace the star chart section. Point the image at the local SVG. Set the link target to wherever you want clicks to go (the repo, a docs page, or your own site):
## Stargazers over time
[](https://github.com/OWNER/REPO/stargazers)If replacing a broken star-history.com / starchart.cc embed, swap only the image URL to docs/star-history.svg and keep or update the link target.
Step 4: Commit
git add scripts/generate_star_history.py .github/workflows/star-history.yml docs/star-history.svg README.md
git commit -m "feat(readme): self-hosted stargazers chart with weekly auto-refresh"
git pushStep 5: (Optional) Trigger the auto-refresh now
The workflow runs every Monday at 04:00 UTC. To refresh immediately without waiting: GitHub → Actions → "Update Star History" → Run workflow.
Customization
- Output path — set
STAR_HISTORY_OUTPUT(defaultdocs/star-history.svg). - Different repo — set
STAR_HISTORY_REPO=owner/name. - Colors / size — edit the
.line,.area,.dotCSS andWIDTH/HEIGHT
constants near the top of generate_star_history.py. The chart is theme-aware via a prefers-color-scheme: dark block, so it looks right in both GitHub light and dark modes.
- Refresh cadence — edit the
cronexpression in the workflow.
Notes
- No secrets to add: the workflow uses the automatic
GITHUB_TOKEN. - Private repos work too, as long as the token can read the repo.
- The script uses only
requestsplus the Python standard library.
name: Update Star History
on:
schedule:
# Every Monday at 4:00 AM UTC
- cron: '0 4 * * 1'
workflow_dispatch: # Allow manual runs from the GitHub UI
permissions:
contents: write
jobs:
update-star-history:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
cache: 'pip'
- name: Install Python dependencies
run: |
pip install --upgrade pip
pip install requests
- name: Generate star-history.svg
run: python scripts/generate_star_history.py
env:
# The repo's own GITHUB_TOKEN can read its own stargazers, which
# GitHub now restricts to the repository's admins and collaborators.
# GITHUB_REPOSITORY is set automatically by Actions.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Check for changes
id: check_changes
run: |
git diff --quiet docs/star-history.svg || echo "changed=true" >> $GITHUB_OUTPUT
- name: Commit and push changes
if: steps.check_changes.outputs.changed == 'true'
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
git add docs/star-history.svg
git commit -m "chore: Update star history chart
🤖 Generated with [GitHub Actions](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})"
git push
#!/usr/bin/env python3
"""Generate a self-hosted "Stargazers over time" SVG chart for any repo.
GitHub now restricts the stargazers endpoint to a repository's own admins and
collaborators, so third-party live-chart services (star-history free tier,
starchart.cc, ...) return "Requires authentication" for everyone. This script
fetches the star data with an authenticated token (in CI, the repo's own
GITHUB_TOKEN works because it can read the repo's own stargazers) and renders a
static, theme-aware SVG committed to the repository.
Repo resolution order:
1. STAR_HISTORY_REPO env var ("owner/name")
2. GITHUB_REPOSITORY env var (set automatically inside GitHub Actions)
3. `git remote get-url origin` parsed into "owner/name"
Usage:
GITHUB_TOKEN=xxx python scripts/generate_star_history.py
# local test:
GITHUB_TOKEN=$(gh auth token) python scripts/generate_star_history.py
"""
import os
import re
import subprocess
import sys
from datetime import datetime, timezone
import requests
OUTPUT = os.environ.get("STAR_HISTORY_OUTPUT", "docs/star-history.svg")
PER_PAGE = 100
MAX_PAGES = 400 # GitHub caps stargazers pagination at 400 pages (40k stars)
WIDTH, HEIGHT = 800, 400
PAD_L, PAD_R, PAD_T, PAD_B = 70, 55, 50, 55
def resolve_repo():
"""Return "owner/name" for the repository to chart."""
repo = os.environ.get("STAR_HISTORY_REPO") or os.environ.get("GITHUB_REPOSITORY")
if repo:
return repo.strip()
try:
url = subprocess.check_output(
["git", "remote", "get-url", "origin"], text=True
).strip()
except (subprocess.CalledProcessError, FileNotFoundError):
raise SystemExit(
"Could not determine the repo. Set STAR_HISTORY_REPO=owner/name."
)
# git@github.com:owner/name.git or https://github.com/owner/name.git
match = re.search(r"github\.com[:/]([^/]+/[^/]+?)(?:\.git)?/?$", url)
if not match:
raise SystemExit(f"Unrecognized GitHub remote URL: {url}")
return match.group(1)
def fetch_stargazers(token, repo):
"""Return a sorted list of datetime objects, one per star."""
session = requests.Session()
session.headers.update({
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github.star+json",
"X-GitHub-Api-Version": "2022-11-28",
"User-Agent": "star-history-chart-skill",
})
dates = []
for page in range(1, MAX_PAGES + 1):
resp = session.get(
f"https://api.github.com/repos/{repo}/stargazers",
params={"per_page": PER_PAGE, "page": page},
timeout=30,
)
if resp.status_code != 200:
raise SystemExit(
f"GitHub API error {resp.status_code} on page {page}: {resp.text[:200]}"
)
batch = resp.json()
if not batch:
break
for entry in batch:
starred_at = entry.get("starred_at")
if starred_at:
dates.append(
datetime.strptime(starred_at, "%Y-%m-%dT%H:%M:%SZ").replace(
tzinfo=timezone.utc
)
)
if len(batch) < PER_PAGE:
break
dates.sort()
return dates
def build_series(dates, max_points=100):
"""Cumulative (timestamp, count) points, downsampled to max_points."""
total = len(dates)
if total == 0:
return []
points = [(d, i + 1) for i, d in enumerate(dates)]
if total <= max_points:
return points
step = total / max_points
sampled = [points[int(i * step)] for i in range(max_points)]
sampled.append(points[-1]) # always include the latest point
return sampled
def esc(text):
return (
str(text)
.replace("&", "&")
.replace("<", "<")
.replace(">", ">")
)
def render_svg(series, repo):
if not series:
raise SystemExit("No stargazer data to render.")
t_min = series[0][0].timestamp()
t_max = series[-1][0].timestamp()
c_max = series[-1][1]
t_span = max(t_max - t_min, 1)
plot_w = WIDTH - PAD_L - PAD_R
plot_h = HEIGHT - PAD_T - PAD_B
def x(ts):
return PAD_L + (ts - t_min) / t_span * plot_w
def y(count):
return PAD_T + plot_h - (count / c_max) * plot_h
pts = [(x(ts.timestamp()), y(c)) for ts, c in series]
line = " ".join(f"{px:.1f},{py:.1f}" for px, py in pts)
area = (
f"{PAD_L:.1f},{PAD_T + plot_h:.1f} "
+ line
+ f" {pts[-1][0]:.1f},{PAD_T + plot_h:.1f}"
)
# Y axis ticks (5 gridlines)
y_ticks = []
for i in range(5):
val = round(c_max * i / 4)
y_ticks.append((y(val), val))
# X axis ticks (dates)
x_ticks = []
for i in range(5):
ts = t_min + t_span * i / 4
label = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%b %Y")
x_ticks.append((x(ts), label))
grid_lines = ""
for gy, val in y_ticks:
grid_lines += (
f'<line x1="{PAD_L}" y1="{gy:.1f}" x2="{WIDTH - PAD_R}" y2="{gy:.1f}" '
f'class="grid"/>\n'
f'<text x="{PAD_L - 10}" y="{gy + 4:.1f}" text-anchor="end" '
f'class="tick">{val:,}</text>\n'
)
x_labels = ""
for gx, label in x_ticks:
x_labels += (
f'<text x="{gx:.1f}" y="{HEIGHT - PAD_B + 22}" text-anchor="middle" '
f'class="tick">{esc(label)}</text>\n'
)
updated = datetime.now(tz=timezone.utc).strftime("%Y-%m-%d")
return f'''<svg xmlns="http://www.w3.org/2000/svg" width="{WIDTH}" height="{HEIGHT}" viewBox="0 0 {WIDTH} {HEIGHT}" font-family="-apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif">
<style>
.bg {{ fill: #ffffff; }}
.title {{ fill: #1f2328; font-size: 17px; font-weight: 600; }}
.subtitle {{ fill: #656d76; font-size: 11px; }}
.grid {{ stroke: #d0d7de; stroke-width: 1; stroke-dasharray: 3 3; }}
.axis {{ stroke: #656d76; stroke-width: 1.5; }}
.tick {{ fill: #656d76; font-size: 11px; }}
.area {{ fill: #ffd33d; opacity: 0.18; }}
.line {{ stroke: #f0b400; stroke-width: 2.5; fill: none; stroke-linejoin: round; stroke-linecap: round; }}
.dot {{ fill: #f0b400; }}
@media (prefers-color-scheme: dark) {{
.bg {{ fill: #0d1117; }}
.title {{ fill: #e6edf3; }}
.subtitle {{ fill: #8b949e; }}
.grid {{ stroke: #30363d; }}
.axis {{ stroke: #8b949e; }}
.tick {{ fill: #8b949e; }}
}}
</style>
<rect class="bg" width="{WIDTH}" height="{HEIGHT}" rx="6"/>
<text x="{PAD_L}" y="26" class="title">Stargazers over time</text>
<text x="{PAD_L}" y="42" class="subtitle">{esc(repo)} · {c_max:,} stars · updated {updated}</text>
{grid_lines}
<line x1="{PAD_L}" y1="{PAD_T}" x2="{PAD_L}" y2="{PAD_T + plot_h}" class="axis"/>
<line x1="{PAD_L}" y1="{PAD_T + plot_h}" x2="{WIDTH - PAD_R}" y2="{PAD_T + plot_h}" class="axis"/>
<polygon class="area" points="{area}"/>
<polyline class="line" points="{line}"/>
<circle class="dot" cx="{pts[-1][0]:.1f}" cy="{pts[-1][1]:.1f}" r="4"/>
{x_labels}
</svg>
'''
def main():
token = os.environ.get("GITHUB_TOKEN")
if not token:
raise SystemExit("GITHUB_TOKEN is required (repo read access to stargazers).")
repo = resolve_repo()
print(f"Fetching stargazers for {repo} ...", file=sys.stderr)
dates = fetch_stargazers(token, repo)
print(f"Got {len(dates)} stars.", file=sys.stderr)
series = build_series(dates)
svg = render_svg(series, repo)
out_dir = os.path.dirname(OUTPUT)
if out_dir:
os.makedirs(out_dir, exist_ok=True)
with open(OUTPUT, "w", encoding="utf-8") as fh:
fh.write(svg)
print(f"Wrote {OUTPUT} ({len(svg)} bytes).", file=sys.stderr)
if __name__ == "__main__":
main()
Related skills
FAQ
Why did my README star-history chart stop working?
GitHub now limits the /stargazers endpoint to a repo's own admins and collaborators, so unauthenticated third-party chart services return 'Requires authentication' for every repo.
Do I need to configure any secrets?
No. The chart regenerates inside GitHub Actions, where the repo's built-in GITHUB_TOKEN can read its own stargazers - nothing to set up beyond committing the workflow.
How often does the chart update?
A weekly cron in the bundled GitHub Action regenerates and commits docs/star-history.svg; you can also trigger it manually via workflow_dispatch.