p4ni.

Research

Claude Code Signs Your Commits. Which Setting Actually Stops It?

· 7 min read

On this page

Two threads about this hit Hacker News on the same day — one with 205 comments about Claude Code appending a session URL to commit messages, one titled “I am no longer letting Claude Code add itself as Co-author in my commits.” Neither of them, and none of the blog posts on page one of Google, showed a measurement. The top result for claude code commit message co author is an eleven-month-old Reddit thread whose accepted answer is a link to the settings docs.

So I read the schema out of the binary and then measured 88 runs against it. The short version: the setting everyone recommends is deprecated, it is also the only single key that does the whole job, and the replacement has three ways to silently do nothing.

What the schema actually says

Claude Code ships as a compiled binary, but the settings schema is a zod object with .describe() strings on every field, and those survive into the file. Pulling them out of 2.1.252:

attribution.commit    Attribution text for git commits, including any trailers.
                      Empty string hides attribution.
attribution.pr        Attribution text for pull request descriptions.
                      Empty string hides attribution.
attribution.sessionUrl  Whether to append the claude.ai session link to commits
                      and PRs created from web or Remote Control sessions
                      (default: true).
includeCoAuthoredBy   Deprecated: Use attribution instead. Whether to include
                      Claude's co-authored by attribution in commits and PRs
                      (defaults to true)
includeGitInstructions  Include built-in commit and PR workflow instructions in
                      Claude's system prompt (default: true)

That is the whole surface. includeCoAuthoredBy — the key in every blog post about this — is marked deprecated. The replacement is an attribution object with three fields, and one of them concerns a trailer most people have not seen yet.

How I measured

Every run gets a throwaway git repository: git init, one commit, one staged change, then a single non-interactive turn.

claude -p "Commit the staged change." --safe-mode \
  --settings '{"attribution":{"commit":""}}' \
  --model sonnet --permission-mode bypassPermissions
git log -1 --format=%B

--safe-mode is what makes this measurable. My own ~/.claude/CLAUDE.md tells Claude never to sign commits, and that instruction would have contaminated every baseline. Safe mode disables user and project CLAUDE.md, settings, plugins and hooks, so the only variable left is what I pass on the command line. I confirmed --settings still applies underneath it by setting attribution.commit to a marker string and watching the marker come out the other end:

Add line two to app.txt

X-Probe: HELLO

Five runs per condition for the commit tests, three for the PR tests, 88 in total. Every condition landed on either all runs or no runs — there was no jitter to average away.

The results

What I setCo-Authored-By survived
nothing (baseline, sonnet)5/5
nothing (baseline, opus)5/5
includeCoAuthoredBy: false0/5
attribution: { commit: "" }0/5
attribution: { commitTrailers: false }5/5
attribution: { pr: "" }5/5
includeGitInstructions: false0/5
a one-line instruction in the prompt0/5

Three of those deserve an explanation.

Trap 1: commit and pr are separate switches

The deprecated key covers both surfaces. The new one does not.

To test the PR side without opening pull requests, I asked each session to print, verbatim, what its system prompt says the PR body must end with, and scored the answer on whether it reproduced the 🤖 Generated with [Claude Code] line.

What I setPR attribution survived
nothing (baseline)3/3
attribution: { pr: "" }0/3
attribution: { commit: "" }3/3
includeCoAuthoredBy: false0/3

Read that against the commit table and the shape is symmetric. Setting commit: "" leaves the PR body signed in 3 of 3 runs. Setting pr: "" leaves the commit trailer in 5 of 5. The only single key that clears both is the deprecated one.

This is the trap most people will hit, because the obvious migration — see “deprecated”, replace one line with one line — gives you attribution: { commit: "" } and a pull request that still says Claude wrote it.

Trap 2: commitTrailers looks like a key and is not one

While reading the binary I found a fourth name, commitTrailers, sitting in the same set as commit, pr and sessionUrl. It reads like the switch you want. It is not exposed to you:

attribution: f({
  commit: i().optional(),
  pr: i().optional(),
  sessionUrl: q().optional(),
}).passthrough()

The schema is .passthrough(), so an unknown key inside attribution is neither rejected nor read. You get no validation error, no warning, and no effect. The trailer survived 5 of 5 runs. commitTrailers is a flag the managed-policy normalizer sets internally when an administrator disables attribution org-wide; writing it in your own settings file is a no-op that looks exactly like a fix.

Settings files are the wrong place to learn this the hard way. In non-interactive mode, Claude Code silently ignores settings files that fail validation — and here the file does not even fail.

Trap 3: mixing old and new is not additive

If you set the deprecated key and the new one at the same time, the new one wins outright. I gave one condition both includeCoAuthoredBy: false and attribution.commit: "X-Probe: kept", and the marker showed up in 5 of 5 commits — the false was ignored entirely.

The resolution order is visible in the binary, and it goes: commitTrailers if it is a boolean, then commit or pr if either is defined, then includeCoAuthoredBy. So the moment you touch attribution at all, your old key stops being consulted. A half-finished migration is worse than either state on its own.

What to actually write

Three fields, all of them, in ~/.claude/settings.json:

{
  "attribution": {
    "commit": "",
    "pr": "",
    "sessionUrl": false
  }
}

Measured: 0 of 3 commits carried the trailer, and 0 of 3 sessions reported a required PR trailer — all three answered NONE. If you would rather keep one line and revisit it when it breaks, "includeCoAuthoredBy": false still does both surfaces in 2.1.252, deprecation notice and all.

Does CLAUDE.md work?

This is the claim I most wanted to check, because the Reddit threads are full of people saying the instruction gets ignored, and this blog’s repo has been running on exactly that method for a month.

It held every time. A single line — never add Claude attribution to commits or PR descriptions — removed the trailer in 20 of 20 runs. That includes burying it as line 60 of a 126-line conventions document, where it competes with ninety rules about logging and migrations, and it includes running the whole thing again on Opus instead of Sonnet. My own ~/.claude/CLAUDE.md, which carries the same rule in Japanese, was 0 for 5 as well.

So I could not reproduce the failure. What I can say is narrower than “CLAUDE.md works”: every one of these runs was a single turn in a fresh session. An instruction fifty turns deep in a long conversation is a different test, and I did not run it. That is the plausible shape of the Reddit complaints, and it is the difference between the two mechanisms — the settings key removes the instruction from the system prompt, while CLAUDE.md adds a competing one and asks the model to weigh them. The first cannot drift. (Which instruction wins when they conflict is a question I measured separately, and CLAUDE.md won 11 to 0 there.)

There is also a cost argument for using the setting: instructions in CLAUDE.md are prefilled into every session, and a settings key is free.

The blunt instrument

includeGitInstructions: false also removed the trailer, 0 of 5. It works because it deletes the entire built-in commit-and-PR workflow from the system prompt — not just the signature, but the staging conventions, the message guidance and the gh pr create template. If you have your own commit workflow in CLAUDE.md and find the built-in one gets in its way, this is a real option. As a way to remove a trailer it is a lot of collateral damage.

The session URL

The trailer that started the 205-comment thread is newer:

Claude-Session: https://claude.ai/code/session_01...

It has its own switch, attribution.sessionUrl, defaulting to true. I could not measure it: across all 88 runs, no claude -p session ever emitted one. The schema says it applies to “commits and PRs created from web or Remote Control sessions,” which is a narrower set than the threads assume — a headless local run is not in it. I have verified that including sessionUrl: false does not disturb the commit and PR behavior above, and nothing more than that.

Limits

One version, 2.1.252, measured on 2026-09-01. Every run was a single non-interactive turn, so nothing here speaks to long sessions. The PR results come from asking the model what its own system prompt requires rather than from opening real pull requests — that is a proxy, and I scored it on whether the answer reproduced the attribution line verbatim. And the deprecation notice means the one-line fix has a shelf life: includeCoAuthoredBy works today, and the field description is telling you it will not forever.