From Claude Unwrapped
Generate "Claude Unwrapped" — a Spotify-Wrapped-style HTML recap of the user's Claude Code usage, built from their local ~/.claude data and written in Claude's own voice. Use when the user asks for their Claude usage recap, wrapped, unwrapped, or year-in-review — including period-scoped ("this month", "Q1") versions. Also use to update a single slide of an already-generated recap ("redo my persona slide", "fix the streak footnote") without rebuilding the whole deck.
How this skill is triggered — by the user, by Claude, or both
Slash command
/unwrapped:generate [optional: config dir, a period ('this month', 'Q1'), or 'update <slide>'][optional: config dir, a period ('this month', 'Q1'), or 'update <slide>']This skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
You are about to make the user a personalized, slightly cheeky recap of their life with Claude Code. Everything runs locally; no data leaves their machine.
You are about to make the user a personalized, slightly cheeky recap of their life with Claude Code. Everything runs locally; no data leaves their machine.
The deck is a horizontal scroll-snap recap: slides sit side by side, a pixel crab ("clawdbot") walks a progress bar along the bottom and strikes a per-slide pose, the cover bursts the crab out of a box then types itself in, and the final slide runs the crab to centre-stage and throws confetti. All of that motion lives in the template's CSS and <script> — your only job is to fill the {{PLACEHOLDER}}s with this user's data and copy. Do not touch the crab SVG, the data-mood attributes, or the script — they are wired and slide-order-aware already.
Two modes. A normal run builds the whole deck (Steps 1–5). If instead the user wants to fix or refresh one slide of a deck they already generated — "redo my persona slide", "change the streak footnote", "the head-to-head isn't funny" — and ./claude-unwrapped.html exists, skip the full pipeline and follow Update one slide at the end of this file.
Run the bundled analyzer (stdlib Python, no dependencies). Run it fresh on every invocation — never reuse a stats file from a previous run or numbers from earlier in the conversation; a recap with no period mentioned is always all-time, even if the last one was ranged.
python3 "${CLAUDE_SKILL_DIR}/scripts/analyze.py" > /tmp/claude-unwrapped-stats.json
If the user passed a config dir as an argument, pass it through: analyze.py <dir>. The script honors CLAUDE_CONFIG_DIR and falls back to ~/.claude.
If the user asked for a specific period ("this month", "Q1", "since March"), pass --since YYYY-MM-DD and/or --until YYYY-MM-DD (inclusive). The output's range field echoes what was applied (null = all-time). Ranged caveats to respect in the copy:
statsCache totals are rebuilt from per-day cache data; totalOutputTokens, per-model outputTokens, and longestSession become null (the cache only stores those as lifetime aggregates) — skip or reframe slides that need them.lastComputedDate, the cache-derived numbers undercount; history numbers stay exact.transcripts may be null for older ranges (local retention is short).Read the JSON. First check its range field matches this request — null for all-time, the requested dates otherwise. A mismatch means the stats are stale: re-run the analyzer before continuing.
It has five sections, each possibly null:
statsCache — totals from stats-cache.json: sessions, messages, tool calls, per-model token counts, busiest day, longest session. Note lastComputedDate — these totals may lag behind today.history — from history.jsonl: prompt count, hour histogram, weekday split, streaks, active days, top projects, top slash commands, top words, please/thanks counts.transcripts — top tools, top subagents, and top skills from locally retained transcripts (often a small subset; treat as flavor, not totals — copy built on these must be framed as recent-window, "lately", never lifetime).plugins — installed plugin inventory: count and names (lifetime; null on ranged runs).derived — precomputed ratios of this user's own numbers (tokens per prompt / per active day / per session). This is the only raw material for the headline comparison; the arithmetic is already done.If the script exits non-zero with an error JSON, tell the user what was missing and stop.
Before writing anything, look at the data and pick out 3–5 genuinely surprising or funny facts. Examples of what to hunt for:
/clear count) takes the slot instead. The sub copy on this slide must convert that number into a tangible, real-world equivalent the reader can picture — an everyday / cultural / physical thing from outside Claude Code, with the arithmetic actually done (e.g. "that's X hours of Mr Beast videos", "X transatlantic flights", "a paperback every Y days"). This is required, not optional, and it is the one job of this slide's sub. A sub that merely restates the number as another internal metric — tokens per prompt, characters, sessions — does not satisfy it; the comparison has to reach for something a person can feel in the real world. The big number itself comes from the data and stays accurate; derived (tokens per prompt / active day / session) is raw material for the arithmetic, never the yardstick itself. The yardstick must be fresh and different on every generation, never a tired cliché. Fresh means specific and a little surprising — not merely absent from the blocklist. Reach for a yardstick with cultural texture or a hook into this user's own world (the apps they build, their top project, their domain), and avoid the dry defaults that fit any token count — "X years of talking / typing / nonstop speech" is exactly that default and counts as stale, even though it sounds novel. Draft, then dodge the obvious: jot 3–4 candidate comparisons from different real-world domains, tag each for staleness (Wikipedia, distances to the moon, Olympic pools, War and Peace, "years of talking" all max out the meter — the ones every model reaches for first), bin the stalest, keep the sharpest, most specific one. Ground it in a fact you actually know: the conversion must rest on a stable, knowable constant (an audiobook ≈ 75,000 words; a flight ≈ a known number of hours) — never invent the figure a comparison needs (a video's view count, a film's runtime, box-office totals) just to make it land. If a yardstick only works with a number you're unsure of, it's the wrong yardstick — pick another. Pop-culture references are allowed but are seldom the right call for exactly this reason: they tend to be vivid only when propped up by a stat you'd be guessing at, and a fabricated number poisons the trust the accurate headline earned. If the stat you pick also powers a later slide, reframe that slide around a different angle or delete it — the deck never plays the same number twice.avgPromptChars, streaks, please/thanks), and compress the pair into a "The ..." title. Rules:
HOT_HOURS to the hours that prove it. HOT_HOURS does double duty: its first hour also picks the persona crab's pose — pre-dawn (4–8) makes it sleepy, daytime (9–17) alert, evening/night (18–3) wired — so put the hour that matches the persona first. Non-hourly traits (weekend habits, politeness, prompt length) live in the second half of the name or the sub-copy./clear habit (memory-wipe jokes), a dominant slash command, a workflow obsession.topWords is usually the richest vein, because vocabulary tics are the most personal numbers in the JSON. Rules:
history.topWords (already stopword-filtered, delivered as [word, count] pairs). Build the slide's bars from the top 4–5 (widest = the #1 word's count) and invent the copy from what those words reveal about what they built or how they think. Two hard rules so it doesn't fight its neighbours: never reuse a word the head-to-head or persona copy leans on (the deck never plays the same number, or word, twice), and if the list is thin or all generic filler, delete the slide rather than pad it.transcripts is non-null): who this user actually leans on — a favourite subagent, a skill habit, or the installed-vs-used plugin gap (plugins.installed vs the plugin names actually appearing in topSkills/slash commands). Every line of this slide's copy — kicker, headline, sub, footnote — is invented from this user's delegation pattern; there is no stock framing. Two hard rules: the numbers are a recent-window subset, so the copy says "lately", never lifetime; and if the data is too thin to be funny (one tool call, no agents), delete the slide instead of padding it.Chat output discipline. While running Steps 1–4, do not narrate the template, your plan, or what you're about to do. Emit only a single short status line per step (e.g. "Analyzing your data…", "Writing your deck…"). The only wrap-up is the fixed completion message in Step 4 (and the share addendum in Step 5) — emit that verbatim and add no bespoke prose, no stats recap, no commentary of your own.
${CLAUDE_SKILL_DIR}/templates/template.html to ./claude-unwrapped.html in the current directory.{{PLACEHOLDER}}. Search the file for {{ when done — none may remain.USER_NAME from git config user.name (first name only) or $USER. PERIOD_LABEL is the data date range, e.g. "January 13 — June 3, 2026".data-count attributes (HEADLINE_NUMBER, STREAK_DAYS, TOP_COMMAND_COUNT) must be raw integers, no commas. HEADLINE_UNIT is the short line under the count-up naming what it counts — lowercase, ends with a period, the unit's name plus a short flourish invented for this user. HOUR_DATA is the 24-int JSON array from history.hourHistogram; HOT_HOURS is a JSON array of hour numbers.MODEL_KICKER, PROJECTS_KICKER, WORDS_KICKER, STREAK_KICKER, TOP_COMMAND_KICKER, CAST_KICKER; STREAK_UNIT) are written fresh for this user like every other line. Kickers are short scene-setters, 2–5 words, in the deck's music-recap conceit — invent each one for this user. Unit lines complete the sentence the big number above them starts — lowercase, a few words, invented likewise.MODEL_BARS, PROJECT_BARS, WORD_BARS, CAST_BARS): emit up to 5 rows, widest = 100%, others proportional. WORD_BARS rows are the top words with their counts; CAST_BARS rows are the top subagents and/or skills from transcripts — short names (strip a plugin: prefix when it reads better). Exact row markup (tab-indented three levels):<div class="bar-row"><span class="label">NAME</span><div class="bar-track"><div class="bar-fill" style="--w:NN%"></div></div><span class="val">COUNT</span></div>
Use display names for models and short basenames for projects. Format big values compactly (10.3B, 3,505).
statsCache → no token totals), delete that <section> entirely (including its data-mood) rather than faking numbers. The deck degrades gracefully — the crab walks across whatever slides remain and the finale fires on the last one. If you delete the persona section, still fill HOUR_DATA and HOT_HOURS — set both to [].Write all copy in Claude's first person, addressing the user by name. The register: warm, wry, observant, self-aware about being an AI. Gentle roast, never mean. Concrete numbers beat adjectives. Patterns:
Never write generic filler ("What a year it's been!"). Every line must be earned by a specific number from this user's data and phrased in this user's vocabulary. The test for every sentence: if it would work in someone else's deck, it isn't done.
Confirm no {{ remains: grep -c '{{' claude-unwrapped.html must output 0.
Yardstick gate — blocking, loop until it passes:
open a deck whose headline has no real-world comparison, or one built on a number you guessed at.grep -icE 'wikipedia|encyclopedia|war and peace|library of congress|to the moon|olympic.{0,12}pool|mount everest|times around the (earth|world)|football field|years of (talking|typing|speaking|speech)' claude-unwrapped.html. Non-zero output = swap that yardstick for a fresher one and run the grep again. Repeat until it outputs 0. Substring over-matches are fine — when in doubt, rewrite anyway.open step below is forbidden until both checks pass. There is no exception.Genericness gate — blocking, loop until it passes:
grep -icE 'night owl|early bird|early riser|night shift|the perfectionist|the architect|the machine|the closer|the marathoner|on heavy rotation|now playing|top of the charts|chart.?topper|greatest hits' claude-unwrapped.html. Non-zero output = a persona name or kicker fell into a stock mode; rewrite it from a sharper, less obvious signal and run the grep again. Repeat until it outputs 0. Substring over-matches are fine — when in doubt, rewrite anyway.open stays forbidden until the grep outputs 0.Syntax-check the inline JS: extract the <script> body to a temp file and run node --check on it (skip silently if node is unavailable).
Open it: open claude-unwrapped.html (macOS) or xdg-open (Linux).
Emit the completion message as fixed text — not your own prose and not a stats recap. Substitute the user's first name; change nothing else:
🦀 {USER_NAME}, your Claude Unwrapped is ready — I've opened it in your browser. Give it a scroll.
Help improve this plugin by visiting https://github.com/stuartshields/claude-unwrapped
Like it? Support the project at https://ko-fi.com/claudeunwrapped
The GitHub and Ko-fi lines are always the last two things you say, in that order, on every run. On a share run, the Step 5 share lines slot in between the "ready" line and the GitHub line — never after them.
If the user asks for a shareable link / share file (e.g. "/unwrapped:generate share", "make it shareable"), also write ./claude-unwrapped.share.json — the same slide content as the HTML, as data. Follow references/share-file.md: it carries the privacy review (runs before the file is written), the exact v2 JSON schema the share site validates against, the length caps, the third-person rewrite the share page requires, and the share lines that slot into the Step 4 completion message.
When the user wants to change just one slide of a deck they already made — a fresh take ("redo my persona slide", "make the head-to-head funnier") or a directed edit ("change the persona name to 'The X'", "fix the typo in the streak sub") — don't rebuild the deck. Follow references/update-slide.md. This mode needs ./claude-unwrapped.html to exist; if it doesn't, run a normal generate (Steps 1–5) instead.
references/share-file.md — Step 5: the share-file privacy review, the exact v2 JSON schema, length caps, and the third-person rewrite. Consulted only on share runs.references/update-slide.md — the single-slide update mode. Consulted only when refreshing one slide of an existing deck.Creates, edits, and optimizes skills for Claude Code, including drafting, evaluating with test prompts, iterating on performance, and improving skill descriptions for better triggering accuracy.
npx claudepluginhub stuartshields/claude-unwrapped --plugin unwrapped