Claude Code skill that builds, debugs, audits and packages Claude Code mods from a plain-language description

A command that lists your recent Git commits, instantly and without spending any of Claude's time.
Claude Code is Anthropic's assistant for working with code, in the terminal or in the Claude desktop app. A mod is a small add-on for it: it can add a panel, a bar above the box where you type, a new /command, or a rule that checks what Claude is about to do. This folder is one complete mod you can install and use as it is, or copy as a starting point.
Git keeps a record of every saved change in a project; each saved change is a commit. When you come back to a project you often want to know "what did I do yesterday?". This mod adds a command that answers that right away, by reading Git directly. Claude is not asked, so it costs nothing and does not add to the conversation.
/recap for the last day, or /recap 7 for the last 7 days.None of its own.
| Type | What it does |
|---|---|
/recap | Commits from the last day |
/recap 7 | Commits from the last 7 days (any number from 1 to 30) |
You need Claude Code 2.1.287 or newer in the terminal, or 2.1.286 or newer in the desktop app.
_claude-plugin to .claude-plugin (with a dot at the start). It ships without the dot so the examples never load by accident.claude --plugin-dir path/to/git-recap
To keep it installed for good, ask the claude-mod-forge skill to package it ("package this mod").
From the skill folder (skills/claude-mod-forge), this runs the manifest check, a strict type check and the 6 automated tests of this mod:
node scripts/check-mod.mjs examples/git-recap
The tests cover the mod's logic and what it draws in the terminal and the desktop app. How it looks in your own window is worth a quick look in a real session.
For developers: the examples table lists the hooks and API calls this mod demonstrates.
hooks/register.ts 42 lines1import type { Register } from 'claude-code'
2
3import { formatLog, parseDays } from './recap'
4
5export const register: Register = on => {
6 on('session.start', async ($, e, next) => {
7 try {
8 await $.command.register({
9 name: 'recap',
10 description: 'List your recent commits in this repository',
11 argumentHint: '[days]',
12 immediate: true, // runs even while Claude is working; no turn starts
13 })
14 } catch (error) {
15 // A taken name throws; say so instead of failing silently.
16 $.ui.log(`recap: could not register /recap: ${String(error)}`)
17 }
18 return next(e)
19 })
20
21 // Answers its own command and never calls next. The text prints in the transcript.
22 on('command.run', { command: 'recap' }, async ($, e) => {
23 const parsed = parseDays(e.args)
24 if ('error' in parsed) return { text: parsed.error }
25
26 // argv list, no shell: user text never reaches a shell. The value is validated above.
27 let out
28 try {
29 out = await $.process.run(
30 ['git', 'log', `--since=${parsed.days}.days`, '--pretty=format:%h%x09%ad%x09%s', '--date=short', '--no-merges'],
31 { timeoutMs: 10_000 },
32 )
33 } catch {
34 return { text: 'git did not run (is it installed and on PATH?).' }
35 }
36 if (out.exitCode !== 0) {
37 return { text: /not a git repository/i.test(out.stderr) ? 'This folder is not a git repository.' : `git failed: ${out.stderr.trim().slice(0, 200)}` }
38 }
39 return { text: formatLog(out.stdout, parsed.days) }
40 })
41}
42hooks/recap.ts 27 lines1// Pure logic for /recap: argument parsing and output formatting. No $ here.
2
3export const MAX_DAYS = 30
4
5export type ParsedArgs = { days: number } | { error: string }
6
7export function parseDays(args: string): ParsedArgs {
8 const raw = args.trim()
9 if (raw === '') return { days: 1 }
10 if (!/^\d{1,3}$/.test(raw)) return { error: `"${raw.slice(0, 20)}" is not a number of days. Usage: /recap [days], 1 to ${MAX_DAYS}.` }
11 const days = Number(raw)
12 if (days < 1 || days > MAX_DAYS) return { error: `Pick 1 to ${MAX_DAYS} days.` }
13 return { days }
14}
15
16// git log --pretty=format:%h%x09%ad%x09%s --date=short
17export function formatLog(stdout: string, days: number): string {
18 const lines = stdout.split('\n').map(l => l.trim()).filter(Boolean)
19 if (lines.length === 0) return `No commits in the last ${days} day(s).`
20 const rows = lines.slice(0, 50).map(line => {
21 const [hash = '', date = '', ...subject] = line.split('\t')
22 return `- ${date} ${hash} ${subject.join(' ')}`
23 })
24 const more = lines.length > 50 ? `\n…and ${lines.length - 50} more.` : ''
25 return `${lines.length} commit(s) in the last ${days} day(s):\n${rows.join('\n')}${more}`
26}
27