SLOPSHOPPER

no-coauthor

Strips Co-Authored-By: Claude/Anthropic trailers from git commit messages before the commit runs

newguardtoast
A shopper browsing a rack in a slop shop
README

no-coauthor

Strips Co-Authored-By: Claude / Anthropic trailers from git commit commands before they run.

What it does

  • Intercepts each Bash call that contains a git commit and rewrites the command; the commit itself still runs.
  • Removes trailers naming Claude or Anthropic (case-insensitive) from -m/--message arguments, --trailer arguments, and trailer lines inside multi-line messages or heredocs.
  • Keeps Co-Authored-By: trailers for human co-authors.
  • Shows a toast: no-coauthor: removed N Claude co-author trailer(s) from the commit.
  • Commands without a trailer, or that are not commits, pass through unchanged.

Install

claude --plugin-dir /path/to/ModsTools/mods/no-coauthor

Limits

  • Text-based rewriting of the command line: trailers built through variables, files (-F, --amend without a new message), git commit-tree or scripts are not seen.
  • No command and no on/off switch: remove the plugin to disable it.

Develop

claude plugin validate mods/no-coauthor
claude plugin test mods/no-coauthor   # 21 tests
Source 2 files
hooks/register.ts 14 lines
1import type { Register } from 'claude-code'
2
3import { stripClaudeTrailers } from './rules'
4
5export const register: Register = on => {
6  on('tool.call', { tool: 'Bash' }, ($, e, next) => {
7    const { command, removed } = stripClaudeTrailers(e.command)
8    if (removed === 0) return next(e)
9    $.ui.toast(`no-coauthor: removed ${removed} Claude co-author trailer${removed > 1 ? 's' : ''} from the commit`)
10
11    return next({ ...e, command })
12  })
13}
14
hooks/rules.ts 55 lines
1const IS_COMMIT = /\bgit[ \t]+(?:[^\s;&|]+[ \t]+)*?commit\b/
2const NAMES_CLAUDE = /claude|anthropic/i
3
4// A whole `-m "<trailer>"` / `--trailer "<trailer>"` argument.
5const TRAILER_ARG = /[ \t]+(?:-m|--message|--trailer)(?:[ \t]+|=)(["'])[ \t]*Co-Authored-By:[^"'\n]*["']/gi
6// A trailer line inside a message, with the line break before it.
7const TRAILER_LINE = /\n[ \t]*Co-Authored-By:[^\n"'<]*(?:<[^>\n]*>)?[ \t]*/gi
8
9export const isCommit = (command: string) => IS_COMMIT.test(command)
10
11function heredocEnds(command: string): string[] {
12  return [...command.matchAll(/<<-?[ \t]*(["']?)(\w+)\1/g)].map(m => m[2]!)
13}
14
15// True when nothing of the message follows: a closing quote, the end, or a heredoc terminator line.
16function endsMessage(rest: string, ends: string[]): boolean {
17  if (/^\s*(["']|$)/.test(rest)) return true
18  const next = /^\n[ \t]*(\w+)[ \t]*(\n|\)|$)/.exec(rest)
19
20  return next !== null && ends.includes(next[1]!)
21}
22
23/**
24 * The command without the Claude/Anthropic Co-Authored-By trailers of its
25 * `git commit` messages, and how many were removed. Other co-authors stay.
26 */
27export function stripClaudeTrailers(command: string): { command: string; removed: number } {
28  if (!isCommit(command)) return { command, removed: 0 }
29  let removed = 0
30  let out = command.replace(TRAILER_ARG, arg => {
31    if (!NAMES_CLAUDE.test(arg)) return arg
32    removed++
33
34    return ''
35  })
36
37  const ends = heredocEnds(out)
38  let result = ''
39  let from = 0
40  for (const m of out.matchAll(TRAILER_LINE)) {
41    if (!NAMES_CLAUDE.test(m[0])) continue
42    let start = m.index
43    const end = start + m[0].length
44    if (endsMessage(out.slice(end), ends)) {
45      while (start > from && /\s/.test(out[start - 1]!)) start--
46    }
47    result += out.slice(from, start)
48    from = end
49    removed++
50  }
51  out = result + out.slice(from)
52
53  return { command: out, removed }
54}
55