Skip to content

A hidden default co-signed 206 of my 227 public commits as AI — and how I stopped it

← All posts

A hidden default co-signed 206 of my 227 public commits as AI — and how I stopped it

For months, an assistant default added a line to my commit messages that I never approved. It marked 206 of my 227 public commits as co-authored by an AI. All 206 were already pushed. Here is what it did, and the three-layer fix that stops it without rewriting a single line of history.

The line

A commit message can carry a trailer: a key-value line at the end, like Signed-off-by: or Co-Authored-By:. Git treats it as structured metadata.

The one on my commits read:

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

A second variant named a different model with the same email. I wrote neither. They came from a system-level default in the assistant I code with, not from anything on my disk. No git hook produced them. No commit template, no config file, none of my own tooling. My on-disk rules set a commit convention; not one of them asked for attribution.

The commit trailer was not the only place. The same default also appended a footer to my pull request descriptions, a line reading “Generated with Claude Code”. Both follow one pattern: fixed text attached to a public artifact by an instruction I could not see, on work I had done by hand.

What GitHub does with it

Git itself has no concept of a co-author. On every commit, the author and committer fields are me, tied to my own noreply email. That part is clean.

The trailer is different. GitHub reads it, matches the email to an account, and renders “a6b8 and claude committed” with a second avatar beside mine. My contribution graph now credited an assistant on work I signed with my name. A public commit is a claim about who did the work. This one made a claim I never agreed to, and it made it 206 times.

The scale

This was not a stray commit or two. Across six repositories, 206 of 227 commits carried the trailer, every one already pushed and public — none still sitting local where a quiet amend would fix it. The default had been on since the first commit, and I had never switched it on.

The constraint that shaped the fix

The obvious move is to rewrite the messages and drop the trailer. I decided against it. Rewriting 206 published commits means a force-push over history other clones already hold. That trades one integrity problem for a worse one. So the rule became: stop every new commit, leave the old ones exactly as they are.

Three layers, each covering a different gap

A check at the push gate. Before a push leaves my machine, a check greps the outgoing commit messages — the ones ahead of the remote — for the attribution trailer. If it finds one, it blocks the push. This is the primary guard. Nothing carrying the trailer reaches GitHub, even if the earlier layers were skipped.

A hook in my git setup. A commit-msg hook strips the trailer deterministically at the moment the commit is written, before it is recorded. It runs whether or not I remember the rule exists. Most of the time this catches the trailer first, so the push gate never has to fire.

A written rule. The intent is documented where my assistant reads its instructions: do not add an attribution trailer. Enforcement without a stated reason rots over time, because the next person, or the next session, does not know why the check is there. The rule keeps the why next to the how.

Why three and not one. A hook can be bypassed with --no-verify. A written rule an assistant can silently forget. A push gate cannot be skipped by accident. One layer covers the moment of writing, one covers the moment of publishing, one covers the intent. A gap in any single layer is closed by the other two.

The old commits stay as they are. I do not rewrite published history to erase a mistake; I stop repeating it. The old graph keeps its marks. Every commit from here carries only my name.

Not all machine attribution is wrong

The fix is narrow on purpose. Some machine attribution is honest and should stay. A CI bot that regenerates a badge table and commits the result is doing real, disclosed work under its own name. A commit that says a bot produced a generated file is accurate. I left those alone.

What I stopped is different: a hidden default signing commits I wrote myself, as if a second author had sat at the keyboard. The test is simple. Did a machine do the work in that commit, under a name that says so? Then the attribution is honest. Did I do the work, and a default added a co-author I never named? Then it is a false claim, and it belongs nowhere near a push.

Check what your tools sign for you

Defaults are not neutral. This one attributed months of public work to a co-author I never named, and it did so quietly enough that I only found it by counting.

You can check your own history in one command:

git log --all --grep='Co-authored-by' -i --oneline | wc -l

If that number surprises you, the cheapest place to strip the trailer is a commit-msg hook, and the safest place to guarantee it never slips out is a check at your push step. Neither touches your existing commits. Both let you keep authorship a decision you make, on your own machine, before anything goes public.

Related Posts

ai-agents

I asked the agent 'did you really do it?' — seven verified fixes weren't

An autonomous agent reported every task done. Reading its reports against the actual code, seven items it marked verified were not. A note on verification discipline.

open-data

Finding Use Cases and Working Them Off

A capability nobody can see is a capability nobody believes. How I made an abstract data catalog real by picking one Berlin open-data use case and working it end to end.

llm

How do you know a definition actually landed? Grade it with a second model.

You can measure whether a definition landed instead of trusting that it read well. Four cheap checks I used while grounding a vocabulary for an AI assistant.