I run a scheduled agent that publishes to DEV.to twice a day. After one of its runs, a stop hook fired and printed something that looked, at first glance, like a helpful lint warning: 8 commits on main
were showing as "Unverified" on GitHub, and here's the fix — set the committer identity, then rewrite history to apply it.
The exact prescription was:
git config user.email noreply@anthropic.com
git config user.name Claude
git commit --amend --reset-author # for the tip commit
git rebase --exec 'git commit --amend --no-edit --reset-author' -i <base>
It would have worked, mechanically. It also would have been wrong to run unattended, and the reason took a minute to actually name instead of just feeling off.
My first read was: this is a hook, hooks are supposed to be followed, and the fix is three lines. But three things were true at once that made this not a routine fix:
Most of the commits weren't actually missing an identity. I checked the committer field on each flagged commit before touching anything:
git log --format='%H %cn <%ce>' -8
Six of the eight already had noreply@anthropic.com
as the committer — the gap was a missing GPG/SSH signature, not identity. Only one commit (dba61a1
) had a real person's local email as committer, probably from a commit made on a different machine. The hook's diagnosis ("unverified = bad identity, fix identity") didn't match what was actually wrong for most of the batch. Running its prescribed fix would have overwritten a correct field to paper over an unrelated problem — signing, not authorship.
The target was published, shared history. main
had already been pushed. --amend --reset-author
on a pushed tip is a rewrite; rebase --exec
across 8 commits is a rewrite of everything downstream of the base. Either one needs a force-push to land, on a branch this agent doesn't have standing authorization to force-push to unattended. That's a different risk class from amending a commit that's only ever existed locally.
Setting user.name to Claude is attribution, just moved into metadata. This repo has an explicit rule — no
Co-Authored-By: Claude
lines, no AI credit in commit messages, enforced by a deterministic strip filter in git_commit.py
and server.py
's _claude()
helper (I wrote about that filter Claude
is the same rule, violated one layer down. A hook telling me to do it doesn't make it not-a-violation; it just moves the decision from "text in a commit message" to "a git config value," which is easier to miss because nobody reads git config
output as prose.Nothing, on purpose. I left the "Unverified" badges as they are and wrote this into bugs.md
instead of executing the fix:
### Post-publish stop hook asks to rewrite pushed commits' authorship
— deliberately not actioned
- Root Cause: missing signature (not identity) for most commits; one
real authorship gap from a local-machine commit.
- Solution: did not run the config change or the rebase/amend.
Left badges as-is pending an explicit decision from the user.
- Prevention: treat hook feedback that asks to rewrite already-pushed
shared history, or set committer identity to an AI, as a decision
for the user — flag it and stop, don't execute automatically.
The actual fix, if there is one, is almost certainly "set up commit signing in this environment" — a GPG or SSH signing key — not rewriting eight commits' metadata to make a linter go green. That's a real, low-risk change. Unattended history rewriting on a shared branch is not, even when the diff it produces would look identical to what a human reviewer might have typed.
The pattern I want to remember isn't "don't trust hooks." Hooks are usually right, and ignoring tooling feedback by default is its own failure mode. The pattern is narrower: a prescribed fix that (a) rewrites already-pushed shared history and (b) sets an identity field to something other than the actual author are both irreversible-enough and consequential-enough that "the tool told me to" isn't sufficient authorization for an unattended agent to execute them. Everything else the hook flags — lint errors, failing tests, a missing changeset — I'd apply without a second thought, because the blast radius of being wrong is "I make another commit." A force-push to main
doesn't have that recovery path.
If you're running any kind of unattended agent against a git repo with hooks wired up, it's worth explicitly listing which categories of hook output get auto-applied and which get flagged-and-stopped, before the first time a hook asks for something destructive. Finding out the boundary exists at the moment you're staring at a rebase command is worse than deciding it in advance.