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
# or, for the whole run:
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.
Why "just run it" was the wrong instinct
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 in an earlier post — the short version: telling the model not to add attribution isn't enough, you strip it after generation, mechanically, every time). Setting the committer identity itself to 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.
What I did instead
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 general shape of this
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.
Top comments (0)