---
name: find-repeated-work
description: >
  Find repeated work in your own Claude Code history that's worth freezing into a
  slash command, and draft the SKILL.md for it. Triggers on: "what do I keep
  repeating", "anything worth making a command", "find repeated work".
---

# find-repeated-work

Turns the idea from the post *"Claude Code slash commands: what I actually put in mine"*
into a procedure: **don't freeze repeated tasks — freeze repeated decisions, with the
guardrail baked in.**

The split that makes this work:
- **Counting is code.** A tiny local script tallies what you repeated, how many times,
  across how many sessions and projects. Zero tokens, runs on your machine.
- **Judgment is me (Claude).** Which of those are noise vs. a real command candidate,
  and what each command should *always* and *never* do — that's context judgment. Freeze
  it in code and it goes wrong (it will flag pasted URLs as noise). So I do it each time.

Everything runs locally against `~/.claude/projects/`. Your history never leaves the machine.

---

## 1. Tally the repeats (code)

Run this one-liner. It reads your local Claude Code logs and prints two things: repeated
instructions, and repeated tool-flows (the tool sequence between your turns, collapsed).
No dependency on any repo — it's self-contained.

```bash
node --input-type=module -e '
import{readdirSync,readFileSync}from"node:fs";import{join}from"node:path";import{homedir}from"node:os";
const base=join(homedir(),".claude","projects");const MIN=Number(process.env.MIN||5);
const norm=t=>t.toLowerCase().replace(/[^a-z0-9 ]/g," ").replace(/\s+/g," ").trim().split(" ").slice(0,8).join(" ");
const short=n=>{const m=n.match(/^mcp__([^_]+(?:_[^_]+)*?)__(.+)$/);return m?m[1]+":"+m[2]:n;};
const real=s=>s&&!s.startsWith("<")&&!s.startsWith("[Request")&&!s.startsWith("Caveat")&&!s.includes("tool_result")&&!/^base directory for this skill/i.test(s)&&!/^\[image/i.test(s)&&!/^your tool call was malformed/i.test(s);
const textOf=o=>{const c=o?.message?.content;return typeof c==="string"?c:Array.isArray(c)?c.filter(p=>p?.type==="text").map(p=>p.text||"").join(""):"";};
const instr={},flows={};
for(const dir of readdirSync(base)){let files;try{files=readdirSync(join(base,dir)).filter(f=>f.endsWith(".jsonl"));}catch{continue;}
 for(const f of files){let lines;try{lines=readFileSync(join(base,dir,f)).toString().split("\n").filter(Boolean);}catch{continue;}
  const sid=dir+"/"+f.slice(0,8);const P=[];for(const l of lines){try{P.push(JSON.parse(l));}catch{}}
  let run=[],trg=null;
  for(const o of P){if(o.type==="assistant"&&Array.isArray(o?.message?.content))for(const p of o.message.content)if(p?.type==="tool_use")run.push(p.name);
   if(o.type==="user"){const t=textOf(o).trim();if(!real(t))continue;
    if(run.length>=2&&trg){const c=[];for(const n of run)if(c[c.length-1]!==n)c.push(n);const sig=c.join(" > ");
     if(sig.includes(" > ")){const b=flows[sig]||={c:0,s:new Set(),p:new Set()};b.c++;b.s.add(sid);b.p.add(dir);}}
    run=[];trg=t;const k=norm(t);if(k.length>=3){const c=instr[k]||={c:0,s:new Set(),p:new Set(),ex:t.replace(/\n/g," ").slice(0,90)};c.c++;c.s.add(sid);c.p.add(dir);}}}}}
const rI=Object.entries(instr).map(([k,v])=>({k,c:v.c,s:v.s.size,p:v.p.size,ex:v.ex})).filter(x=>x.c>=MIN).sort((a,b)=>b.c-a.c);
const rF=Object.entries(flows).map(([k,v])=>({k:k.split(" > ").map(short).join(" > "),c:v.c,s:v.s.size,p:v.p.size})).filter(x=>x.c>=MIN).sort((a,b)=>b.c-a.c);
console.log("\n## Repeated instructions (count / sessions / projects)");for(const x of rI.slice(0,25))console.log(`  ${x.c} / ${x.s} / ${x.p}  «${x.k}»  ▸ ${x.ex}`);
console.log("\n## Repeated tool-flows (between your turns, dedup runs)");for(const x of rF.slice(0,20))console.log(`  ${x.c} / ${x.s} / ${x.p}  ${x.k}`);
'
```

Raise the bar with `MIN=8 node ...`. This output is **raw, not a verdict** — pasted URLs,
`[Image …]`, skill-load headers all show up. Filtering them is the next step (mine).

## 2. Filter the noise (judgment — this is why it isn't code)
Drop what isn't a real repeated instruction/workflow:
- **Skill-load injection** — lines starting with "Base directory for this skill". System-injected, not you.
- **Attachments / pastes** — `[Image …]`, bare URLs. *But watch out:* a URL that recurs *with an instruction* ("implement this figma") is a real workflow signal — tell it apart from URL-only turns.
- **Errors / system** — "malformed", "system-reminder".
- **Bare approvals** — "ok", "commit", "yes" alone. Repeated, but there's no judgment to freeze. Unless approval+action always travel together ("ok deploy it") — then the action may be a candidate.

## 3. Pick candidates (judgment)
A real candidate meets three tests:
1. **Spread across many sessions/projects** — a repeat stuck in one session is that session's quirk.
2. **There's judgment in it** — not one command ("commit"), but the same *ordered check* every time (the tool-flow is its fingerprint).
3. **It isn't already a command** — check `ls ~/.claude/skills/` (and any project `.claude/skills/`).

## 4. Draft the SKILL.md (four-question frame — you fill the judgment)
For each candidate, draft with the frame from the post. **Don't auto-fill 3·4** — propose from
what the logs show, then confirm with the user.

```markdown
---
name: <kebab-case>
description: >
  <one line>. Triggers on: "<the phrases you actually repeated>".
---

# <name>

## 1. When should this trigger?
<the observed trigger phrasings, cleaned up>

## 2. What inputs does it expect?
<what the flow usually started from — a file / URL / current state>

## 3. What decisions should always be made?
<the judgment repeated in the flow. Not a command — a frozen decision.>

## 4. What must never happen?   ← the guardrail; most accidents live here
<what it must never do. Post's lesson: a review command must never become the publish command.
 If deploy/commit/delete/publish is in the flow, put "never without explicit approval" here.>
```

## 5. Ship it (optional — human gate)
Only publish on explicit approval ("looks good" ≠ "ship it"). Before a public gist, scan the
draft for project names, client names, paths, internal URLs — strip or anonymize. Then
`gh gist create <file> --public`. Tell the user to save the raw as
`.claude/skills/<name>/SKILL.md`. **But confirm the judgment is filled in first** — a bare
skeleton doesn't transfer (per the post: copying someone's SKILL.md gives you their
instructions without the judgment).

## Never
- **Mistake the tally for the verdict.** Top of the `--repeats` list is often skill-load noise.
- **Auto-confirm the always/never rules.** Propose from observation; the judgment is the user's.
- **Publish a gist without approval.**
- **Re-propose an existing skill.** Check `.claude/skills/` first.
