SSD Nodes Learn Hosting plans →
Guides Matt ConnorBy Matt Connor

What Claude Code writes into your commits

Claude Code adds a Co-authored-by trailer to commits, and cloud sessions add a claude.ai link. Read what is there, then control it before you push.

What Claude Code puts in a commit

Claude Code adds a Co-authored-by: trailer to the end of the commit messages it writes, and an attribution line to the pull request descriptions it opens. Sessions running in the cloud or through Remote Control also append a link to the session on claude.ai. All of it is plain text stored inside your git history. Once you push it to a public repository it is public, and it stays there until somebody rewrites the history.

The exact wording has changed between releases, so do not trust a copy of the trailer printed in any guide, including this one. Read your own commits instead. The git commands below are the durable half of this topic: git's trailer handling has worked the same way for years, and it will keep working that way after the next release changes the text.

What a git trailer actually is

A trailer is a line shaped like Token: value in the last block of a commit message. Git ships no fixed list of tokens. Signed-off-by:, Reviewed-by:, Fixes: and Co-authored-by: are conventions built on one mechanism, and a forge, meaning the site that hosts the repository such as GitHub or GitLab, reads them to decide what to show on a commit page.

Git is strict about where that block sits. The documentation for git interpret-trailers says the group must be preceded by one or more empty lines, must sit at the end of the message or immediately before a line starting with ---, and must either be all trailers or "contain at least one Git-generated or user-configured trailer and consists of at least 25% trailers".

That last rule matters here. A bare URL or a line of prose inside the final block is not a trailer, and enough non-trailer lines stop the whole block from parsing as trailers. This is why a commit can look like it carries a Co-authored-by: line while every tool that reads trailers properly sees nothing.

How do I read the trailers already in my history?

Start with the raw message of the most recent commit.

git log -1 --format=%B

%B prints the subject and body exactly as stored, with no wrapping and no reformatting. That output is the ground truth. Anything a forge shows you is a rendering of it.

Now ask git which of those lines it counts as trailers.

git log -1 --format=%B | git interpret-trailers --parse

--parse is shorthand for --only-trailers --only-input --unfold, so the output is the trailer block and nothing else. A healthy result is one line per trailer. Empty output while you can plainly see a Co-authored-by: line means the block failed the placement rules above.

To scan the whole history, ask for the trailer by key.

git log --format='%h %(trailers:key=Co-authored-by,valueonly)'

Older git versions do not support the key= option on %(trailers). A search over the message text works everywhere.

git log -i --grep='^Co-authored-by:' --format='%h %an %s'

--grep matches the commit message and -i makes the match case-insensitive, which matters because the capitalisation of this trailer has not been consistent across tools. Before a push, narrow the question to what you have not sent yet.

git log origin/main..HEAD --format=%B

Those commits are still local, which means you can still change them cheaply.

What does Co-authored-by: do to attribution on GitHub?

GitHub reads the trailer and shows a second author on the commit page. It links that author to a profile only when the email address belongs to an account. GitHub's own documentation says commits appear on a contributions graph when they are "made with an email address that is connected to your account on GitHub", so an address that belongs to nobody cannot be attached to a profile. For a human pair that is the whole point: your colleague's address is on their account, and the commit counts for both of you. For an address no account owns, the trailer changes what the commit page displays and leaves the repository's contributor list alone.

Git itself ignores the trailer completely. git shortlog -sn and git log --author read the author header, which holds your name and your email, so no local count will ever show the co-author. Attribution here is a forge feature layered on a plain text convention, which is the split between git itself and the forge you host it on.

A trailer names a co-author. A session URL is a pointer to a transcript. The Claude Code settings reference documents attribution.sessionUrl as the key that omits "the claude.ai session link from cloud and Remote Control commits", which also tells you where the link comes from: sessions on the web and sessions driven through Remote Control.

The link is not a credential. Whether anyone can open that session is decided by account access, not by the URL staying unknown. The reason to keep it out of a public repository is simpler. It is permanent public text naming an internal session id, and it points at a working transcript that was never written for an audience. On a private repository the opposite argument holds, because a reviewer can follow the link and read how the change was reached. Where those transcripts live, and how long they survive, is covered in how Claude Code stores sessions and resumes them.

The settings that control Claude Code commit attribution

Checked against the Claude Code settings reference on 1 September 2026, these keys are documented under the heading "Git and attribution":

  • attribution: "Customize the attribution Claude Code adds to commits and pull requests"
  • attribution.commit: "Change or hide the trailer Claude Code adds to commits"
  • attribution.pr: "Change or hide the attribution line in pull request descriptions"
  • attribution.sessionUrl: "Omit the claude.ai session link from cloud and Remote Control commits"
  • includeGitInstructions: "Remove the built-in commit and PR instructions from the system prompt"
  • includeCoAuthoredBy: marked deprecated, with the note "use attribution to hide or change commit and PR attribution"

Take the value each key accepts from its own entry on the settings reference rather than from a guide. Key names are stable. Accepted values and defaults are the part that moves, and a settings file with the key spelled right and the value spelled wrong fails quietly.

Where you write it decides who gets it. ~/.claude/settings.json applies to every project you open. A .claude/settings.json committed at the top of the repository reaches everyone who clones it. .claude/settings.local.json is yours in that one project, and Claude Code adds it to your global git excludes the first time it writes the file, so it stays out of your commits. Precedence runs managed settings first, then the command line, then project local, then shared project, then user. A teammate's local file therefore wins over the file you committed, so treat a committed setting as a default and not as a guarantee.

Then verify it, because a setting you believe in is not evidence. Let Claude Code make the next commit the way it normally does, then read the result back.

git log -1 --format=%B
git log -1 --format=%B | git interpret-trailers --parse

The trailer still printing means the change never reached the session. Check which file you edited, and check the precedence order above, before you decide the key is broken.

The controls that do not depend on a setting

A setting configures one tool. A repository rule has to survive a contributor who runs a different tool or no agent at all. Git gives you a place to put that rule: the commit-msg hook runs with the path to the message file as its first argument, and it can edit that file or reject the commit outright.

#!/bin/sh
# .githooks/commit-msg
grep -qi '^co-authored-by: claude' "$1" || exit 0
grep -vi '^co-authored-by: claude' "$1" > "$1.new" && mv "$1.new" "$1"
chmod +x .githooks/commit-msg
git config core.hooksPath .githooks

The first grep exits early when there is nothing to do, so an ordinary commit pays almost nothing. The second writes the message back without the matching line. Match one exact token rather than every trailer, because a filter written as "drop the last block" will also drop a Signed-off-by: line that a project requires.

.git/hooks is not part of the repository, so a hook you drop in there never reaches anyone else. core.hooksPath points git at a directory you can commit, and each person still runs that one git config line themselves. Git will not set it for them, which is deliberate: a repository that could install its own executables on clone would be a way to run code on your machine.

To refuse the commit instead of rewriting it, print a message to standard error and exit 1 from the hook. Refusing is the honest choice on a shared repository, since silently editing somebody's commit message hides the rule instead of teaching it. This is a different layer from the agent's own hooks, which fire on a tool call before git is involved at all, and how a Claude Code hook matches a tool and blocks it covers that side.

Neither of those helps with a pull request from a fork, because the hook lives on a machine you do not control. A check in CI (continuous integration) is the only layer that sees every commit before it merges.

if git log --format=%B "origin/${BASE_BRANCH:-main}..HEAD" | grep -qi '^co-authored-by: claude'; then
  echo "Attribution trailer found. Rewrite the branch before merging." >&2
  exit 1
fi

Write the rule down as well as enforcing it. An AGENTS.md sitting next to a human-readable CONTRIBUTING.md tells the agent and the person the same thing, and the CI check is what makes it true.

Removing the trailer before you push

For the commit you just made:

git log -1 --format=%B | grep -vi '^co-authored-by: claude' | git commit --amend -F -

-F - reads the new message from standard input. Git strips leading and trailing empty lines from a message supplied that way, so the blank line left behind by the deleted trailer disappears on its own. Read the result back with git log -1 --format=%B before you move on.

For several commits on a branch, run an interactive rebase against the branch you will merge into, and mark each message you want to change as reword.

git rebase -i origin/main

Every commit from the earliest edited one onward gets a new hash, because a commit's hash covers its message and its parent. That is free while the commits are local, and expensive once they are not.

Removing the trailer after you have pushed

On a branch only you use, rewrite it as above and then push over it.

git push --force-with-lease

--force-with-lease refuses the push if the remote moved since your last fetch, so it cannot silently discard a commit somebody else added. Plain --force has no such check.

For a trailer spread across a long history, git filter-repo rewrites every message in one pass. Run the clone from inside your existing working copy, so the URL comes from the remote you already have.

pip install git-filter-repo
git clone "$(git remote get-url origin)" ../project-rewrite
cd ../project-rewrite
git filter-repo --message-callback '
return b"\n".join(l for l in message.split(b"\n") if not l.lower().startswith(b"co-authored-by: claude"))
'

The callback receives each message as bytes and returns the message to store, which is why every string in it carries a b prefix. filter-repo refuses to run on a repository that is not a fresh clone unless you pass --force, and it removes the origin remote when it finishes so you cannot push the rewrite back by accident. Add the remote again deliberately, force-push, and then tell everyone to re-clone, because every hash they hold is now wrong.

Be clear about what a rewrite cannot do. It changes your copy of the repository. It does not reach forks, existing clones, mirrors, pull request pages that already quoted the message, or a code search index that crawled you last week. For a leaked secret the fix is rotating the secret and the rewrite is cleanup. A disclosure trailer is not a secret, so weigh a full-history rewrite against what it costs, which is every open pull request needing to be rebuilt.

What the reviewer on the other side expects

Maintainers do not agree on this, and that disagreement is the real reason to check before you strip anything. Some projects want the disclosure and will ask you to put it back, because a reviewer reads a machine-written patch with different eyes. Some forbid it, often over the question of who legally authored the change. Projects using a DCO (developer certificate of origin) require a Signed-off-by: line, which is a statement you are making about your right to submit the code, and a careless message filter removes that along with the attribution.

Read CONTRIBUTING.md first. When the project says nothing, pick one rule for the repository and write it down, because a trailer that appears on half the commits is worse than either answer: it makes one history look like two projects.

If you run the agent on a server instead of your laptop, the same question returns one layer down. Running Claude Code on a VPS under its own account decides which repositories it can reach at all, and what a coding agent sends off the machine covers the traffic that never shows up in a commit message.

FAQ

Does a Co-authored-by trailer for Claude count in my repository's contributor stats?

No. GitHub attaches a commit to a profile through an email address connected to a GitHub account, and it counts contributions on that basis. An address that belongs to no account cannot be linked, so the trailer changes the commit page and leaves the contributor list unchanged. Git itself never reads trailers for this. git shortlog -sn counts the author header, so the co-author never appears there.

How do I find every commit that already carries the trailer?

git log -i --grep='^Co-authored-by:' --format='%h %an %s' lists them across the whole history, case-insensitively. On a recent git, git log --format='%h %(trailers:key=Co-authored-by,valueonly)' reads the value through git's own trailer parser instead of matching raw text. To see only what you have not pushed yet, add a range: git log origin/main..HEAD --format=%B.

Can I remove the trailer from commits I already pushed?

Yes, by rewriting history, and the cost is real. On a branch only you use, amend or rebase, then git push --force-with-lease. On a shared branch, every commit from the earliest edited one onward gets a new hash, so every clone and every open pull request has to be rebuilt. A rewrite also never reaches forks, mirrors, or a clone somebody made yesterday.

It is not a credential, and access to the session is decided by the account rather than by the URL being hard to guess. The problem is that it is permanent public text pointing at a transcript written as working notes. The Claude Code settings reference documents attribution.sessionUrl as the key that omits that link from cloud and Remote Control commits, and a commit-msg hook strips it from anything the setting does not cover.

Should I turn the attribution off at all?

That depends on the repository rather than on taste. On a public project, follow CONTRIBUTING.md, since a maintainer who wants the disclosure will ask for it back. On a company repository, keeping the trailer is often useful, because it tells a reviewer a year later why a change looks the way it does. Decide once for the whole repository and enforce it in CI, so the history stays consistent.

#claude-code#git#commit-messages#attribution#privacy