🦄 I shipped a linter that fails your commit if you won't admit AI touched the code, and then did the most predictable thing possible—let nine months of the data sit there untouched while I busied myself with other things.

Then I actually looked at it: nine months of footers piled up in git log like a lonely change jar. Every one of them said how much of those commits were mine, but I hadn't ever sat down and actually counted the jar. So I built the thing to count it. 🪙


The Jar Nobody Counted 🫙

Nine months of commits, every single one carrying a footer that states how much of it I actually wrote, and I could not have told you the number—not roughly or even within twenty points. It was all sitting in git log, structured, and enforced on every commit by a hook I built specifically for that purpose. But it was completely inert.

Dropping change in a jar isn't the same as knowing how much money is in it. rai-lint will block your commit until you write the footer, but then it's done—the pile just sits there, and I never built the thing that adds it up.

GitHub logo anchildress1 / rai-lint

Dual-language linter for Responsible AI commit footers — shared logic for Node (commitlint) and Python (gitlint).


What is this? 🤖

RAI Lint enforces Responsible AI (RAI) attribution in every commit. No more "who wrote this?" moments. No more mystery code. Just honest, trackable AI contributions.

Read the full story: Did AI Erase Attribution? Your Git History Is Missing a Co-Author

%%{init: {'theme':'dark'}}%%
flowchart LR
    A[Developer Commits] --> B{Has AI Footer?}
    B -->|Yes| C[Commit Accepted ✅]
    B -->|No| D[Commit Rejected ❌]
    C --> E[Clear AI Attribution]
    D --> F[Add Footer & Retry]
Loading

Why does this exist?

Because transparency matters. When AI writes code, everyone should know. This isn't about fear or compliance theater—it's about building trust and maintaining clear audit trails.


Features 🎯

The footers aren't some format I made up, either. They're Git trailers—the same key-value convention Signed-off-by and Co-authored-by have used for years, which is exactly why git log can hand them back without anything having to parse prose. That was my first idea: encode attribution in a shape Git already understands, so something else can read it later.

Turns out everyone had roughly the same idea. The Linux kernel codified Assisted-by: AGENT_NAME:MODEL_VERSION, Fedora and LLVM recommend the same trailer in their AI contribution policies, and Artsy argued the whole thing out in public in an RFC titled disclose LLM usage in commits or PRs, opened in May 2026 and merged in June. Three projects that don't talk to each other landed on the same trailer.

So rai-lint isn't a special private dialect—it enforces vocabulary the rest of the industry already uses, and it fails the commit if yours is missing, which is more than a policy document can do.

Now rai-commit-badge reads them back. It weights every footer in the history by the lines that commit actually changed, then writes the result into a README as a Shields.io badge.

rai-commit-badge — your git history already knows how much AI wrote

rai-commit-badge

Your git history already knows how much AI wrote. This reads it back.

A GitHub Action that scores RAI attribution footers and publishes a shields.io badge.

📊 Project Stats

GitHub Issues Release License: Polyform Shield License 1.0.0

Sonar Tech Debt Coverage Bugs Code Smells

CI check-dist

📦 Marketplace

Marketplace

AI attribution

That badge is this action, scoring itself.

🗣️ Languages

JavaScript Badge Node.js Badge

🤖 AI & Automation

Claude Badge GitHub Actions Badge

🔧 Quality & Standards

Conventional Commits Badge commitlint Badge ESLint Badge Lefthook Badge

Vitest Badge Prettier Badge SonarQube Cloud Badge


Getting StartedConfigurationAboutScoreRelated


Getting Started 🚀

1. Mark where the badge belongs in your README.md:

<!--START_SECTION:rai-badge-->
<!--END_SECTION:rai-badge-->

Enter fullscreen mode Exit fullscreen mode

Tip

If prettier formats your README, wrap the pair in <!-- prettier-ignore-start --> and <!-- prettier-ignore-end -->. Prettier adds a blank line after the start marker, which the action then rewrites on the next run — the fences keep the block byte-stable.

2. Add the workflow:

name: RAI Attribution
on:
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}

jobs:
  score:
    runs-on

Enter fullscreen mode Exit fullscreen mode

The color moves with the number—blue at 33% or less, purple through 66%, and magenta once you clear 67%—so the band reads before the numbers do.

The AI attribution badge in all three color bands: blue for 0-33%, purple for 34-66%, pink for 67-100%

💡 ProTip: Purple is where rai-lint landed—66% across 158 commits going back to October. The badge's own repo, rai-commit-badge, sits in the pink band at 88%, which is what two days of mostly-Claude output looks like next to nine months of my own typing.


Not Every Footer Weighs the Same ⚖️

Assisted-by and Generated-by are not the same confession, and a scorer that flattens them into one bit—AI touched this, yes or no—throws away the only interesting thing the convention captured. So every footer is worth something different, the same way a handful of change would be:

Footer Declares Weight
Authored-by Zero AI 0.00
Commit-generated-by Trivial AI, no code 0.05
Assisted-by AI helped, human led 0.25
Co-authored-by Roughly 50/50 0.50
Generated-by Majority AI 0.90

Keeping Co-authored-by in that scale is a call some of the newer policies would argue with, but I made it anyway: it's already widely accepted. Nothing I do changes that and dropping it would mean I'm scoring a vocabulary nobody actually uses. The cost is real though—the scorer has to know which co-authors are AI, and that list gets unmanageable eventually—but until there's a better system, this one works just fine.

Counting commits tells you nothing, since one commit is a typo fix and the next is a new module, so each commit gets weighted by the lines it actually changed. Lockfiles, dependency trees, build output, and minified assets don't count toward that total—a regenerated lockfile says nothing about who wrote the feature.

The ceiling is 0.90 and it doesn't move. Even on a commit where the model wrote every line, a human still chose to build the thing and at least directed it—even if that human reviews code the way I do. Ninety percent is the highest this system will reasonably attribute to AI, so nothing here ever reads 100%.

💡 ProTip: 0.05 covers trivial contributions, an AI-generated commit message being the obvious one. I didn't put it at zero because it isn't the same as writing your own.


Since When, Exactly 🪟

A bare percentage doesn't tell you much. 66% of what—everything since the repo was created, back when there were no footers at all and every commit scores as human? That version of the number only ever gets smaller, and it says more about how old the repo is than about how I work now.

So the window opens at the earliest RAI footer, auto-detected, and the date sits right there in the badge: 66% since 2025-10. In practice that reads as "since we adopted this," which is what it actually means. I stopped at month precision on purpose, because since 2025-10-31 makes people ask what happened on Halloween, and the answer is nothing—that's just the day the first footer went in. A day-precise date claims more accuracy than the input has, and three extra characters on an already-wide badge aren't worth it. You don't lose the detail: the workflow job summary prints the exact window start next to the commit counts.

The workflow job summary for rai-lint showing score 66.1% rounded to 66%, window start 2025-10-31, 158 of 158 commits in the window, 114 attributed, and 28 squashed commits with averaged footer weights

That table is also where the rest of the story shows up. Of those 158 commits, 114 carry a footer and 44 don't, and every one of those 44 scores as human. Twenty-eight are squashed, so their footer weights got averaged, which the summary says out loud instead of folding it quietly into the number.

Inside that window, a commit with no footer at all counts as human, at weight zero. That's a hole and I know it's a hole—skip the footer and the number drops, and nothing in git distinguishes a commit a human wrote from a commit someone didn't feel like labeling. Erring toward human is still the right default. This tool exists to attribute AI where AI is known, and if I started guessing at the commits I can't account for, the badge would stop being a measurement.

I'm not the only one stuck there. An IETF Internet-Draft, Identity-Attributed Git Commits via Tier-Structured Trailers, goes considerably further than a footer and a hook, splitting attribution across three tiers:

Trailer Tier Can sign
Acted-By: Human Yes
Executed-By: Bot Yes, scoped
Drafted-With: AI instrument No keys, ever

An Ed25519 signature over the commit's tree hash rather than its commit hash keeps that attribution intact through rebase and squash, which is genuinely more than I built. Section 11.7 still lands where I did: it names negative-attribution risk and concludes the protocol layer can't close it. The draft gives honest committers a way to say so and stops there.

🦄 Before anyone cites that at me as settled: it's an individual submission, it has no formal standing in the IETF process, and it expires in November 2026. I'm pointing at it because someone else mapped the same territory and reached the same dead end.


One PR, Open Until You Merge It 🪛

You should find out your attribution number changed by reading a diff, not by noticing a commit you didn't make. So nothing here touches your default branch: every run cuts rai-badge--branches--<base>, force-pushes the rewritten README onto it, and opens a pull request, reusing the one already open if there is one. The branch gets rebuilt from base each time, so that PR holds exactly one commit no matter how many times you push.

Setup is two markers and a workflow. The markers go wherever the badge belongs:

<!--START_SECTION:rai-badge-->
<!--END_SECTION:rai-badge-->

Enter fullscreen mode Exit fullscreen mode

If prettier formats your README, wrap the pair in <!-- prettier-ignore-start --> and <!-- prettier-ignore-end -->, since prettier adds a blank line after the start marker that the action rewrites on the next run. The fences keep the block byte-stable.

Then the workflow:

name: RAI Attribution

on:
  push:
    branches: [main]

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}

jobs:
  score:
    runs-on: ubuntu-latest
    timeout-minutes: 5
    permissions:
      contents: write
      pull-requests: write
    steps:
      - uses: actions/checkout@v7
        with:
          fetch-depth: 0

      - uses: anchildress1/rai-commit-badge@v1

Enter fullscreen mode Exit fullscreen mode

fetch-depth: 0 is required, because the default checkout is shallow and a shallow clone doesn't carry the history the score is made of—the run fails rather than publish a number derived from four commits. Over in Settings → Actions → General → Workflow permissions, Actions also needs permission to create and approve pull requests, or you get a clean score and a 403 where the PR should be.

🦄 Check it out on the Marketplace at https://github.com/marketplace/actions/rai-commit-attribution-badge and leave a star if you found it useful.


The Number Was Always There 🪙

Every commit in that history already stated how much of it was mine. All I added was arithmetic.

rai-lint enforces the footer at commit time. rai-commit-badge scores what it collected.

If you've been writing RAI footers, your number already exists whether or not you go looking for it. Mine is 66% across nine months, and I'd rather publish that than pretend I don't know.


anchildress1 image

🛡️ The Jar Counted Itself

Claude wrote most of the scorer, then wrote this footer about the scorer, which under my own weights lands at Generated-by — 0.90, the heaviest coin in the jar. I counted the change. It did the multiplication.