Wie schreef deze commit? Git-attributie als een agent meewerkt
8m leestijd

Wie schreef deze commit? Git-attributie als een agent meewerkt

Ik ging op zoek naar de coding agent in 1.644 commits uit tien van mijn publieke repositories. Hij liet geen enkel spoor na. Wat de auteursgegevens wél herschreef, was de merge-knop.

Ik ging op zoek naar de agent in mijn eigen git-historie.

Tien publieke repositories, 1.644 commits op main op de middag dat ik ze telde. Bij een deel van dat werk was een coding agent betrokken en bij een deel niet, en over wat nu volgt wil ik precies zijn: ik kan je niet vertellen welk deel. Niets in de historie houdt die twee uit elkaar. Geen enkele attributie-trailer, op geen enkele branch, in geen van de tien.

Dat is de eerste bevinding, en het loont om daar even bij stil te staan. Het document waar ik naar zou grijpen bij de vraag of er bij deze ene commit een machine heeft meegeholpen, bevat dat antwoord niet. En ik heb die repositories zelf geschreven.

De tweede bevinding is dat de auteursgegevens tóch zijn herschreven, door iets waar niemand ruzie over maakt.

De trailer waar iedereen ruzie over maakt

Claude Code levert een commit-trailer mee. Standaard plakt de tool Co-Authored-By: Claude <noreply@anthropic.com> onder je commits en een regel "Generated with Claude Code" onder je pull request. Daar is genoeg over te doen geweest om bugmeldingen op te leveren, waarvan er één betoogt dat die standaardinstelling verder gaat dan smaak en gewoon feitelijk onjuist is.

Dat argument is het lezen waard. De registratierichtlijn van het Amerikaanse Copyright Office zegt letterlijk: "Applicants should not list an AI technology or the company that provided it as an author or co-author simply because they used it when creating their work." Diezelfde richtlijn houdt vast aan "to qualify as a work of 'authorship' a work must be created by a human being". Git-tooling behandelt elke naam achter Co-authored-by als auteur van de commit, dus die trailer doet een uitspraak over auteurschap.

De instelling die het regelt is verhuisd. includeCoAuthoredBy is deprecated en vervangen door een attribution-object dat de commit-trailer en de pull request-tekst los van elkaar zet. Allebei worden ze nog gelezen. In versie 2.1.263 verloopt het zo: staat attribution er en klopt de vorm, dan wint die. Staat includeCoAuthoredBy exact op false, dan komt er niets. In alle andere gevallen komt de trailer er gewoon onder.

Uit die volgorde volgen twee dingen. Het ene staat gedocumenteerd op een plek waar je nooit kijkt, het andere staat nergens.

Het eerste keert zich tegen je. De attribution-tak vult elk veld met ??, dus een ontbrekende sleutel valt terug op de ingebouwde standaard en niet op leeg. Zet je attribution: {"pr": ""} omdat die regel onder je pull request je irriteerde, dan heb je stilzwijgend de commit-trailer laten staan die je dacht weg te configureren. Een lege string onderdrukt. Een afwezige sleutel herstelt. Anthropic schrijft het wel op, in het settings-schema dat in dezelfde binary meekomt. "Each field defaults to the standard Claude Code attribution if not set", en "Empty string hides attribution." Het klopt precies, en het staat in een JSON-schema in plaats van op de pagina die je opzoekt als je die trailer wilt uitzetten.

Het tweede is een stukje archeologie. Wie via de oude sleutel binnenkomt, zet een telemetrieteller aan die tengu_dead_probe_include_coauthored_by heet en die per plek maar één keer afgaat. Een "dead probe" bouw je in als je wilt weten of iemand een stuk code nog gebruikt voordat je het weggooit. Zet je de nieuwe sleutel, dan gaat die teller nooit af, want de code komt niet voorbij de eerste tak. De groep die geteld wordt bestaat dus precies uit de mensen die alleen de oude sleutel hebben staan, en die op false.

Bij mij kostte dat allemaal één regel in de settings. attribution met lege strings, globaal en in het project. Daarom staat het aantal trailers in tien repositories op nul, en daarom kan de historie de vraag uit mijn openingsalinea niet beantwoorden.

Of dat verkeerd is, weet ik overigens nog zo net niet. Een trailer die aangeeft dat er bij een commit een machine aan te pas kwam, klinkt als transparantie, tot je je afvraagt wat een lezer ermee doet. De review overslaan? Juist scherper kijken? De code is verdedigbaar of niet, en die trailer vertelt je niet welke van de twee. Wat ik wel zeker weet: het uitzetten was een keuze met een gevolg, en het gevolg is dat de historie er voorgoed het zwijgen toe doet.

Ik ging ervan uit dat het verhaal daar ophield.

Toen telde ik de auteurs

Staafdiagram van wie 1.644 commits op naam heeft: git config 1289, GitHub 355, de agent 0.

Tien verschillende auteursidentiteiten over 1.644 commits, voor een historie die door één persoon en twee bots is geschreven.

aandeelwie
29,0%mijn GitHub-noreply-adres, precies het adres dat in elke repository is ingesteld
27,8%mijn naam met een heel ander adres erachter
22,3%een werkadres uit een eerdere situatie
14,0%nog vijf schrijfwijzen van mijzelf, in wisselende combinaties van naam en adres
6,8%github-actions[bot]
0,1%een reviewbot

In alle tien de repositories staan user.name en user.email lokaal vastgezet op één identiteit. Ik controleer dat. Er is een regel voor. En het dekt nog geen derde van de historie.

De reden is saai en meteen de hele pointe: gh pr merge --squash merget op de server. Je lokale git-config komt er niet aan te pas, want jouw machine maakt die commit niet. GitHub bouwt hem en schrijft hem toe aan je accountprofiel en het commit-mailadres dat daarin staat. Merge vijfenveertig pull requests op één dag, zoals ik deed op de dag dat ik dit mat, en die commits landen onder de identiteit die op dat moment aan je account hangt, wat elke checkout op je schijf ook beweert. Over de hele historie zijn 355 commits gemaakt op de servers van GitHub, buiten elke machine van mij om.

Diezelfde merge schrijft ook de Co-authored-by-regels. Alle 161 in mijn historie komen van GitHub, toegevoegd voor de auteurs van de commits die gesquasht worden. GitHub documenteert die trailer zelf, en documenteert dat het adres van een co-auteur aan diens account gekoppeld hoort te zijn. Dat de merge-knop ze voor je schrijft, staat er niet bij.

Op één en dezelfde regel staan dus een auteur die ik niet heb gekozen en co-auteurs die ik niet heb toegevoegd.

Waar dat document voor dient

Dit doet ertoe vanwege wat mensen van git log verwachten. Het argument dat AI-code een audittrail nodig heeft, komt bijna altijd uit bij de commit, omdat de commit blijft staan. Wie verantwoordelijk is voor AI-code is een vraag waarbij mensen hopen dat de historie meehelpt.

Dat kan die historie niet, en de agent is niet de reden. Nog voordat er één woord over AI valt, wordt het auteursveld van een commit gevuld door wie er in een webinterface op een knop drukt, en worden de trailers eronder bepaald door een merge-strategie. Een trailer met de naam van een model zou terechtkomen in een document waar al drie partijen in schrijven: jouw git-config voor lokale commits, de accountinstellingen van GitHub voor gemergede commits, en de merge-strategie voor alles onder de onderwerpregel.

Zet daar een modelnaam bij en je hebt geen verantwoording gecreëerd. Je hebt een vierde schrijver toegevoegd aan een document dat niemand zorgvuldig leest.

Eerlijker is het om te erkennen dat de commit hier een slechte plek voor is. In de pull request zit de redenering: wat er gevraagd was, wat de review eruit haalde, wat de checks zeiden, wie akkoord gaf. Dat document is rijker, je herschrijft het niet per ongeluk, en het overleeft een squash. De commit die landt is er een samenvatting van, geschreven door een machine en toegeschreven door een webserver.

Wat ik zou doen

Bepaal welke vraag je historie moet beantwoorden en stel je configuratie daarop in, in plaats van te vechten tegen een standaardinstelling.

  • Zet attribution expliciet, met elke sleutel die je belangrijk vindt erin. Lege strings onderdrukken. Weggelaten sleutels herstellen de standaard, zonder dat je het merkt.
  • Stel het commit-adres van je account in, niet alleen je lokale config. Je lokale config bepaalt de commits die jij zelf maakt, de accountinstelling bepaalt elke commit die GitHub namens jou maakt. Bij een squash-merge-workflow is dat de meerderheid.
  • Kijk wat je historie echt zegt voordat je erop vertrouwt. git log --format='%an <%ae>' | sort | uniq -c | sort -rn kost een seconde en vertelde mij iets waarvan ik had gezworen dat het niet zo was.
  • Leg de verantwoording in de pull request, waar ruimte is om specifiek te zijn, en laat de commit de samenvatting zijn.

Wat me vooral bijblijft, is hoe weinig de agent hiermee te maken had. Ik ging op zoek naar vingerafdrukken en vond er geen enkele, omdat één instelling goed stond. Alles wat er echt mis was met dat document kwam van een merge-strategie die ik jaren geleden koos en een profielinstelling waar ik nooit naar heb gekeken.

We hebben twee jaar gediscussieerd over de vraag of een model in het auteursveld thuishoort. Ondertussen werd dat auteursveld ingevuld door een webserver.

(4 van 4)