Enforces the no-em-dash rule: a system prompt section, a reminder to the model after a write that adds one, a toast when a reply uses one

Function-hook mods for Claude Code. Each folder is one plugin.
| Mod | What it does | ||
|---|---|---|---|
context-watch | One-line gauge above the prompt for context fill, cost and hot rate limits; toasts at 60/80/90%; compaction keeps your instructions and edited files; /ctx opens a context breakdown pane | ||
live-spinner | Spinner shows what is running: the Bash description or a trimmed command, the ssh host, file being read or edited, search, subagent, MCP tool; a tool's own elapsed time after 15s; "Waiting for signing approval" when a signed git commit or tag sits quiet | ||
pr-watch | Tracks PRs from gh pr / gt submit / git push output, shows checks above the prompt, toasts on green/red/merged, blocks foreground CI polling loops; /prs | ||
house-style | No em dashes: system prompt rule, reminder to the model after a write that adds one, toast when a reply uses one | ||
handoff | /carryover [focus] writes a state note from the conversation and clears; the next prompt carries it. /carryover save, show, drop | ||
agent-jobs | /jobs opens a pane of running subagents and background shells with elapsed time and a Stop button; toasts when background work finishes | ||
zsh-safe | Quotes glob flags zsh would expand (--include=*.ts, find -name *.ts) and keeps a grep that finds nothing from failing a cd ... && grep chain | ||
pm-guard | Blocks npm/npx/yarn where the lockfile says bun or pnpm, and steers to bun where there is no lockfile | ||
remote-guard | Asks before a destructive command (restart, rm, kill, reboot, pct/qm stop, zfs destroy) runs over ssh | ||
xcode-status | Only in a folder with an .xcodeproj/.xcworkspace: one line with the last build or test result (flags runs that executed 0 tests) and the booted simulator; blocks a second xcodebuild while one is running; re-checks on /cd and keeps each project's last result | ||
agent-models | Pins the model each subagent type runs on (Explore, general-purpose, Plan, any other, plus custom type=model pairs), set in /config or with /agent-models <type> <model>; a model the caller names wins unless "Override explicit models" is on | ||
worktrees | /worktrees pane: each git worktree with branch, uncommitted changes, ahead/behind and PR state; Remove (after asking) on clean worktrees whose PR is merged or closed, plus "Remove N merged" to clear every merged one at once | ||
pr-rules | Blocks gh pr create / gt submit when the repo keeps an [Unreleased] changelog and the branch adds no entry (skip with --label no-changelog); where the repo's CLAUDE.md says to label every PR (or requireLabels is on), blocks a PR with no --label and lists the repo's labels. Repos with neither are untouched; `/pr-rules on | off | auto` overrides per repo (kept across sessions) |
claude plugin marketplace add narrowstacks/claude-code-mods
claude plugin install context-watch@claude-code-mods
Or load from a checkout without installing:
claude --plugin-dir ./context-watch --plugin-dir ./live-spinner
claude plugin validate ./context-watch
claude plugin test ./context-watch
tsc -p ./context-watch works once Claude Code has loaded the mod and written .claude-plugin/types/.
hooks/register.ts 55 lines1import type { Register } from 'claude-code'
2
3const EM_DASH = /—/g
4
5const RULE = [
6 'House style (strict): never write the em dash character (U+2014) anywhere: replies, docs, commit messages, PR bodies, code comments, agent instructions.',
7 'Use a comma, colon, parentheses, a period, or " - " instead. Check your text for it before sending.',
8].join(' ')
9
10export const countEmDashes = (text: string) => text.match(EM_DASH)?.length ?? 0
11
12const writtenText = (input: object): string => {
13 const record = input as Record<string, unknown>
14 const parts = [record.content, record.new_string, record.new_source]
15
16 return parts.filter((part): part is string => typeof part === 'string').join('\n')
17}
18
19export const register: Register = on => {
20 on('prompt.compose', async ($, e, next) => {
21 const composed = await next(e)
22
23 return {
24 sections: [...composed.sections, { id: 'house-style:em-dash', text: RULE, scope: 'session' as const }],
25 }
26 })
27
28 on('tool.call', async ($, e, next) => {
29 const ran = await next(e)
30 const isWrite = e.tool === 'Write' || e.tool === 'Edit' || e.tool === 'NotebookEdit'
31 if (!isWrite || ran.deny !== undefined || ran.isError === true) {
32 return ran
33 }
34
35 const count = countEmDashes(writtenText(e))
36 if (count === 0) {
37 return ran
38 }
39
40 const where = 'file_path' in e && typeof e.file_path === 'string' ? e.file_path : 'the file'
41 const note = `house-style: that write added ${count} em dash${count === 1 ? '' : 'es'} to ${where}. The user forbids em dashes in docs and comments; replace them unless they are part of data the user asked to preserve.`
42
43 return { ...ran, context: [...(ran.context ?? []), note] }
44 })
45
46 on('turn.complete', async ($, e, next) => {
47 const count = e.agentId === undefined ? countEmDashes(e.answer) : 0
48 if (count > 0) {
49 $.ui.toast(`house-style: reply used ${count} em dash${count === 1 ? '' : 'es'}`)
50 }
51
52 return next(e)
53 })
54}
55