← All Articles
Glenn Gore

Commit trust, with nothing in the repo

A signature says who signed. It cannot say whether they are still allowed to. That answer belongs in one place a community can check and change.

On 4 August, someone took over the GitHub account of the maintainer behind keyv, a widely used JavaScript caching library. They pushed straight to the main branch and cut a release. That same day, the payload reached other packages from the same project, and from there it spread to hundreds more.

The release even passed npm’s provenance check, a signed record of the build that produced it, because that build was genuine.

The code was not.

This week, a large part of the open-source world is in Prague for Linux Plumbers and the Open Source Summit Europe. Most of the people in those rooms maintain something that others depend on, and they make the same judgement many times a day. A commit arrives, and the maintainer has two questions:

  • Who wrote this?
  • Were they allowed to, right now?

A signature can answer the first. Most projects answer the second with a file in the repository that anyone with write access can edit. A community should be able to see, and withdraw at any moment, who may change its code.

In my last piece, Move the authorisation, not the keys, I wrote about what an AI agent carries when it acts on my behalf. I ended it by saying that an agent’s authority is only as good as the code it runs. More of that code is now written, reviewed or merged with an AI agent somewhere in the loop, so both questions are getting harder to answer.

Getting there meant retiring some of our own work first. I’ll explain that below.

The file every project keeps

Projects that check signatures at all usually keep a list of the keys allowed to sign. It might be an allowed-signers file, a keyring, or a list of maintainers in the README. It lives in the repository, it is edited like any other file, and it is only as current as the last person who remembered to update it.

Say a maintainer steps down from a project spread across 40 repositories. Someone now has to edit 40 files, and one missed file is a door left open. Most of us have run that sweep, and wondered afterwards whether we finished it.

The usual answer is to sign more. Since the XZ Utils backdoor in 2024, this community has once again argued about key signing and webs of trust. A signature proves that a key was used. It cannot say whether the person holding that key is still meant to have it, and git itself will let anyone write any name and email address into a commit.

The platform has its own answer: whoever holds an account with write access. That answers a different question. It tells you who could push, not who should. On 4 August, the person who could push was not the maintainer.

It is also getting harder to tell who is on the other end at all. On 17 September, the Rust project warned that attackers were setting up job- and project-related video calls to get maintainers to install malware on their own machines.

The Linux kernel has already drawn its own line on the first question. Its guidance for AI coding assistants is plain: “AI agents MUST NOT add Signed-off-by tags. Only humans can legally certify the Developer Certificate of Origin (DCO).” On 24 September, Sasha Levin proposed adding an AGENTS.md file, the file most AI coding agents load automatically, to point them to that guidance. In his testing, one AI agent added a Signed-off-by tag for the developer on its own.

Others deserve credit, too. Sigstore’s gitsign has made signing far easier, with no long-lived keys to manage. It ties each signature to the signer’s account identity at the moment of signing. And the crates.io team’s warning on 17 September asked every maintainer to verify that multi-factor authentication is enabled.

The kernel already insists that a human is accountable for every change. Commit trust, which we have built in OpenVTC, answers the second question at the moment the commit is checked: it asks the community’s registry whether the signer is still allowed to make this change.

Who may change this code, and who says so right now?

The badge, not the lock

Most of us already use a better pattern every time we walk into an office. When someone leaves a company, their badge stops working. Each door checks one list, and that list is kept in one place. A repository is a building with a lock on every door. Commit trust gives it one list. Who may sign is a grant in the community’s registry, not a file in the repo. A grant can name one repository or a whole organisation, and an organisation-level grant covers every repository in it. Enrolling a contributor is one grant.

Two panels. Today: a file in every repository — four repositories each with an allowed-signers file edited by hand, one highlighted as missed in the sweep, because a maintainer leaving means editing every file. Commit trust: one grant — the same four repositories hold nothing in the repo and each check asks a single community registry that carries one grant, may commit to org slash star. Revoking a contributor is one registry operation.

An allowed-signers file in every repository, against one grant in the community’s registry.

Two identifiers, no list in the repo

The key that signs my commits lives in my trust agent, the Verifiable Trust Agent (VTA) I described in my last piece. It is never written to my laptop, and it never leaves the trust agent: my laptop sends the commit to be signed, and only the signature comes back. It is the same principle, applied to code: the key stays where it is, and the permission travels.

Trust agent is a confusing name, because it competes with “AI agent”. I’d like a better one, and Trust Over IP is working on it. Until it settles, when I mean an AI agent, I will say so.

Each commit names its signer in a signed line at the end of the commit message, using a decentralised identifier (DID): a public identifier that the signer controls, with no platform in between. When a pull request arrives, a check runs over every commit in it, with no signing tools and no build toolchain. For each commit, it does three things:

  1. Resolve the signer. Does that identifier publish the key that signed this commit?
  2. Check the signature. Is it valid for this commit?
  3. Ask the registry. The community’s trust registry is asked one question: may this signer commit to this repository, right now?

Three checks, and it fails closed at every one. Because the signer is an identifier rather than an email address, a contributor can rotate their key without anyone having to edit a list. The identifier publishes the new key, and the next commit resolves to it.

Even the platform’s shortcuts are held to the rule. An edit made in GitHub’s web editor and a commit from Dependabot must be signed the same way. The only exemption is a clean merge commit signed by GitHub’s own key, and only when every commit beneath it passes.

Trusted or exempt commits pass. Anything else fails: an unsigned commit, an unknown key, a signer the registry does not authorise, or a registry that cannot be reached. If the registry is down, the check fails, and the pull request waits.

Say a pull request carries 12 commits, and one of them was signed on a laptop that the contributor never enrolled. The check fails, and that one commit is the reason. Eleven good commits do not carry a bad one through.

Two identifiers on the command line. No list of signers in the repository. One identifies the registry. The other is the community’s own DID, held by its Verifiable Trust Community (VTC).

A terminal window titled CI, pull request check. A comment reads: two identifiers, no signer list in the repository. The command is verify-trust with four arguments: range origin/main..HEAD, registry-did did:webvh:…registry, vtc-did did:webvh:…your-community, and resource github.com/your-org/your-repo.

The whole configuration: two identifiers on the command line.

Two rows. Signing: the trust agent holds the key with nothing on disk, signs the commit with the signer DID in its header, and a pull request check runs over every commit with no signing tools and no toolchain. The check, for each commit: resolve the signer — does that identifier publish the key that signed? — then ask the registry — may this signer commit to this repository, right now? Trusted or exempt commits pass; anything else fails closed, including an unreachable registry.

The commit check: resolve the signer, check the signature, ask the registry, fail closed.

The verifier cannot sign. It never opens a session with a trust agent and never touches a keyring, so a pull-request run has no signing capability at all. If the continuous integration job that runs the check is compromised, there is no signing key in it to steal. There is no registry address to configure either. The check finds it in the registry’s own identifier document, and uses the best transport both sides support: the Trust Spanning Protocol (TSP), then DIDComm, a messaging protocol built on DIDs, then plain HTTPS.

What a passing check proves

What a passing check proves is narrow on purpose. It says the commit was signed by someone this community allows to commit here, right now. It cannot say the code is good, and it would not have stopped XZ Utils, where the attacker spent years earning the right to commit. Nor can it say that the device that asked for the signature was clean. That is why the trust agent can be told to ask again before it signs: a second factor from me, or approval from another of my devices or another person, for each signature. It is opt-in, and each community decides what it will accept.

A stolen password should not be enough

Go back to 4 August. Would commit trust alone have stopped it? Not on its own, and no single check should carry the weight of a stolen account. Multi-factor authentication on the account is the first layer. Commit trust adds layers that the account does not control. The key that signs lives in my trust agent, out of my GitHub account’s reach, so taking over the account does not hand over the signature. And the right to commit lives in the community’s registry, which accepts only signed changes from an administrator acting for that community, so nobody can grant it by editing a file in the repository. For critical software, a community can also require several people to review and approve a change before it goes in.

A chain of layers between a stolen account and the main branch: multi-factor authentication on the account as the first layer, the signing key in the trust agent out of the account's reach, a commit grant in the community registry accepting signed changes only, and several people reviewing and approving for critical software. No single check carries the weight of a stolen account; commit trust adds layers the account does not control.

Layers against a stolen account: the account, the key, the grant, and several people approving.

Membership does the rest. When a community grants someone the right to commit, the registry records it. When that is revoked, the record is kept for audit.

Revoking a contributor is one registry operation, not a sweep of every repository’s allowed-signers file.

It also changes what an approval means. With commit trust, a green tick on a pull request means every change in it was signed by someone the community recognises, today. I would rather not learn who could commit to my project from an incident report.

We were part of the fragmentation

In my first piece in this series, Show me the proof, not the promise, I argued that our field keeps fragmenting into more standards and implementations rather than aligning on a common set. We were part of that. Trust Tasks, which I introduced in that piece, are the named, versioned operations our stack exchanges, published as specifications in a shared public registry under Trust Over IP. For a while, the Verifiable Trust Infrastructure (VTI), which I helped build, also published its own namespace, a private set of the same operations, alongside it. All 66 task versions in that namespace are now retired, each pointing to its replacement in the shared public registry. Anyone can publish a registry. Migrating your production system to it and deprecating your own namespace are the parts that cost something. Moving was not free. It meant changing what every service in our stack sends and expects, and making breaking changes to our own integrations on the way. Keeping our namespace and calling it a standard would have been easier.

The VTI vendor namespace, holding 66 task versions and marked retired, superseded by the shared public registry: Trust Tasks under Trust Over IP at trusttasks.org. Anyone can publish a registry; migrating your own system onto it costs something. Count from the Geneva workshop deck, slide 24, 31 August 2026.

66 task versions retired from our own namespace in favour of the shared registry.

The trust registry that answers the commit check has moved on, too. A change merged on 25 September makes it refuse any grant written under an authority the writer cannot act under, and record every write, including those it refuses. By my own test, I owe you one more admission. We moved our own system onto the shared task registry, but we have not yet switched commit trust on for OpenVTC’s own repositories. OpenVTC is an LF Decentralized Trust Labs project, and we are working with them to turn it on. Until then, judge commit trust by its code, which is public.

Shared under whose rules?

A registry that decides who may commit raises a fair question, and this audience will ask it first: shared under whose rules? Part of the answer is already in the design. Each community runs its own registry, under its own authority, and decides who administers it and who it grants the right to commit. We don’t decide who any community lets commit. The next step is richer rules that each community writes for itself: who may join, which roles carry the right to commit, and who may revoke it. Those belong in open specifications, not in our code. The Trust Tasks specifications are developed in the open, in a task force of Trust Over IP’s Decentralized Trust Graph Working Group, and are versioned publicly. The process for deciding on changes is still being firmed up. Others are already building on the same foundations, such as Harvard’s Applied Technology Lab (ATL), whose Keyring app builds on the same Decentralized Trust Graph credential specifications.

Thank you to the people building OpenVTC with us, including Drummond Reed, Geoff Turk, Brendan Miller, Alberto Leon and Sankarshan Mukhopadhyay.

The wider lesson

A signature tells you who signed. Whether they are still allowed is the community’s call, and that answer belongs in one place that the community can check and change. Code is only the first place this matters. Swap the commit for a scientific dataset, a news article or a professional sign-off, and the two questions are the same: who made this, and were they allowed to? Any community can make its membership mean something that others can check. Every project should be able to say who can commit before something goes wrong. If you maintain something others depend on, try one exercise this week. List everyone who could get a commit into your main branch tomorrow, and how long it would take to remove each of them. If you are in Prague this week, I would love to chat about how your project handles that. I am running a hands-on OpenVTC workshop at Linux Plumbers, giving a talk with a live demo there on Wednesday morning, and presenting again at Open Source Summit Europe on Friday. Nobody should have to wonder who changed the code, or who allowed it.

Where this stands: the signer and the commit check are open source in OpenVTC’s verifiable-git-infrastructure repository; the trust registry is open source; and the Trust Tasks specifications are working drafts that will continue to change. The place to start is openvtc.net.

Sources

Open by defaultVerifiable identity

Cookie Preferences

We use cookies to enhance your experience. You can manage your preferences below. For more information, read our Cookie Policy.

Strictly Necessary Always Active

These cookies are essential for core website functions such as security, session integrity, and cookie preference storage. They cannot be disabled.

  • _cf_bm: Distinguishes humans from bots (Cloudflare) · 30m
  • _cfuvid: Ensures secure browsing (Cloudflare) · Session
  • __hs_initial_opt_in: Prevents HubSpot's banner · 7 days
  • _gtm_debug: GTM debug mode (testing only) · Session
Analytics

These cookies help us understand how visitors interact with the site so we can improve content and performance. All data is aggregated and anonymous.

  • _ga, _gid, _gat: Google Analytics · Session – 2 years
  • __hstc, hubspotutk, __hssrc: HubSpot visitor tracking · 13 months
  • __hs_opt_out: HubSpot opt-out preference · 6 months
Marketing & Targeting

These cookies allow us and our partners to serve personalised ads and measure campaign performance.

  • _gcl_au, _gcl_dc: Google Ads conversion tracking · 90 days
  • IDE: Google Display Network personalisation · 1 year
  • _fbp: Meta / Facebook remarketing · 90 days
  • li_gc, _li_fat_id, bcookie: LinkedIn tracking · 1–24 months
  • guest_id, personalization_id: Twitter/X analytics · 2 years