SLOPSHOPPER

dashless

Rewrites em dashes out of Claude's replies, file writes and edits, and commit/PR messages before they land.

newguardstatus
v0.1.0MITupdated 2026-10-020xGondarxyz/claude-code-mods/dashless
A shopper browsing a rack in a slop shop
README

dashless

Removes em dashes from what Claude writes, before it lands. About 80 lines of TypeScript.

WhereWhat gets fixed
Write toolthe file content
Edit toolnew_string only (old_string must match the file, so it is never touched)
Bashonly git commit and gh pr / gh issue create, edit, comment; every other command runs as is
Repliestext blocks in Claude's responses, before they are stored

Rewrite rules (<code>&mdash;</code> is the em dash, <code>&ndash;</code> the en dash):

<!-- Dashes in this file are HTML entities on purpose. With literal characters, dashless rewrites its own examples whenever Claude edits this file. -->

InputOutput
<code>a &mdash; b</code>, <code>a&mdash;b</code>a, b
<code>&mdash; item</code> at line startitem (indentation kept)
<code>done &mdash;</code> at line enddone
<code>note: &mdash; x</code>note: x
<code>a &ndash; b</code> (spaced en dash)a, b
<code>1990&ndash;2000</code> (range)unchanged
--force, -- fileunchanged

The status line shows a running count: dashless: 3 em dashes fixed.

Tested by running the same prompt twice in a throwaway git repo (write a file, commit it, reply with a sentence, each with an em dash). Without the mod 4 em dashes landed; with it, 0.

Run its tests: claude plugin test dashless

Source 2 files
hooks/register.ts 67 lines
1import type { Register } from 'claude-code'
2import { fixDashes } from './fix'
3
4const COMMIT_OR_PR = /\bgit\s+commit\b|\bgh\s+(pr|issue)\s+(create|edit|comment)\b/
5
6let total = 0
7
8export const register: Register = (on) => {
9  on('tool.call', { tool: 'Write' }, ($, e, next) => {
10    try {
11      const fixed = fixDashes(e.content)
12      if (fixed.count > 0) {
13        total += fixed.count
14        $.ui.status(`dashless: ${total} em dashes fixed`)
15        return next({ ...e, content: fixed.text })
16      }
17    } catch {}
18    return next(e)
19  })
20
21  on('tool.call', { tool: 'Edit' }, ($, e, next) => {
22    try {
23      const fixed = fixDashes(e.new_string)
24      if (fixed.count > 0) {
25        total += fixed.count
26        $.ui.status(`dashless: ${total} em dashes fixed`)
27        return next({ ...e, new_string: fixed.text })
28      }
29    } catch {}
30    return next(e)
31  })
32
33  on('tool.call', { tool: 'Bash' }, ($, e, next) => {
34    try {
35      if (COMMIT_OR_PR.test(e.command)) {
36        const fixed = fixDashes(e.command)
37        if (fixed.count > 0) {
38          total += fixed.count
39          $.ui.status(`dashless: ${total} em dashes fixed`)
40          return next({ ...e, command: fixed.text })
41        }
42      }
43    } catch {}
44    return next(e)
45  })
46
47  on('session.append', ($, e, next) => {
48    try {
49      if (e.door === 'response') {
50        let changed = 0
51        const content = e.message.content.map((block) => {
52          if (block.type !== 'text' || typeof block.text !== 'string') return block
53          const fixed = fixDashes(block.text)
54          changed += fixed.count
55          return fixed.count > 0 ? { ...block, text: fixed.text } : block
56        })
57        if (changed > 0) {
58          total += changed
59          $.ui.status(`dashless: ${total} em dashes fixed`)
60          return next({ ...e, message: { ...e.message, content } })
61        }
62      }
63    } catch {}
64    return next(e)
65  })
66}
67
hooks/fix.ts 15 lines
1// Em dash is written as a unicode escape so this file never contains the character.
2const DASH = /[ \t]*(?:\u2014|(?<=[ \t])\u2013(?=[ \t]))[ \t]*/g
3
4export function fixDashes(text: string): { text: string; count: number } {
5  let count = 0
6  const out = text.replace(DASH, (match, offset: number, whole: string) => {
7    count++
8    const next = whole[offset + match.length]
9    if (offset === 0 || whole[offset - 1] === '\n') return match.match(/^[ \t]*/)![0]
10    if (next === undefined || next === '\n' || next === '\r') return ''
11    return /[,;:.(]/.test(whole[offset - 1] ?? '') ? ' ' : ', '
12  })
13  return count === 0 ? { text, count: 0 } : { text: out, count }
14}
15