Removing the Co-Authored-By Signature in Claude Code
Two ways to remove the 'Co-Authored-By' / 'Generated with' lines Claude Code adds to commits and pull requests: settings.json attribution and a commit-msg hook.
Removing the Co-Authored-By Signature in Claude Code
Commits produced with Claude Code show a line like this by default:
Co-Authored-By: Claude <noreply@anthropic.com>PR bodies get a line like this:
🤖 Generated with [Claude Code](https://claude.com/claude-code)If you don't want this, or you also want to clean the "Generated with Claude Code" line from PR bodies, there are two methods. First check the current state:
git log -1 --format=full | grep -i -E "co-authored-by|generated with"Method 1: settings.json attribution
Add the attribution block to ~/.claude/settings.json as empty strings. That way Claude Code won't add the co-author line to commit and PR text:
{
"attribution": {
"commit": "",
"pr": ""
}
}These fields are separate strings in the official settings reference; emptying one doesn't turn the other off. Don't replace the existing JSON wholesale; merge the attribution object with the other settings. The user setting can be overridden at the project/local/managed layers; seeing the key in the file is not proof of the effective setting. includeCoAuthoredBy is the old name. On supporting versions, attribution.sessionUrl: false is also needed for the cloud/Remote Control session link.
Note: this setting only stops the line Claude Code adds. It doesn't stop signatures added by other tools (Copilot, Gemini CLI, Codex) - those need Method 2.
Check that the JSON parses; this is only a syntax check:
jq -e '.attribution | .commit == "" and .pr == ""' ~/.claude/settings.jsonCopying the Same Key to Other Tools
Claude Code's attribution schema is not a common standard for Codex, Gemini, or Grok. First identify the installed client and version. If you can't document a supported key, don't give the impression it works with guessed settings like [git] commit_footer or append_coauthor. A git hook can be a tool-independent message filter, but it doesn't apply to the PR body or to past commits.
Method 2: Filtering the New Commit Message Narrowly
The Git commit-msg hook receives the file path of the message being created and can edit it. It can be skipped with --no-verify; it doesn't change already-made commits. The filter below removes only the known Claude attribution and session trailer; it doesn't touch human contributors' Co-Authored-By lines.
#!/bin/sh
set -eu
MSG_FILE="$1"
TMP_FILE=$(mktemp "${MSG_FILE}.XXXXXX")
trap 'rm -f "$TMP_FILE"' EXIT HUP INT TERM
awk '
{
lower = tolower($0)
if (lower ~ /^co-authored-by:[[:space:]]*claude[[:space:]]+<noreply@anthropic[.]com>[[:space:]]*$/) next
if (lower ~ /^claude-session:/) next
if ($0 ~ /^Generated with Claude Code[[:space:]]*$/) next
lines[++n] = $0
}
END {
while (n > 0 && lines[n] ~ /^[[:space:]]*$/) n--
for (i = 1; i <= n; i++) print lines[i]
}' "$MSG_FILE" > "$TMP_FILE"
mv "$TMP_FILE" "$MSG_FILE"The filter doesn't remove a "Generated with" phrase appearing inside a prose sentence, or a different human co-author. If your real tool produces a different bot name/attribution format, first inspect the exact line in a message fixture and add a narrow rule. On error the temp file is cleaned and the hook fails; rather than silently continuing with a missing message, it keeps the error visible.
Enable the hook:
git config --global core.hooksPath ~/.git-hooks
chmod +x ~/.git-hooks/commit-msgThis global setting shadows existing repository hook directories. First inspect where the effective value comes from with git config --show-origin --get-all core.hooksPath and don't lose existing checks. For a single repo only, git config core.hooksPath .githooks is more narrowly scoped; Git doesn't automatically enable a cloned hook.
Verify:
git config --global core.hooksPath
# ~/.git-hooksTest the hook - with a fake message file:
cat > /tmp/test-msg <<'EOF'
feat: add new feature
Co-Authored-By: Claude <noreply@anthropic.com>
Generated with Claude Code
EOF
sh ~/.git-hooks/commit-msg /tmp/test-msg
cat /tmp/test-msg
# Only "feat: add new feature" is expected to remainMethod 2.5: PR Body Cleanup
The commit message hook doesn't touch the PR description, because the PR body goes to GitHub, not to git. To clean the line in the PR body, lean on two places:
# 1) Clean the body beforehand when opening the PR with the gh CLI
gh pr create --title "feat: x" --body "$(cat pr-body.md | grep -v 'Generated with Claude Code')"# 2) Update the body of an open PR
gh pr edit 42 --body "$(gh pr view 42 --json body -q .body | grep -v 'Generated with Claude Code')"If you use the GitHub UI you can clean the PR description by hand too. When the squash message is produced server-side, the local commit-msg hook doesn't run; also inspect the PR body and the proposed squash message.
Two Checks to Be Sure of the Filter
Add a real-person co-author and a DCO line to the fixture:
Co-Authored-By: Human Contributor <human@example.com>
Signed-off-by: Human Contributor <human@example.com>These two should be preserved while the Claude line is deleted. Also run the hook twice on the same file and check that the second run doesn't change the content. Finally, see that the hook doesn't hide the error when the file can't be written. The form grep ... && exit 1 || echo OK can show the test command's own error as success too; check the message content and the command's exit status separately.
Which, When?
| Situation | Recommendation |
|---|---|
| Block only the signatures Claude Code produces | Method 1 is enough |
| Mixed multiple tools | Use the documented setting in each tool; add only verified bot lines to the hook |
| The "Generated with" line in the PR body should be cleaned too | Method 2.5 (gh CLI or manual) |
| Cleaning signatures from old commits | Rewrite repo history with git filter-repo; no hook needed |
| Both together | One prevents production, the other sweeps up escapees |
PR Workflow Notes
- Add the hook to the repo template. Version it as
.githooks/commit-msg, and put thegit config core.hooksPath .githooksinstall line in the README. Every developer runs it once on first clone. - Verify in CI. A narrow example for the last commit; if all PR commits are needed, also scan the real base/head range:
- name: No AI trailers
run: |
if git log -1 --format=%B | grep -Eiq '^Co-Authored-By:[[:space:]]*Claude[[:space:]]+<noreply@anthropic[.]com>[[:space:]]*$'; then
echo "AI signature found; the hook may not have run" && exit 1
fi- Watch squash-merge behavior. During a GitHub squash merge the PR body is carried into the commit message; the hook doesn't kick in at that point because the message is produced on GitHub's side. So clean the line in the PR body too.
- Conflict with CLA / DCO bots. Some repos require a
Signed-off-by:line; the hook doesn't delete it because it isn't in the filter. While sweeping your own lines, deliberately preserveSigned-off-by. - Keep the template policy. Don't confuse the desired
Co-Authored-Byguidance for human contributors with AI attribution; change only the unnecessary bot default. - Re-check after rebase. Don't assume the rebase runs
commit-msgon every replayed commit. The Git path and options used can differ; inspect the resulting messages separately.
Cleaning Past Commits
Deleting signatures from already-pushed commits isn't done with a hook; you need to rewrite history:
# install filter-repo
brew install git-filter-repo
# or: pipx install git-filter-repo
# Remove only the known Claude co-author line; preserve human contributors
git filter-repo --message-callback '
return b"\n".join(
line for line in message.split(b"\n")
if not line.lower().startswith(
b"co-authored-by: claude <noreply@anthropic.com>"
)
)
'This changes commit identities and those of descendant commits, can invalidate existing signatures, and affects PR/tag references. Rewriting published shared history just for a presentation preference is usually not worth it. If needed, proceed with team approval, a verified backup, and a separate fresh clone; coordinate the force-push impact with all consumers.
You can also add a pre-push hook to check the last N commits before pushing:
#!/bin/sh
# ~/.git-hooks/pre-push
-if git log origin/main..HEAD --format=%B | grep -Eiq 'co-authored-by|generated with claude'; then
+if git log origin/main..HEAD --format=%B | grep -Eiq '^[[:space:]]*(Co-Authored-By:[[:space:]]*Claude[[:space:]]+<noreply@anthropic[.]com>|Generated with Claude Code([[:space:]]|$)'; then
echo "AI signature in commits to be sent to the remote." >&2
exit 1
fiTroubleshooting
| Symptom | Probable cause | Fix |
|---|---|---|
| Hook doesn't run | core.hooksPath undefined | git config --global core.hooksPath ~/.git-hooks |
| Hook doesn't run | File not executable | chmod +x ~/.git-hooks/commit-msg |
| Line still in the message | --no-verify, a different hook path, or a server-side squash | Inspect the real call path and the produced message; -m doesn't solve these |
sed: -i: invalid | macOS vs GNU sed | Use sed -i '' -e ... or take the mktemp version |
| Encoding broken | 🤖 emoji encoding issue | Write the grep -v filter locale-independently: LC_ALL=C |
To see that the hook really fires, add a temporary echo "hook ran" >> /tmp/hook.log line, make a commit, then check the log.
Cases to Test the Hook
| Input line | Expected result |
|---|---|
Co-Authored-By: Claude <noreply@anthropic.com> | known bot line deleted |
| The same Claude line in lowercase | the filter matches case-insensitively |
Someone else's Co-Authored-By line | preserved |
Claude-Session: abc123 | line deleted |
| The message's only line is the Claude attribution | an empty message remains; don't bypass Git's normal empty-message check |
Signed-off-by: Me <...> | preserved |
| The hook errors | the commit stops; inspect the current message and fix the error |
Don't delete someone else's contribution record just to clean attribution. This filter is not a security boundary that satisfies authorship/organization policy; CI and server-side merge paths need their own checks.
Attribution That Requires Policy
If the project requires disclosing AI assistance, emptying the setting or replacing the bot name with a human name doesn't remove that obligation. Write the wording the team accepts explicitly into attribution.commit and attribution.pr. Signed-off-by is the author's DCO declaration; it doesn't stand in for a tool-usage note. The final decision is made based on the produced commit message and PR description, not on the existence of the settings file.
Related Research & Internal References
- AI CLI Statusline - another Claude Code
settings.jsonsurface worth auditing. - Security Logging Design - keeping CI checks narrow and verifiable.
What do you think?
React to show your appreciation