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

Strips Co-Authored-By: Claude / Anthropic trailers from git commit commands before they run.
Bash call that contains a git commit and rewrites the command; the commit itself still runs.-m/--message arguments, --trailer arguments, and trailer lines inside multi-line messages or heredocs.Co-Authored-By: trailers for human co-authors.no-coauthor: removed N Claude co-author trailer(s) from the commit.claude --plugin-dir /path/to/ModsTools/mods/no-coauthor
-F, --amend without a new message), git commit-tree or scripts are not seen.claude plugin validate mods/no-coauthor
claude plugin test mods/no-coauthor # 21 testshooks/register.ts 14 lines1import 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}
14hooks/rules.ts 55 lines1const 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