Honestly Co-Authored
How I disclose which and how AI helped create an article
Scroll to the bottom of any article on this site and you'll find a section called "Changes". Sometimes it lists a single change with me as the author. Sometimes it lists me and "Claude", or one of the other agents I write with.
I'm not against using AI to draft, edit, or refactor. I do it all the time. But it should be disclosed.
This site's content lives on GitHub. Every edit is a commit, every commit has an author and co-authors, and the full history is right there — who, or what, changed which line.
I write the articles in markdown and deploy them with Zola 1. The deploy is a 30-line GitHub Action that runs zola build and ships the static assets to a Cloudflare Worker. No CMS — just a markdown file in content/articles/, a git commit, a git push, and a few minutes later the post is live.
The trick
Inside the page footer template, Zola calls the GitHub API for the commits that touched the current article:
{% set token = get_env(name="GITHUB_TOKEN") %}
{% set commits = load_data(
url="https://api.github.com/repos/<REPO_PATH>/commits?path=content/" ~ page.relative_path,
headers=["accept=application/json", "authorization=Bearer " ~ token],
format="json", required=false
) %}
Then turn the JSON response into a list — short SHA, commit subject, primary author, and any Co-Authored-By names parsed out of the body:
<ul>
{% for commit in commits %}
<li class="commit" data-hash="{{ commit.sha | truncate(length=6, end="") }}">
<span class="subject"
>{{ commit.commit.message | split(pat="\n") | first }}</span
>
<span class="author">{{ commit.commit.author.name | split(pat=" ") | first }}</span>
{% for line in commit.commit.message | split(pat="\n") %} {% if line is
starting_with("Co-Authored-By:") %}
<span class="author"
>{{ line | trim_start_matches(pat="Co-Authored-By:") | split(pat="<") | first |
trim | safe }}</span
>
{% endif %} {% endfor %}
</li>
{% endfor %}
</ul>
All of it runs at build time on the short-lived GITHUB_TOKEN that GitHub Actions hands every job — no Personal Access Token to manage, no secret to rotate, no JavaScript, no client-side fetch. The HTML ships with the attribution already baked in.
Parsing the commit message in jinja feels pretty hacky though, but gets the job done.
The trailer
That changes list is only as honest as the trailers feeding it. Some I add by hand; others arrive automatically — Claude Code, for example, appends one when it commits on my behalf:
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There's an ongoing discussion about introducing a dedicated trailer for coding agents, something like AI-Assistant: or Coding-Agent:, and ones to capture the model and its context size as additional key:value pairs instead of smuggling them into a name. I think that's the right direction. But Co-Authored-By: is what GitHub renders today, and the habit matters more than the format.
That being said, none of this is specific to prose. The same disclosure belongs on code, configs, anything an agent helps produce — in whatever format the platform already understands.
References
-
Zola is a super fast static site engine shipped as a single binary so one doesn't need to deal with millions of npm dependencies or the like. And I can highly recommend it. https://www.getzola.org/ ↩