@aggc/no-coauthor
v2.5.0
Published
Git hook that strips AI co-author trailers (Claude, Copilot, Codex, Cursor, Oz/Warp, GPT, Gemini, Devin, etc.) from commit messages while preserving human co-authors
Maintainers
Readme
no-coauthor
A commit-msg git hook that strips AI Co-Authored-By: trailers from commit
messages — while preserving human co-authors and every other trailer
(Signed-off-by, Refs, Closes, …).
It runs inside git, not inside the AI tool, so it catches the trailer regardless of which tool added it or whether that tool's own "disable attribution" setting actually works.
Fork of
0xdsgnrd/no-coAuthor— full credit to the original author for the concept and initial implementation. This fork adds email-based detection (not just names),core.hooksPathsupport, non-destructive install/uninstall, a POSIX shell fallback for machines without Node.js, a user config file, an automated test suite, and cross-platform CI. Published on npm as@aggc/no-coauthor; the installed hook and CLI command are still calledno-coauthor.
Quick start
npx @aggc/no-coauthor installThat's it — every commit in this repo now has AI Co-Authored-By: trailers
stripped automatically, human co-authors untouched. See
Install for the global, curl, and no-Node.js variants.
Table of contents
- Quick start
- Why
- How it decides what to strip
- Covered tools
- Install
- Uninstall
- How install works (non-destructive)
- Configuration
- Temporarily disabling
- Server-side enforcement
- CLI reference
- Architecture
- Cross-platform
- Limitations
- Contributing
- Releasing
- Credits
- License
Why
AI coding assistants (Claude Code, Copilot, Cursor, Codex, Gemini, Oz/Warp, …)
inject Co-Authored-By: trailers into commits. Some tools expose a setting to
disable this, but those settings are unreliable in practice: Claude Code's
attribution setting is intermittently ignored, Cursor has re-enabled it
across updates, and tools like Copilot Agent or Gemini Code Assist offer no
setting at all.
no-coauthor is a git-level safety net that works regardless of the tool,
its version, or its settings. Use it together with your tool's built-in
setting when one exists — prevention at the source plus enforcement at the
commit boundary. Belt and suspenders.
How it decides what to strip
A Co-Authored-By: Name <email> line is removed when any of these rules
match (all matching is case-insensitive and anchored to the trailer line):
| Rule | Condition | Example |
|---|---|---|
| A. Bot address | The <email> matches a known bot address/domain ([email protected], [email protected], [email protected], …[email protected], …). High confidence — no human co-author uses these. | Co-Authored-By: Claude <[email protected]> |
| B. Name + bot-shaped email | The name matches a known AI tool name and the email is bot-shaped (noreply, users.noreply.github.com, [bot]). | Co-Authored-By: gemini-code-assist[bot] <176…@users.noreply.github.com> |
| C. Name + tool domain | The name matches a known AI tool name and the email domain is a known AI-tool domain (anthropic.com, cursor.com, warp.dev, …). Catches non-noreply bot addresses. | Co-Authored-By: Oz <[email protected]> |
A name match alone never strips a line. This is what keeps a human literally named "Claude", "Cody", or "Devin" intact:
Co-Authored-By: Claude <[email protected]> → stripped (A, B)
Co-Authored-By: Oz <[email protected]> → stripped (A, C)
Co-Authored-By: Claude Opus 4.6 <[email protected]> → stripped (A, B — model-suffixed names too)
Co-Authored-By: Claude Smith <[email protected]> → KEPT (human, unrelated email)
Co-Authored-By: Jane <[email protected]> → KEPT (same domain, non-AI name)
Co-Authored-By: Pat <[email protected]> → KEPT (google.com deliberately excluded, see Limitations)Everything else in the commit message — Signed-off-by:, Refs:, Closes:,
free text, other trailers — is left untouched. Removal is line-based and
collapses any blank line left behind so the trailer block stays valid for
git's trailer parser.
Separately, a small set of known AI banner/footer lines — plain body lines some tools insert that aren't trailers at all — are stripped the same way. Today this is Claude Code's default commit-body attribution line, which covers the confirmed variants across its CLI versions/docs (emoji prefix optional, link optional, markdown or plain-parenthetical link):
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Generated with [Claude Code](https://claude.ai/code)
🤖 Generated with Claude Code
Generated with Claude Code (https://claude.com/claude-code)Of the other major tools surveyed (Copilot, Cursor, Codex CLI, Gemini CLI,
Aider, Devin, Amazon Q Developer, Windsurf), none currently ship a
standardized commit-body banner like this — they either only use a
Co-Authored-By: trailer (already covered above) or have no attribution
convention at all. Only exact, confirmed formats are matched (see
AI_BANNER_LINES in lib/patterns.js), so a commit message that merely
mentions a tool by name in its own body text is left untouched.
Covered tools
| Tool | Detected via |
|---|---|
| Claude / Claude Code (Anthropic) | [email protected], name match |
| GitHub Copilot | [email protected], *copilot*@users.noreply.github.com, [bot] forms |
| OpenAI Codex | [email protected], [email protected], GitHub noreply form |
| ChatGPT / GPT (OpenAI) | @chatgpt.com, name match |
| Cursor (incl. Cursor Agent / background agents) | [email protected]/.sh, [email protected] |
| Gemini / Bard / Gemini Code Assist (Google) | gemini*@google.com, bard*@google.com, [bot] forms |
| Oz (Warp) | [email protected] |
| Codeium / Windsurf | [email protected], [email protected] |
| Tabnine | [email protected] |
| Amazon Q / CodeWhisperer | name match (amazon-q, amazonq, CodeWhisperer) |
| Aider | [email protected]/.ai |
| Zed AI | [email protected] |
| Cody (Sourcegraph) | [email protected] |
| Devin (Cognition) | [email protected]/.dev |
| Augment Code | [email protected] |
| Replit Agent / Ghostwriter | [email protected] |
| Cline, Continue, Llama, Tabby, Bolt, v0, Lovable, Goose, OpenHands, Plandex, Qoder, Jules | name match |
Also stripped as a standalone body line (not a trailer): Claude Code's
Generated with Claude Code attribution banner, in its confirmed variants
(with/without emoji, with/without a link) — see How it decides what to
strip and AI_BANNER_LINES in
lib/patterns.js.
This list is best-effort and evolves as tools change their default trailers.
Missing one? Add it locally via config — no code change
needed — or open a PR against lib/patterns.js.
Install
npm (recommended — Node.js hook with config-file support)
# Per-project (inside a git repo)
npx @aggc/no-coauthor install
# Every repo on this machine
npx @aggc/no-coauthor install --global--global sets git's own core.hooksPath globally, so it affects every
repo on the machine that doesn't already set its own local
core.hooksPath — not just repos you plan to use it with. If you have
older per-project hooks (e.g. a husky v4-style setup that writes straight
into .git/hooks instead of setting core.hooksPath), they'll stop running
silently. Run npx @aggc/no-coauthor status in any repo you're unsure about
to check.
curl (no Node.js required — POSIX shell hook)
# Per-project
curl -fsSL https://raw.githubusercontent.com/jmtrs/no-coAuthor/main/install.sh | sh
# Global
curl -fsSL https://raw.githubusercontent.com/jmtrs/no-coAuthor/main/install.sh | sh -s -- --globalPOSIX shell hook via npm
npx @aggc/no-coauthor install --no-nodeHomebrew (no Node.js required)
brew install jmtrs/tap/no-coauthor
no-coauthor install # per-project
no-coauthor install --global # every repo on this machineSame POSIX shell hook as the curl installer above — jmtrs/homebrew-tap
just packages install.sh under the no-coauthor command name. The formula
is updated automatically on every release (see .github/workflows/release.yml).
pre-commit framework
Already using pre-commit? Add this repo to your
.pre-commit-config.yaml instead of running install — pre-commit manages
the commit-msg hook file for you:
repos:
- repo: https://github.com/jmtrs/no-coAuthor
rev: v2.4.0 # use the latest tag: https://github.com/jmtrs/no-coAuthor/tags
hooks:
- id: no-coauthorThen:
pre-commit install --hook-type commit-msgThis runs no-coauthor commit-msg <file> under pre-commit's own Node
environment instead of through no-coauthor install, so it does not get
the wrapper/preserve-foreign-hook handling lib/install.js does for a direct
install — pre-commit is already managing the hook file and any other
commit-msg hooks you've configured through it. Everything else (which
trailers/banners get stripped, .no-coauthorrc.json, NO_COAUTHOR_DISABLE)
behaves the same either way.
Uninstall
npx @aggc/no-coauthor uninstall # per-project
npx @aggc/no-coauthor uninstall --global # globalHow install works (non-destructive)
- No existing
commit-msghook → writes ours (standalone). - Existing foreign hook → preserved as
commit-msg.origand replaced with a small shellcommit-msgwrapper that runs the previous hook then no-coauthor. Your existing hook keeps working unmodified. - Already installed → updated in place (idempotent — safe to re-run).
core.hooksPath-aware — if the repo sets a localcore.hooksPath(e.g..githooks), the hook is installed there, not in.git/hooks(which git would otherwise ignore). If a globalcore.hooksPathwould shadow a per-project install, you get a warning with the fix.- Worktree- and submodule-safe —
install/uninstall/statusresolve the hooks directory throughgit rev-parse --git-common-dirrather than assuming<repo>/.gitis a directory (it's a text file pointing elsewhere in both). Installing from a linked worktree writes to the main repo's shared hooks dir — the same place git itself reads hooks from for every worktree — so one install covers all of them.
Uninstall restores the preserved foreign hook if one exists, otherwise removes the no-coauthor hook cleanly.
Run npx @aggc/no-coauthor status any time to confirm the hook is still
there and actually stripping trailers — see CLI reference.
Configuration
Optional .no-coauthorrc.json adds custom patterns on top of the built-ins.
Entries are treated as literal strings and escaped automatically, so no
regex knowledge is required:
{
"names": ["MyAgent"],
"emails": ["[email protected]"],
"domains": ["myteam.ai"],
"banners": ["Generated by MyAgent v2"]
}The Node.js hook and the check command read this at runtime. There are two
locations, with different trust levels:
~/.no-coauthorrc.json— your own machine, always honored../.no-coauthorrc.json(repo root) — ignored by default. A cloned repo could ship a config that silently strips real human co-author attribution, which is exactly what this tool exists to prevent. To honor a repo-root config you control (e.g. a team-shared one in CI), opt in with:
export NO_COAUTHOR_TRUST_REPO=1The POSIX fallback hook does not read either file — it is fully self-contained by design, with no filesystem reads beyond the commit message itself.
Temporarily disabling
Two ways to skip stripping, for different scopes:
# Skip every hook for one commit (git's own escape hatch) — also skips any
# OTHER hook this may be wrapping (see [How install works](#how-install-works-non-destructive)).
git commit --no-verify -m "..."
# Skip only no-coauthor for one commit — any wrapped hook still runs.
NO_COAUTHOR_DISABLE=1 git commit -m "..."
# Skip only no-coauthor for a whole shell session.
export NO_COAUTHOR_DISABLE=1NO_COAUTHOR_DISABLE is checked first thing in both the Node and POSIX
hooks, before any matching happens. Prefer it over --no-verify whenever a
foreign hook is wrapped and you still want that one to run.
Server-side enforcement
The commit-msg hook is a client-side, opt-in safety net, not a security
boundary. Anything running on the committer's own machine — including an
AI agent with shell access — can skip it: --no-verify, NO_COAUTHOR_DISABLE=1,
or just never running install in the first place. No local git hook can be
made bypass-proof against the same machine it runs on; that's true of this
tool and of every other commit-msg hook (husky, pre-commit, lint-staged, …)
equally, not a no-coauthor-specific weakness.
If your threat model includes an agent that might deliberately re-add attribution rather than just doing it by unreliable default, the hook alone isn't enough — you need a check that runs somewhere the committer doesn't control: CI, as a required PR status check.
npx @aggc/no-coauthor check <range> # exit 1 if any commit in <range> has an AI trailer
npx @aggc/no-coauthor check # defaults to HEAD~1..HEADcheck reuses the exact same detection logic as the hooks (same
lib/patterns.js, same .no-coauthorrc.json), so it can never flag
something the hook wouldn't also have stripped, or vice versa. Copy
examples/reject-ai-coauthor.yml into
your repo's .github/workflows/ — it computes the commit range from the
PR's base/head SHAs, so it works the same regardless of which branch a PR
targets or is opened from, not just main.
A failing check does not block a merge by itself — add a branch protection rule (or ruleset) on your target branch(es) requiring that job to pass, and restrict who can bypass required checks. That combination — hook for the common case, required CI check for the case where someone tries to route around it — is what actually can't be defeated from a local shell.
CLI reference
$ npx @aggc/no-coauthor <command> [options]
Commands
install Install hook in current repo
install --global Install as global git hook
install --no-node Install POSIX shell hook (no Node.js needed)
uninstall Remove hook from current repo
uninstall --global Remove global git hook
status Check the hook is installed and actually stripping trailers
status --global Check the global hook instead of the local one
check [range] Scan already-made commits for AI trailers (default: HEAD~1..HEAD)
For CI use — see [Server-side enforcement](#server-side-enforcement)
commit-msg <file> Strip trailers/banners from a commit message file in place.
Internal: invoked by pre-commit, not meant to be run by hand
— see [pre-commit framework](#install)
Options
--no-node Use POSIX shell hook instead of Node.js
-h, --help Show help
-v, --version Show versionstatus exists because this tool's whole job is to run invisibly on every
commit — if something else later overwrites .git/hooks/commit-msg (a hook
manager like husky/lefthook reinstalling, a manual edit, a fresh git init
template), no-coauthor silently stops working with zero visible signal,
which is exactly the failure mode it exists to prevent. status checks the
hook file is present, is ours, is executable, and then actually runs it
against a synthetic AI trailer to confirm it still strips correctly — not
just that a plausible-looking file exists:
$ npx @aggc/no-coauthor status
no-coauthor: checking local hook at /path/to/repo/.git/hooks
✔ commit-msg hook is installed and managed by no-coauthor
✔ /path/to/repo/.git/hooks/commit-msg is executable
✔ live check: a synthetic AI trailer was stripped and a human trailer was keptArchitecture
bin/no-coauthor.js CLI entry point (install / uninstall / --help / --version)
lib/git-utils.js Shared git plumbing (gitConfig, gitRoot, gitCommonDir) used
by install/uninstall/status to resolve the hooks dir —
worktree/submodule-safe (see How install works above)
lib/install.js Non-destructive install: standalone, wrap, or update-in-place
lib/uninstall.js Restores a preserved foreign hook, or removes ours cleanly
lib/status.js Checks the hook is installed, managed, executable, and
actually strips a synthetic AI trailer right now
lib/check.js Server-side companion: scans a git revision range of
already-made commits for AI trailers (reuses strip.js)
lib/commit-msg.js Runtime commit-msg entry point for `no-coauthor commit-msg
<file>` — what .pre-commit-hooks.yaml shells out to
(reuses strip.js, unlike the generated hooks below)
lib/hook.js Builds the self-contained Node.js commit-msg hook that gets
written to disk (inlines strip.js + patterns.js — no
runtime dependency on this package once installed)
lib/hook-posix.js Builds the POSIX /bin/sh commit-msg hook from the same
patterns.js source (JS regex fragments transliterated
to POSIX ERE character classes)
lib/patterns.js Single source of truth for bot emails, tool domains,
and AI tool names — both hooks are generated from this
lib/strip.js Core matching/stripping logic (rules A/B/C), shared by
the Node hook builder and the test suite
install.sh Standalone POSIX installer for the curl one-liner;
embeds a copy of the hook-posix.js output
scripts/sync-install-sh.js Regenerates that embedded copy after a
lib/patterns.js change (`npm run sync-install-sh`)
examples/reject-ai-coauthor.yml Copy-paste GitHub Actions workflow wired to
`no-coauthor check` — see Server-side enforcement above
.pre-commit-hooks.yaml Exposes this repo as a pre-commit `commit-msg`-stage
hook — see pre-commit framework under Install aboveBoth hook implementations are generated from lib/patterns.js, so a tool
added there is picked up by the Node hook, the POSIX hook, and (after
regeneration) install.sh alike. test/install.test.js asserts the
install.sh embedded hook body stays byte-identical to lib/hook-posix.js's
output, so the two can't silently drift.
Cross-platform
- Node.js hook (default) — Linux, macOS, and Windows (Git for Windows / Git Bash).
- POSIX shell hook (fallback) — anywhere with
/bin/sh,grep -E, andawk; no Node.js required.
CI (.github/workflows/ci.yml) runs the full test suite on Ubuntu, macOS,
and Windows against Node 18 / 20 / 22, plus a dedicated end-to-end job that
installs the POSIX hook with real sh/grep/awk and performs an actual
git commit.
Limitations
status's live check only exercises the built-in patterns (a synthetic Claude/Anthropic trailer) — it does not know about your.no-coauthorrc.jsoncustom patterns, so a workingstatusdoesn't guarantee a misconfigured custom pattern is actually matching.- Lines longer than 500 chars are never treated as a
Co-Authored-Bytrailer and are left untouched (no real one gets remotely close — this bound exists to keep the Node hook's regex matching from doing quadratic-time work on an adversarial or buggy-tool-generated giant line). - The POSIX fallback is best-effort: it compiles all patterns into one
combined ERE and does not read the config file. Prefer the Node hook when
possible; use
--no-nodeonly when Node.js is unavailable. google.comis intentionally not listed as an AI-tool domain (rule C) — it's too broad and would risk stripping real Google employees. Gemini/Bard bot accounts are still caught by their specific addresses (rule A).- A human co-author who shares a tool-company domain and happens to have an AI tool name (e.g. an Anthropic employee literally named "Claude") would be stripped by rule C. This is an accepted, rare trade-off — narrow it with config if it matters to you.
- Detection is pattern-based against known trailer shapes. A tool that starts emitting a previously-unseen bot address/domain won't be caught until patterns.js is updated (or you add it via config).
- Banner/footer-line stripping is intentionally narrow: only Claude Code's
confirmed default formats are built in today (a survey of Copilot, Cursor,
Codex CLI, Gemini CLI, Aider, Devin, Amazon Q Developer, and Windsurf found
no other standardized commit-body banner as of this writing — only
Co-Authored-By:trailers, already covered above, or no attribution convention at all). Claude Code's ownattribution.commitsetting also lets a user/team override this text to anything, which by definition no fixed pattern can catch — add a custom banner viabannersin config, or open a PR againstAI_BANNER_LINESif another tool starts shipping its own standardized one.
Contributing
Bug reports and new tool patterns are welcome.
npm test # full suite: strip logic, install/uninstall, POSIX hook, sh -n
npm run sync-install-sh # regenerate install.sh after touching lib/patterns.js
npx changeset # record a changelog entry for your change (patch/minor/major + summary)If you add or change a pattern in lib/patterns.js:
- Add a
strip.test.jscase for both the strip and the preserve side. - Run
npm run sync-install-shso the curl installer matches — CI fails the build otherwise (test/install.test.jschecks they're byte-identical). test/posix-parity.test.jsautomatically checks your new pattern strips correctly under the POSIX hook too; no extra test needed for that part.npx changesetbefore opening the PR — merging tomainwon't publish anything by itself, but a maintainer merging the resulting "Version Packages" PR will (see Releasing).
Releasing
This repo publishes via Changesets:
every push to main with a pending changeset opens/updates a "Version
Packages" PR; merging that PR bumps the version, updates CHANGELOG.md, and
publishes to npm automatically (.github/workflows/release.yml).
Credits
Originally created by 0xdsgnrd —
0xdsgnrd/no-coAuthor. This fork
builds on that work; see the table of contents above
for everything added on top.
License
MIT — see LICENSE.
