Keeps the Claude co-author trailer and the Generated-with footer out of your commits and pull requests: the model is told not to write them, and any that slip…

Claude Code adds Co-Authored-By: Claude ... <noreply@anthropic.com> to commits it writes and a Generated with Claude Code footer to pull requests it opens. If you want your commits to carry only your name, this mod keeps both out.
Two layers, because telling the model is advice and a command is a fact:
git commit or gh pr create / gh pr edit command runs, the mod deletes any Claude co-author line, any Anthropic noreply co-author line, and the Generated-with footer from the command, in Bash and PowerShell alike. It does this silently.A co-author you name yourself (Co-Authored-By: Ada Lovelace <ada@example.com>) is never touched. Only commit and pull request commands are looked at, so git log --grep or an echo is left alone.
/trailers off lets Claude write its trailers as usual, for a repository where you want them or a one-off. /trailers on brings the mod back; /trailers shows which it is. The setting is saved across sessions and applies to every project.
attribution.text hook, does not run in a plugin you install where an organization manages Claude Code; there the second layer still removes the lines from the command.git log -1./plugin install no-ai-trailers --marketplace 0langa/claude-code-modshooks/register.ts 50 lines1import type { EngineInterface, Register } from 'claude-code'
2
3import { stripAttribution } from './strip'
4
5const isEnabled = async ($: EngineInterface) => (await $.store.get('enabled')) !== false
6
7export const register: Register = on => {
8 on('session.start', async ($, e, next) => {
9 await $.command.register({
10 name: 'trailers',
11 description: 'Claude co-author trailers in commits and PRs: on (removed) or off (left as Claude writes them)',
12 argumentHint: '[on|off]',
13 immediate: true,
14 })
15
16 return next(e)
17 })
18
19 on('command.run', { command: 'trailers' }, async ($, e) => {
20 const arg = e.args.trim().toLowerCase()
21
22 if (arg === 'on' || arg === 'off') {
23 await $.store.set('enabled', arg === 'on')
24 }
25 const state = (await isEnabled($)) ? 'on: Claude trailers are kept out of commits and pull requests' : 'off: Claude writes its trailers as usual'
26
27 return { text: `no-ai-trailers is ${state}. Use /trailers on or /trailers off.` }
28 })
29
30 // Layer one: the model is not told to write the trailer or the footer at all.
31 on('attribution.text', async ($, e, next) =>
32 (e.kind === 'commit' || e.kind === 'pr') && (await isEnabled($)) ? { text: '' } : next(e),
33 )
34
35 // Layer two: if one is written anyway (an instruction from somewhere else, a
36 // habit), it is removed from the command before it runs.
37 on('tool.call', { tool: ['Bash', 'PowerShell'] }, async ($, e, next) => {
38 if (!(await isEnabled($))) {
39 return next(e)
40 }
41 const { command, removed } = stripAttribution(e.command)
42
43 if (removed === 0) {
44 return next(e)
45 }
46
47 return next({ ...e, command })
48 })
49}
50hooks/strip.ts 33 lines1// Removes Claude's attribution lines from a git commit or gh pr command.
2// A co-author you name yourself (a person's name and email) is never touched.
3
4// "Co-Authored-By: Claude ... <noreply@anthropic.com>", whatever the model name.
5const TRAILER_LINE = /^[ \t]*Co-Authored-By:[ \t]*(?:Claude[^\n]*|[^\n]*noreply@anthropic\.com[^\n]*)\r?\n?/gim
6
7// The same as a whole -m argument: git commit -m "msg" -m "Co-Authored-By: Claude ..."
8const TRAILER_ARGUMENT = /[ \t]+(?:-m|--message)(?:[ \t]+|=)(["'])[ \t]*Co-Authored-By:[ \t]*(?:Claude[^"'\n]*|[^"'\n]*noreply@anthropic\.com[^"'\n]*)\1/gi
9
10// "🤖 Generated with [Claude Code](https://claude.com/claude-code)"
11const FOOTER_LINE = /^[ \t]*(?:🤖[ \t]*)?Generated with \[?Claude Code\]?(?:\([^)\n]*\))?[ \t]*\r?\n?/gim
12
13// Only a command that commits or opens or edits a pull request.
14const IS_GIT_TEXT = /\bgit\b[^\n]*\bcommit\b|\bgh[ \t]+pr[ \t]+(?:create|edit)\b/
15
16export type Stripped = { command: string; removed: number }
17
18export const stripAttribution = (command: string): Stripped => {
19 if (!IS_GIT_TEXT.test(command)) {
20 return { command, removed: 0 }
21 }
22 let removed = 0
23 const drop = () => {
24 removed += 1
25
26 return ''
27 }
28 // The argument form first: the line form would otherwise eat its quote.
29 const stripped = command.replace(TRAILER_ARGUMENT, drop).replace(TRAILER_LINE, drop).replace(FOOTER_LINE, drop)
30
31 return { command: stripped, removed }
32}
33