A toast when a long turn or a subagent finishes

A personal Claude Code skills marketplace by jafforgehq. It currently lists superpowers, context7, humanizer, marketing-skills, caveman, godot-prompter, taste-skill, designlang and ponytail, all referenced from upstream, and wrappers and plugins of my own: rtk, around rtk, ccusage, around ccusage, domain-scout, and three terminal UI mods: status-bar, done-toast and session-recap. It is where my own skills will be added over time.
One command adds the marketplace, installs every plugin in it, turns on auto-update, and makes jaffskils the only source of skills (see Exclusive mode):
git clone https://github.com/jafforgehq/jaffskils && ./jaffskils/setup.sh
or, without cloning:
curl -fsSL https://raw.githubusercontent.com/jafforgehq/jaffskils/main/setup.sh | bash
Look before you leap: setup.sh --dry-run prints what it would change and changes nothing. With the curl form, pass flags after bash -s --:
curl -fsSL https://raw.githubusercontent.com/jafforgehq/jaffskils/main/setup.sh | bash -s -- --dry-run
Afterwards run /reload-plugins in Claude Code, or restart it. The first run saves your original settings.json as settings.json.jaffskils.bak and never overwrites that copy.
| Flag | Effect |
|---|---|
--dry-run | Show what exclusive mode would change; change nothing |
--yes | Skip the confirmation prompt (needed when there is no terminal) |
--no-clean | Install only; leave other skills alone |
--no-auto-update | Do not touch the auto-update setting |
--list | Show every plugin the marketplace offers against what is installed; change nothing |
--check | Like --list, and exit 1 if a plugin is missing, disabled or no longer in the marketplace |
setup.sh is safe to re-run. It needs claude and python3 on your PATH.
After a successful install, setup.sh removes everything that is not from jaffskils from your Claude Code config (~/.claude, or $CLAUDE_CONFIG_DIR):
| What | What happens |
|---|---|
| Plugins from other marketplaces (user scope) | Uninstalled |
Marketplaces other than jaffskils, including Anthropic's claude-plugins-official | Removed. This also uninstalls their plugins, wherever they were installed, and deletes those plugins' saved options and data. Only the built-in Anthropic directory stays. Claude Code may re-add claude-plugins-official in a later session; it adds no skills, and re-running removes it again. To get it back: claude plugin marketplace add anthropics/claude-plugins-official |
Standalone skills in ~/.claude/skills (except the app-managed synced folder) | Moved to ~/.claude/skills-removed-<date>/, never deleted |
| Plugins synced from claude.ai | Disabled (they are account-managed and cannot be uninstalled) |
It prints the full list first and asks Proceed? [y/N]. With no terminal and no --yes it refuses before changing anything. To delete the moved skills for good, remove the skills-removed-<date> folder yourself; the script prints the exact command.
What a script cannot remove: Claude Code's built-in commands and skills, skills the Claude desktop app supplies, and organisation-managed plugins.
caveman and ponytail register SessionStart hooks that change how Claude answers in every session (terse prose, and simplest-possible code). Disable one with /plugin if you want normal output back.
designlang is different: its slash commands run npx designlang, which downloads the latest npm package when you call one. The marketplace pin fixes the commands, not that package. It needs Node and runs only when you invoke a command.
ccusage reports Claude Code token use and cost from the session logs already on your machine. It publishes no Claude Code plugin of its own, so the ccusage plugin here is a small skill of mine that teaches Claude its commands (daily, monthly, session, blocks, statusline). Like designlang, it runs npx ccusage@latest, which downloads the latest npm package when used; nothing is installed or pinned. It needs Node. The status line is not switched on for you: the skill gives the settings.json snippet and asks before editing.
rtk shrinks command output before it reaches the context window. The rtk plugin adds a PreToolUse hook that rewrites Bash commands through rtk rewrite, plus a small skill. The plugin does not install the binary; get it with brew install rtk-ai/tap/rtk (the hook also needs jq). Without them the hook leaves commands unchanged. Do not also run rtk init -g, or the hook runs twice.
Finds, shortlists and wires up a domain name. With the Namecheap MCP connected it checks availability in one batch, prefers a purchase link over registering for you, and reads existing DNS records before saving new ones. Without it, the skill says so and falls back to manual mode: whois and dig for a "likely available" answer, registrar links, and DNS records printed as a table to paste. The plugin does not install or bundle the MCP; connect Namecheap yourself (a claude.ai connector, or your own MCP config) and start a new session. Registering a domain, saving DNS records, changing nameservers and saving contacts always wait for a yes.
Three small mods for Claude Code's mod system. They load like any plugin, and run on a build that supports mods; elsewhere /plugin shows them but they do nothing.
| Mod | What it does |
|---|---|
status-bar | One status line entry after each turn: session cost, git branch, model, context use ($0.42 · main · opus · ctx 31%) |
done-toast | A toast when a turn that took 30 seconds or more ends, or a subagent finishes |
session-recap | /recap prints the files changed, shell commands run, cost, context use and open TODOs |
Each has a register.test.ts; run one with claude plugin test plugins/<name>.
/plugin marketplace add jafforgehq/jaffskils
/plugin install superpowers@jaffskils
If superpowers is also installed from another marketplace, its hooks run twice, so use one source only.
Upstream changes reach you only when all three of these hold:
setup.sh turns it on. By hand: /plugin → Marketplaces → jaffskils → Enable auto-update. Plugins refresh up to about ten minutes after the first message of a session, and a running session needs /reload-plugins.sha, so upstream changes never reach you unreviewed. To take them, run scripts/update-pins.sh, read the diff and commit.version in its plugin.json, so a pin bump only matters to you when that version moves. A plugin of mine with no version is versioned by commit, so every push reaches you.| Script | Use |
|---|---|
scripts/update-pins.sh | Move every upstream pin to its repo's HEAD |
scripts/pin-drift.sh | Markdown table of how many commits each pin is behind; changes nothing |
scripts/pin-changelog.sh | After update-pins.sh: the upstream commits behind each pin that moved |
scripts/marketplace-lint.sh [--remote] | Fail on duplicate names, missing local plugin folders, bad sha values, unlisted plugins/ folders; --remote also asks GitHub whether each sha exists |
CI runs the lint on every push. The monthly update-pins job puts the changelog in its pull request body. The pin-drift workflow writes the drift table to the job summary every Monday, and comments it on any pull request that touches marketplace.json.
./setup.sh --check is the multi-machine check: run it on any machine to see whether it has every plugin the marketplace offers. Run claude plugin marketplace update jaffskils first to compare against what is pushed.
plugins/<name>/.claude-plugin/plugin.json and plugins/<name>/skills/<skill>/SKILL.md..claude-plugin/marketplace.json with "source": "./plugins/<name>" and no version.Check the manifest with claude plugin validate .. A "no version specified" warning is expected for plugins without a version.
tests/test-setup.sh runs setup.sh against throwaway config directories, so your real ~/.claude is never touched. Most cases use a small local marketplace; one installs every plugin in the real manifest, so the suite needs network access. GitHub Actions runs it, and claude plugin validate ., on every push:
tests/test-setup.shhooks/register.ts 18 lines1import type { Register } from 'claude-code'
2
3const LONG_TURN_MS = 30_000
4
5// Toasts when a turn that took a while ends, or when a subagent finishes, so you
6// can look away while it works.
7export const register: Register = on => {
8 on('turn.complete', async ($, e, next) => {
9 if (e.agentId !== undefined) {
10 $.ui.toast('A subagent finished.')
11 } else if (!e.isAborted && e.durationMs >= LONG_TURN_MS) {
12 $.ui.toast(`Done after ${Math.round(e.durationMs / 1000)}s.`)
13 }
14
15 return next(e)
16 })
17}
18