Initial commit: skill files, docs site, README

- SKILL.md and pipeline.py from ~/.claude/skills/screenshot-rename/
- docs/index.html — archival/typewriter aesthetic homepage with hero
  monument, problem, 4-stage pipeline, before/after split, run-log
  receipt, ten gotchas, four use cases, install snippets
- MIT license

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Anthony Cardinale
2026-05-04 09:23:02 -04:00
commit 63edc33fc4
6 changed files with 1547 additions and 0 deletions
+996
View File
@@ -0,0 +1,996 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>screenshot-rename — a Claude Code skill</title>
<meta name="description" content="A Claude Code skill that turns a folder of timestamp-named screenshots into human-readable, searchable filenames using parallel Haiku vision agents.">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Fraunces:ital,opsz,wght@0,9..144,300;0,9..144,400;0,9..144,500;0,9..144,600;0,9..144,700;1,9..144,400;1,9..144,500;1,9..144,700&family=JetBrains+Mono:wght@400;500;600&display=swap">
<style>
:root {
--paper: #f7f3ed;
--paper-2: #efe7d8;
--paper-3: #e8dfcd;
--ink: #1c1916;
--ink-soft: #3c3530;
--ink-mute: #8d8478;
--accent: #7a1f3d;
--accent-soft: rgba(122,31,61,.10);
--accent-deep: #5d1730;
--rule: #d8cebf;
--rule-soft: #e6dfd2;
--serif: "Fraunces", "Iowan Old Style", "Charter", Georgia, serif;
--mono: "JetBrains Mono", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
--max: 1180px;
--gutter: clamp(20px, 4vw, 56px);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--paper);
color: var(--ink);
font-family: var(--serif);
font-feature-settings: "ss01", "ss02", "kern";
font-size: 17px;
line-height: 1.55;
-webkit-font-smoothing: antialiased;
text-rendering: optimizeLegibility;
background-image:
radial-gradient(1200px 400px at 88% -10%, rgba(122,31,61,.05), transparent 60%),
radial-gradient(800px 300px at 0% 20%, rgba(0,0,0,.03), transparent 70%);
}
/* subtle paper grain via SVG noise */
body::before {
content: "";
position: fixed; inset: 0;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='180' height='180'><filter id='n'><feTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/><feColorMatrix values='0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.045 0'/></filter><rect width='100%' height='100%' filter='url(%23n)'/></svg>");
pointer-events: none;
z-index: 0;
mix-blend-mode: multiply;
}
a { color: var(--accent); text-decoration: none; }
a:hover { color: var(--accent-deep); text-decoration: underline; text-decoration-thickness: 1px; text-underline-offset: 3px; }
.wrap {
max-width: var(--max);
margin: 0 auto;
padding: 0 var(--gutter);
position: relative;
z-index: 1;
}
/* ───── Header ────────────────────────────────────────── */
header {
border-bottom: 1px solid var(--rule);
background: rgba(247,243,237,.85);
backdrop-filter: saturate(140%) blur(8px);
-webkit-backdrop-filter: saturate(140%) blur(8px);
position: sticky; top: 0; z-index: 10;
}
header .wrap {
display: flex; align-items: baseline;
justify-content: space-between;
padding-top: 14px; padding-bottom: 14px;
gap: 24px;
}
.brand {
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.04em;
text-transform: lowercase;
color: var(--ink);
}
.brand .dot {
display: inline-block;
width: 7px; height: 7px;
background: var(--accent); border-radius: 50%;
vertical-align: 2px;
margin-right: 8px;
}
nav.top {
font-family: var(--mono);
font-size: 12px;
letter-spacing: 0.04em;
text-transform: lowercase;
display: flex; gap: 22px;
}
nav.top a {
color: var(--ink-mute);
}
nav.top a:hover { color: var(--accent); text-decoration: none; }
/* ───── Hero ────────────────────────────────────────── */
.hero {
padding: clamp(56px, 9vw, 120px) 0 clamp(40px, 6vw, 80px);
position: relative;
}
.eyebrow {
font-family: var(--mono);
font-size: 11.5px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--accent);
display: flex; align-items: center; gap: 12px;
margin-bottom: 28px;
}
.eyebrow .bar {
flex: 0 0 36px; height: 1px; background: var(--accent);
}
h1.display {
font-family: var(--serif);
font-weight: 400;
font-style: italic;
font-variation-settings: "opsz" 144;
font-size: clamp(48px, 8.5vw, 116px);
line-height: 0.96;
letter-spacing: -0.025em;
color: var(--ink);
margin: 0 0 28px;
max-width: 14ch;
}
h1.display .accent {
font-style: normal;
font-weight: 500;
color: var(--accent);
position: relative;
}
h1.display .accent::after {
content: "";
position: absolute;
left: 0; right: 0; bottom: 0.04em;
height: 0.13em;
background: var(--accent-soft);
z-index: -1;
}
.hero .lede {
max-width: 56ch;
font-size: clamp(17px, 1.45vw, 21px);
line-height: 1.55;
color: var(--ink-soft);
margin: 0 0 44px;
}
.hero .lede em { font-style: italic; color: var(--ink); }
.hero-cta {
display: flex; gap: 14px; align-items: center;
font-family: var(--mono);
font-size: 13px;
}
.btn {
display: inline-block;
padding: 12px 22px;
font-family: var(--mono);
font-size: 13px;
letter-spacing: 0.02em;
border-radius: 0;
border: 1px solid var(--ink);
background: var(--ink);
color: var(--paper);
transition: transform .15s ease, background .15s ease;
}
.btn:hover {
background: var(--accent-deep);
border-color: var(--accent-deep);
color: var(--paper);
text-decoration: none;
transform: translateY(-1px);
}
.btn.ghost {
background: transparent;
color: var(--ink);
}
.btn.ghost:hover {
background: transparent;
color: var(--accent);
border-color: var(--accent);
}
/* The hero rename "monument" */
.monument {
margin-top: clamp(60px, 8vw, 100px);
padding: 32px 28px;
background: var(--paper-2);
border: 1px solid var(--rule);
position: relative;
font-family: var(--mono);
font-size: clamp(13px, 1.25vw, 16.5px);
line-height: 1.7;
word-break: break-word;
}
.monument::before {
content: "FILENAME · BEFORE ↓ AFTER";
position: absolute;
top: -10px; left: 22px;
background: var(--paper);
padding: 0 10px;
font-size: 10px;
letter-spacing: 0.18em;
color: var(--ink-mute);
}
.monument .row {
display: grid;
grid-template-columns: 22px 1fr;
gap: 14px;
align-items: baseline;
padding: 6px 0;
}
.monument .row + .row { border-top: 1px dashed var(--rule); }
.monument .glyph {
color: var(--accent);
text-align: center;
font-weight: 600;
}
.monument .before { color: var(--ink-mute); }
.monument .after { color: var(--ink); }
.monument .after .desc { color: var(--accent-deep); font-weight: 500; }
/* ───── Section scaffolding ────────────────────────── */
section { padding: clamp(72px, 10vw, 140px) 0; position: relative; }
section + section { border-top: 1px solid var(--rule-soft); }
.section-label {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.22em;
text-transform: uppercase;
color: var(--ink-mute);
margin-bottom: 18px;
display: flex; align-items: center; gap: 12px;
}
.section-label .num {
display: inline-block;
width: 26px; height: 26px;
border: 1px solid var(--ink);
border-radius: 50%;
text-align: center; line-height: 24px;
font-size: 10px; color: var(--ink);
letter-spacing: 0;
}
h2 {
font-family: var(--serif);
font-weight: 400;
font-variation-settings: "opsz" 96;
font-size: clamp(34px, 4.5vw, 56px);
line-height: 1.05;
letter-spacing: -0.02em;
color: var(--ink);
margin: 0 0 28px;
max-width: 22ch;
}
h2 em { font-style: italic; color: var(--accent); }
.lede-2 {
max-width: 60ch;
font-size: 17px;
color: var(--ink-soft);
margin: 0 0 60px;
}
/* ───── Section: the problem ──────────────────────── */
.problem-grid {
display: grid;
grid-template-columns: minmax(0, 5fr) minmax(0, 6fr);
gap: clamp(28px, 4vw, 64px);
align-items: start;
}
@media (max-width: 720px) { .problem-grid { grid-template-columns: 1fr; } }
.opaque-list {
font-family: var(--mono);
font-size: 13.5px;
line-height: 2.05;
color: var(--ink-mute);
border-left: 2px solid var(--rule);
padding: 4px 0 4px 22px;
}
.opaque-list li { list-style: none; }
.opaque-list .ellipsis { color: var(--accent); margin-top: 6px; font-family: var(--serif); font-style: italic; }
.problem-note {
font-size: 17px;
color: var(--ink-soft);
line-height: 1.6;
}
.problem-note strong { font-weight: 500; color: var(--ink); }
.problem-note .stat {
display: block;
font-family: var(--serif);
font-style: italic;
font-variation-settings: "opsz" 96;
font-size: clamp(40px, 5vw, 68px);
line-height: 1;
color: var(--accent);
margin: 28px 0 14px;
}
/* ───── Section: pipeline ─────────────────────────── */
.pipeline {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 0;
border: 1px solid var(--rule);
background: var(--paper-2);
}
@media (max-width: 920px) { .pipeline { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 540px) { .pipeline { grid-template-columns: 1fr; } }
.stage {
padding: 32px 26px;
border-right: 1px solid var(--rule);
position: relative;
}
.stage:last-child { border-right: none; }
@media (max-width: 920px) { .stage:nth-child(2) { border-right: none; } }
.stage .step {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-mute);
margin-bottom: 14px;
}
.stage h3 {
font-family: var(--serif);
font-weight: 500;
font-style: italic;
font-variation-settings: "opsz" 60;
font-size: 30px;
line-height: 1.05;
margin: 0 0 16px;
color: var(--ink);
letter-spacing: -0.01em;
}
.stage p {
font-size: 14.5px;
color: var(--ink-soft);
margin: 0 0 16px;
line-height: 1.55;
}
.stage code {
font-family: var(--mono);
font-size: 12px;
color: var(--accent-deep);
background: rgba(122,31,61,.07);
padding: 1px 6px;
border-radius: 2px;
}
/* ───── Section: before / after ──────────────────── */
.split {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border: 1px solid var(--rule);
}
@media (max-width: 720px) { .split { grid-template-columns: 1fr; } }
.split > div {
padding: 40px 32px;
}
.split .lhs {
background: var(--paper-3);
border-right: 1px solid var(--rule);
}
.split .rhs {
background: var(--paper);
position: relative;
}
.split .rhs::before {
content: "→";
position: absolute;
left: -22px; top: 50%;
transform: translateY(-50%);
width: 44px; height: 44px;
background: var(--accent);
color: var(--paper);
border-radius: 50%;
display: grid; place-items: center;
font-family: var(--mono);
font-size: 18px;
}
@media (max-width: 720px) { .split .rhs::before { display: none; } }
.split .tag {
font-family: var(--mono);
font-size: 10.5px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--ink-mute);
margin-bottom: 18px;
}
.split .filename {
font-family: var(--mono);
font-size: clamp(15px, 1.7vw, 19px);
line-height: 1.55;
word-break: break-word;
margin-bottom: 18px;
}
.split .lhs .filename { color: var(--ink-mute); }
.split .rhs .filename { color: var(--ink); }
.split .rhs .filename .desc { color: var(--accent-deep); font-weight: 500; }
.split .meta {
font-size: 14px;
color: var(--ink-soft);
}
.split .meta .row { display: flex; gap: 10px; margin: 4px 0; }
.split .meta .row .k {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.08em;
color: var(--ink-mute);
min-width: 90px;
padding-top: 2px;
text-transform: uppercase;
}
/* ───── Section: receipt ─────────────────────────── */
.receipt {
background: var(--paper-2);
border: 1px solid var(--rule);
padding: clamp(36px, 4vw, 56px);
font-family: var(--mono);
font-size: 13.5px;
line-height: 1.85;
color: var(--ink);
position: relative;
max-width: 720px;
margin: 0 auto;
}
.receipt::before, .receipt::after {
content: ""; position: absolute; left: 0; right: 0; height: 8px;
background-image:
radial-gradient(circle at 6px 4px, var(--paper) 4px, transparent 4px);
background-size: 14px 8px;
background-repeat: repeat-x;
}
.receipt::before { top: -1px; }
.receipt::after { bottom: -1px; transform: scaleY(-1); }
.receipt .head {
font-family: var(--serif);
font-style: italic;
font-size: 18px;
color: var(--accent);
margin-bottom: 20px;
padding-bottom: 14px;
border-bottom: 1px dashed var(--rule);
}
.receipt .line { display: flex; justify-content: space-between; gap: 16px; }
.receipt .line .v { color: var(--accent-deep); font-weight: 500; }
.receipt .ok { color: var(--accent); font-weight: 600; }
.receipt .total {
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--ink);
font-weight: 500;
}
/* ───── Section: gotchas ─────────────────────────── */
.gotchas {
list-style: none; padding: 0; margin: 0;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 32px;
counter-reset: g;
}
@media (max-width: 720px) { .gotchas { grid-template-columns: 1fr; } }
.gotchas li {
counter-increment: g;
padding: 22px 0 22px 56px;
border-top: 1px solid var(--rule);
font-size: 16px;
color: var(--ink-soft);
line-height: 1.5;
position: relative;
}
.gotchas li::before {
content: counter(g, decimal-leading-zero);
position: absolute;
left: 0; top: 22px;
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.12em;
color: var(--accent);
width: 36px;
}
.gotchas li b {
font-family: var(--serif);
font-weight: 500;
font-style: italic;
color: var(--ink);
font-size: 18px;
display: block;
margin-bottom: 4px;
}
.gotchas li code {
font-family: var(--mono);
font-size: 13px;
color: var(--accent-deep);
}
/* ───── Section: use cases ──────────────────────── */
.cases {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 22px;
}
@media (max-width: 720px) { .cases { grid-template-columns: 1fr; } }
.case {
padding: 36px 28px;
background: var(--paper-2);
border: 1px solid var(--rule);
position: relative;
transition: transform .2s ease, box-shadow .2s ease;
}
.case:hover {
transform: translateY(-2px);
box-shadow: 0 12px 30px -16px rgba(28,25,22,.18);
border-color: var(--ink);
}
.case .num {
font-family: var(--mono);
font-size: 11px;
letter-spacing: 0.18em;
color: var(--ink-mute);
margin-bottom: 18px;
}
.case h3 {
font-family: var(--serif);
font-weight: 500;
font-style: italic;
font-size: 26px;
line-height: 1.15;
margin: 0 0 14px;
color: var(--ink);
letter-spacing: -0.01em;
}
.case p {
font-size: 15.5px;
color: var(--ink-soft);
line-height: 1.55;
margin: 0 0 16px;
}
.case .example {
font-family: var(--mono);
font-size: 12.5px;
color: var(--accent-deep);
background: var(--paper);
border-left: 2px solid var(--accent);
padding: 10px 12px;
word-break: break-word;
line-height: 1.5;
}
/* ───── Section: install ─────────────────────────── */
pre.code {
background: var(--ink);
color: #ece4d2;
font-family: var(--mono);
font-size: 13.5px;
line-height: 1.7;
padding: 26px 28px;
margin: 0 0 22px;
border-left: 3px solid var(--accent);
overflow-x: auto;
position: relative;
}
pre.code .c { color: #8d8478; font-style: italic; }
pre.code .k { color: #d8a4b3; }
pre.code .s { color: #e9c98c; }
pre.code .p { color: #c8d3a3; }
pre.code .lbl {
position: absolute;
top: 8px; right: 14px;
font-size: 10px;
letter-spacing: 0.18em;
color: #8d8478;
text-transform: uppercase;
}
.install-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 22px;
}
@media (max-width: 760px) { .install-grid { grid-template-columns: 1fr; } }
.install-grid > .col h3 {
font-family: var(--serif);
font-weight: 500;
font-style: italic;
font-size: 22px;
margin: 0 0 14px;
color: var(--ink);
}
.install-grid > .col p { color: var(--ink-soft); font-size: 15.5px; margin: 0 0 14px; line-height: 1.55; }
/* ───── Footer ─────────────────────────────────── */
footer {
border-top: 1px solid var(--rule);
padding: 56px 0 80px;
margin-top: 0;
font-family: var(--mono);
font-size: 12.5px;
color: var(--ink-mute);
}
footer .wrap {
display: flex;
justify-content: space-between;
align-items: baseline;
flex-wrap: wrap;
gap: 22px;
}
footer .marks { display: flex; gap: 22px; }
footer a { color: var(--ink); }
footer a:hover { color: var(--accent); text-decoration: none; }
footer .colophon {
font-family: var(--serif);
font-style: italic;
font-size: 14px;
color: var(--ink-mute);
max-width: 50ch;
}
/* ───── Reveal animation ─────────────────────── */
@media (prefers-reduced-motion: no-preference) {
.reveal { opacity: 0; transform: translateY(8px); transition: opacity .8s ease, transform .8s ease; }
.reveal.in { opacity: 1; transform: none; }
h1.display, .hero .lede, .hero-cta, .monument {
opacity: 0; transform: translateY(10px);
animation: rise .9s ease forwards;
}
.hero .lede { animation-delay: 0.10s; }
.hero-cta { animation-delay: 0.18s; }
.monument { animation-delay: 0.28s; }
@keyframes rise {
to { opacity: 1; transform: none; }
}
}
/* ───── Selection ────────────────────────────── */
::selection { background: var(--accent); color: var(--paper); }
</style>
</head>
<body>
<header>
<div class="wrap">
<a href="#" class="brand"><span class="dot"></span>screenshot-rename</a>
<nav class="top">
<a href="#problem">problem</a>
<a href="#pipeline">pipeline</a>
<a href="#gotchas">gotchas</a>
<a href="#cases">use cases</a>
<a href="#install">install</a>
<a href="https://gitea.tojo.team/cardinale/screenshot-rename">repo&nbsp;</a>
</nav>
</div>
</header>
<main>
<!-- ───── Hero ────────────────────────────────────── -->
<section class="hero">
<div class="wrap">
<div class="eyebrow"><span class="bar"></span>A claude code skill · vision-described renames</div>
<h1 class="display">A folder<br>of timestamps,<br>turned into a <span class="accent">manifest</span>.</h1>
<p class="lede">
Two hundred screenshots, all named <em>CleanShot 2026-04-15 at 09.14.07.png</em>.
Run this skill: ten Haiku subagents read each one in parallel, write a six-to-eight word
description, and rename the file in place — atomically, with the safety nets the author
cost himself four files to learn.
</p>
<div class="hero-cta">
<a class="btn" href="#install">Install</a>
<a class="btn ghost" href="https://gitea.tojo.team/cardinale/screenshot-rename">View on gitea ↗</a>
</div>
<div class="monument">
<div class="row">
<span class="glyph"></span>
<span class="before">CleanShot 2026-04-15 at 09.14.07.png</span>
</div>
<div class="row">
<span class="glyph"></span>
<span class="after">CleanShot · <span class="desc">Shamel Studio Affiliate Referral Code Modal</span> · 2026-04-15 at 09.14.07.png</span>
</div>
</div>
</div>
</section>
<!-- ───── The problem ──────────────────────────────── -->
<section id="problem">
<div class="wrap">
<div class="section-label"><span class="num">01</span>The problem</div>
<h2>You can't <em>find</em> a screenshot you took six months ago.</h2>
<div class="problem-grid">
<ul class="opaque-list reveal">
<li>CleanShot 2025-09-26 at 16.27.39.png</li>
<li>CleanShot 2025-11-19 at 13.12.36.png</li>
<li>CleanShot 2025-12-05 at 11.24.33.png</li>
<li>CleanShot 2026-02-18 at 12.48.31.png</li>
<li>CleanShot 2026-03-04 at 06.13.44.png</li>
<li>CleanShot 2026-03-17 at 22.10.20.mp4</li>
<li>CleanShot 2026-03-21 at 11.46.42.png</li>
<li>CleanShot 2026-04-08 at 12.09.10.png</li>
<li class="ellipsis">…and 187 more</li>
</ul>
<div class="problem-note reveal">
<p>
A timestamp tells you <strong>when</strong> a screenshot exists.
It doesn't tell you what's <strong>in</strong> it. Spotlight indexes the
pixels reluctantly; iCloud-synced folders less reliably still. The only
way most people find an old screenshot is by remembering, roughly,
what they were doing the week they took it — and scrolling.
</p>
<p>
The real cost isn't filesystem clutter. It's the screenshots you
stopped taking, because past you knew future you wouldn't be able to
surface them.
</p>
<span class="stat">196 →</span>
<p style="margin-top: -4px;">files renamed in the first run that motivated this skill, in three minutes, with zero loss after the second pass. The first pass cost four files. <em>That's why the safety rules below are written the way they are.</em></p>
</div>
</div>
</div>
</section>
<!-- ───── Pipeline ─────────────────────────────────── -->
<section id="pipeline">
<div class="wrap">
<div class="section-label"><span class="num">02</span>The pipeline</div>
<h2>Four stages, <em>in two minutes</em>.</h2>
<p class="lede-2">
The skill does as little as possible, and validates as much as possible.
Subagents handle the work that benefits from parallelism (vision); Python
handles the work that benefits from being correct (filename mutation,
collision detection, the actual <code>os.rename</code>).
</p>
<div class="pipeline reveal">
<div class="stage">
<div class="step">Stage 01</div>
<h3>Prep.</h3>
<p>Extract the first frame from every <code>.mp4</code> and <code>.pdf</code>. Resize every image to <code>1568px</code> max — Read's image cap is real. Build a manifest TSV.</p>
<p style="color:var(--ink-mute); font-size: 13px;"><code>ffmpeg · sips · /tmp/screenshot-rename/full-batch-NN</code></p>
</div>
<div class="stage">
<div class="step">Stage 02</div>
<h3>Describe.</h3>
<p>Dispatch one Haiku subagent per batch, in parallel — ten at a time. Each agent reads its 19 images and writes 6&ndash;8&nbsp;word descriptions to <code>desc-full-NN.tsv</code>.</p>
<p style="color:var(--ink-mute); font-size: 13px;"><code>model · "haiku" · ~$0.30 / 200 files</code></p>
</div>
<div class="stage">
<div class="step">Stage 03</div>
<h3>Plan.</h3>
<p>Aggregate. Validate every line: 6+&nbsp;words, alnum&nbsp;only, source exists, target doesn't, no plan-internal collisions. Build the full rename map <em>in&nbsp;memory</em>.</p>
<p style="color:var(--ink-mute); font-size: 13px;"><code>plan-full.tsv · zero-error policy</code></p>
</div>
<div class="stage">
<div class="step">Stage 04</div>
<h3>Execute.</h3>
<p>One <code>os.rename</code> per row, with pre-existence check. Audit <code>len(listdir)</code> before&nbsp;and&nbsp;after — it must&nbsp;be&nbsp;equal. <em>That equality is your only proof no overwrites happened.</em></p>
<p style="color:var(--ink-mute); font-size: 13px;"><code>before == after · ok / fail</code></p>
</div>
</div>
</div>
</section>
<!-- ───── Before / After ───────────────────────────── -->
<section id="example">
<div class="wrap">
<div class="section-label"><span class="num">03</span>An actual rename</div>
<h2><em>Before</em> a timestamp.<br>After, a sentence.</h2>
<p class="lede-2">
A real rename from the run that motivated this skill. The description
was generated by Haiku in roughly two seconds.
</p>
<div class="split">
<div class="lhs">
<div class="tag">Before</div>
<div class="filename">CleanShot 2026-03-17 at 22.10.20.mp4</div>
<div class="meta">
<div class="row"><span class="k">Length</span><span>36 chars</span></div>
<div class="row"><span class="k">Searchable</span><span>by date only</span></div>
<div class="row"><span class="k">Tells you</span><span>when</span></div>
</div>
</div>
<div class="rhs">
<div class="tag">After</div>
<div class="filename">CleanShot · <span class="desc">Claude Conversation About Context Calculator Implementation</span> · 2026-03-17 at 22.10.20.mp4</div>
<div class="meta">
<div class="row"><span class="k">Length</span><span>91 chars</span></div>
<div class="row"><span class="k">Searchable</span><span>by content + date</span></div>
<div class="row"><span class="k">Tells you</span><span>what, when</span></div>
</div>
</div>
</div>
<p style="font-family:var(--mono); font-size:12px; color:var(--ink-mute); letter-spacing:0.1em; text-transform:uppercase; margin-top:20px;">
The original timestamp survives unchanged. Sorting still works. The description sits between, set off by em-dashes.
</p>
</div>
</section>
<!-- ───── Receipt ──────────────────────────────────── -->
<section>
<div class="wrap">
<div class="receipt reveal">
<div class="head">screenshot-rename · run log · 2026-05-04</div>
<div class="line"><span>source files</span><span class="v">196</span></div>
<div class="line"><span>resized to 1568px</span><span class="v">196</span></div>
<div class="line"><span>frames extracted (mp4 / pdf)</span><span class="v">9</span></div>
<div class="line"><span>batches dispatched</span><span class="v">10 · parallel</span></div>
<div class="line"><span>haiku descriptions returned</span><span class="v">196</span></div>
<div class="line"><span>plan validated</span><span class="v">189 renames · 0 errors</span></div>
<div class="line"><span>plan collisions</span><span class="v">none</span></div>
<div class="line"><span>file count before</span><span class="v">195</span></div>
<div class="line"><span>file count after</span><span class="v">195</span></div>
<div class="line total"><span>renames committed</span><span class="ok">189 ✓</span></div>
<div class="line total"><span>files lost</span><span class="ok">0 ✓</span></div>
</div>
</div>
</section>
<!-- ───── Gotchas ──────────────────────────────────── -->
<section id="gotchas">
<div class="wrap">
<div class="section-label"><span class="num">04</span>The rules that prevent data loss</div>
<h2>Every rule below was <em>paid for</em>.</h2>
<p class="lede-2">
During development, four files were destroyed by a one-line bash mistake.
Each rule names the failure mode that earned its place. None are aspirational.
</p>
<ol class="gotchas">
<li><b>Resize before vision.</b>Retina screenshots exceed Read's image cap. Use <code>sips -Z 1568 -s format jpeg</code> first. The agent will fail mid-batch otherwise.</li>
<li><b>Frames, not videos.</b>The vision tool can't read <code>.mp4</code> or multi-page <code>.pdf</code>. Extract a frame with <code>ffmpeg -ss 1 -frames:v 1</code> and describe that.</li>
<li><b>Never trust bash regex on filenames.</b>zsh's <code>[[ =~ ]]</code> does not populate <code>BASH_REMATCH</code>. Pattern silently fails, target name is empty, multiple <code>mv</code>s collide. <em>Use Python.</em></li>
<li><b><code>mv</code> overwrites silently.</b>One off-by-one in target construction destroys data with no error. Use <code>mv&nbsp;-n</code> in shell, or <code>os.rename</code> after an <code>os.path.exists</code> check in Python.</li>
<li><b>Plan the full rename in memory first.</b>Build every <code>(src, dst)</code> tuple. Verify each <code>dst</code> is unique, doesn't exist, and corresponds to a real <code>src</code>. <em>Then</em> mutate disk.</li>
<li><b>File-count audit, every time.</b><code>len(listdir(DEST))</code> before and after must be equal. Inequality is the only evidence of silent loss you'll get.</li>
<li><b>iCloud snapshots are stubs, not bytes.</b>Files in a Time Machine local snapshot inside an iCloud-synced tree are file-provider stubs. <code>cat</code> them and the read times out. Real recovery comes from external backups.</li>
<li><b>Run renames foreground.</b><code>Bash run_in_background</code> with <code>while read</code> may exit early with no progress. Run via Python in the same shell — <code>os.rename</code> is just a syscall.</li>
<li><b>Validate the filename column.</b>Haiku occasionally returns the resized <code>.jpg</code> name instead of the original <code>.png</code>. The plan-builder must try alternate extensions when the claimed source isn't found.</li>
<li><b>Preserve the original extension.</b>The pipeline reads from a resized JPEG but renames the original <code>.mp4</code> / <code>.pdf</code>. Write the source extension back into the new name.</li>
</ol>
</div>
</section>
<!-- ───── Use cases ────────────────────────────────── -->
<section id="cases">
<div class="wrap">
<div class="section-label"><span class="num">05</span>Use cases</div>
<h2>What this looks like in <em>practice</em>.</h2>
<p class="lede-2">
The skill earns its keep when "Spotlight will find it" stops being true. Four scenarios where it has.
</p>
<div class="cases">
<div class="case">
<div class="num">A · Archive</div>
<h3>An audit of a year of work.</h3>
<p>Run the skill on a ~year-old screenshot folder. The output is a chronologically-sorted
narrative of what you were thinking about, week by week — readable from the filename column
in Finder. No app needed.</p>
<div class="example">CleanShot · Synqora Audit Context Calculator Discussion Continued · 2026-03-15 at 08.08.29.png</div>
</div>
<div class="case">
<div class="num">B · Recall</div>
<h3>"Find the screenshot of the bug from last March."</h3>
<p>Renaming once buys you free-text search forever. <code>mdfind "synqora session load"</code>
surfaces the right file in a fraction of a second, with no manual tagging.</p>
<div class="example">CleanShot · Synqora Session Load Failed Disconnect Reconnecting Error · 2026-04-18 at 13.37.12.png</div>
</div>
<div class="case">
<div class="num">C · Onboarding</div>
<h3>Designer joins. Hands them the folder.</h3>
<p>Instead of curating a deck of "what we've shipped this quarter," point them at the renamed
screenshot folder. The filenames are the deck. Categorize by app, by feature, by
timeline — the descriptions are already there.</p>
<div class="example">CleanShot · Xcode Preview Swiftui Render Table Comparison Tools · 2026-03-21 at 10.47.26.png</div>
</div>
<div class="case">
<div class="num">D · Memory</div>
<h3>A searchable design memory.</h3>
<p>Pair with a periodic re-run on new captures. The folder becomes a queryable artifact:
every screenshot you took, with what was in it, in plain text, in the filesystem you
already use. No new tool to adopt.</p>
<div class="example">CleanShot · Storyboard Browser With Harry Bridges 1933 Rally Shots · 2026-05-03 at 07.58.27.png</div>
</div>
</div>
</div>
</section>
<!-- ───── Install ──────────────────────────────────── -->
<section id="install">
<div class="wrap">
<div class="section-label"><span class="num">06</span>Install &amp; run</div>
<h2>Three commands, <em>one folder</em>.</h2>
<p class="lede-2">
The skill installs as a Claude Code skill. Once cloned into <code>~/.claude/skills/</code>, it
activates automatically when you ask Claude to rename a screenshot folder. It can also be
driven from the command line.
</p>
<pre class="code"><span class="lbl">install</span><span class="c"># clone into your Claude Code skills directory</span>
<span class="k">git</span> clone https://gitea.tojo.team/cardinale/screenshot-rename.git \
<span class="s">~/.claude/skills/screenshot-rename</span></pre>
<div class="install-grid">
<div class="col">
<h3>Driven by Claude Code</h3>
<p>Open Claude Code in any project and say it conversationally. The skill activates from its description and runs the workflow end to end.</p>
<pre class="code" style="border-left-color:var(--accent-deep);"><span class="lbl">claude code</span><span class="p">&gt;</span> rename all the cleanshots in
<span class="s">~/Documents/Screenshots/</span>
based on their content.</pre>
</div>
<div class="col">
<h3>Driven directly from the shell</h3>
<p>For folders too large for a single session, run each stage by hand. Dispatch the Haiku subagents from a Claude Code session in between.</p>
<pre class="code"><span class="lbl">cli</span><span class="k">python3</span> pipeline.py prep --src <span class="s">"./shots"</span>
<span class="c"># dispatch one haiku agent per batch...</span>
<span class="k">python3</span> pipeline.py plan --src <span class="s">"./shots"</span>
<span class="k">python3</span> pipeline.py execute --src <span class="s">"./shots"</span></pre>
</div>
</div>
</div>
</section>
</main>
<footer>
<div class="wrap">
<div class="marks">
<a href="https://gitea.tojo.team/cardinale/screenshot-rename">repository ↗</a>
<a href="./SKILL.md">SKILL.md</a>
<a href="./LICENSE">MIT</a>
</div>
<div class="colophon">
Set in Fraunces &amp; JetBrains Mono. Written after losing four files to a bash regex bug.
</div>
</div>
</footer>
<script>
// Tiny stagger for sections that fade in on scroll.
(function () {
if (!('IntersectionObserver' in window)) return;
const io = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) { e.target.classList.add('in'); io.unobserve(e.target); }
});
}, { threshold: 0.08 });
document.querySelectorAll('.reveal').forEach(el => io.observe(el));
})();
</script>
</body>
</html>