How GitHub gave every repository a durable owner

GitHub had over 14,000 repositories. Fewer than half had clear ownership. Here’s how we gave every active repository a validated owner in under 45 days, archived the rest, and made ownership the foundation for everything that followed.

Mona floats above green geometric blocks that include a GitHub invertocat logo in a decorative scene.
| 9 minutes

GitHub has over 14,000 repositories across our primary internal GitHub organization. As of early 2025, there were over 11,000 non-archived repositories, the vast majority of which with no clear owner. For repositories attached to production services, we have historically had robust durable ownership, but for repositories with no associated service, there was no reliable way to tell who the owner is.

That gap became a recurring problem during our secret scanning remediation effort: while we could technically rotate a secret, doing so without knowing the repository owner was risky and often disruptive, and we had no clear way to route remediation work. Over the course of a month and a half, we validated ownership for every active repository, archived about 8,000 repositories that were no longer in use, and changed repository creation so that ownership was required from the start.

Our original ownership model

For years, GitHub has been tracking ownership for deployed services through our internal Service Catalog. Each service entry recorded metadata like which repository it lived in, which gave us a mapping from service to repository; the owning team; executive sponsor; and support information.

Here’s an example of the Repo Ownership app’s service ownership entry:

- team: github/repo-ownership-dev 
  repo: https://github.com/github/repo-ownership 
  name: repo-ownership 
  kind: moda 
  long_name: Repo Ownership 
  description: Service enforcing repo ownership across the org 
  maintainer: mrecachinas 
  exec_sponsor: stephanmiehe 
  ... 

Having this rich metadata enables service-centric workflows, such as incident response, on-call routing, vulnerability management, and compliance scoping.

Unfortunately, that relationship was many-to-one (i.e., a service could only be attached to a single repository, but a single repository could have multiple services). That meant if you started from a service, you could find the repository and its owners. But if you started from a repository and needed to find an owner, you had to reverse the lookup, and that only worked for repositories that mapped to a service in the first place.

That left a significant ownership gap that included team repositories, documentation repositories, internal tools, one-off project repositories, personal experiment repositories, and anything else that didn’t back a deployed service. Every time we needed to contact the owner of one of these “unowned” repositories, it required manual work: check the commit history, read the README, ask around in Slack, or make a guess based on the repository name.

For a one-off effort, that kind of ambiguity is annoying but manageable. For recurring security workflows that fan out across the entire organization, it presents a real risk. During our secret scanning cleanup, we spent too much time trying to find the right owners before we could make informed decisions about alerts.

Designing the new ownership model

Fundamentally, we needed repository ownership to be a first-class property. We considered storing ownership in a dedicated file within each repository or maintaining it in a centralized repository, but ultimately chose GitHub custom properties. This approach provided a native, structured, and organization-wide queryable way to manage ownership. It also enabled us to enforce enterprise and organization policies and rulesets selectively according to ownership type.

We created two custom properties: ownership-type and ownership-name.

  • ownership-type accepted three values: “Service Catalog,” “Hubber Handle” (a “Hubber” is what we call a GitHub employee), and “Team.” These covered the realistic range of repository ownership at GitHub. A repository either belongs to a service (with an on-call team and a defined lifecycle), a team (like a shared documentation repository or internal tool), or an individual (like a personal project or experiment).
  • ownership-name was a text field with light validation. Our GitHub App validated every value: Hubber handles were checked against actual membership in our GitHub organization, teams were verified to exist in the organization and have at least two members, and Service Catalog entries were confirmed against our Service Catalog itself. We were intentionally permissive on formatting. If someone typed @my-team instead of my-team, we accepted it. We wanted to make it frictionless to add ownership and lean on robust validation to catch invalid entries like nonexistent teams, former employees, and services that had been decommissioned.

Day-one coverage

Before we asked anyone to do anything, we built a periodic sync from Service Catalog to repository custom properties. Every repository that backed a known service had its ownership-type set to “Service Catalog” and its ownership-name populated automatically. That took care of about 1,500 service-backed repositories, leaving team repos, docs repos, one-off projects, and personal repos remaining.

The rollout

To roll this out, we built a GitHub App backed by a Kubernetes CronJob. The enforcement logic needed access to Service Catalog, the GitHub API, and a few internal systems, so a simple GitHub Actions workflow wasn’t sufficient.

The diagram below shows the repository ownership enforcement flow, from initial ownership scan through warning issue creation, automatic closure, or archival after 30 days.

Flowchart of repository ownership enforcement: repositories are scanned for ownership, missing ownership opens a 30-day archive warning issue, unresolved issues lead to archiving, and resolved issues are closed automatically.

We scheduled the first run of the CronJob for a Saturday morning thinking nobody would be paying attention… Big mistake! Issues started appearing in repositories across the organization, and people began jumping on Slack, asking about this new issue in their repository saying it would be archived. At a globally distributed company, someone is always online.

After the 30-day grace period, we archived any repository that still didn’t have ownership set. We chose archiving because it’s reversible and non-destructive: the repository becomes read-only and GitHub Actions stops running, but nothing is deleted. If someone needs it again, we provided an easy way for them to unarchive it, set ownership, and continue. That enabled us to safely apply archival broadly instead of debating every edge case.

Once the initial grace period passed and the bulk of archiving was done, we tightened the enforcement loop from 30 days to one hour. A new repository that somehow bypassed the creation-time ownership requirement would get flagged almost immediately.

The sharp edges

This mostly rolled out seamlessly, with two minor internal incidents exposing some interesting edge cases.

The first incident was caused by archiving a repository where ownership had not been applied. Datadog had been configured to open issues in that repository as part of a monitoring workflow. When the repository was archived and Datadog couldn’t create the issue, our internal monitoring service noticed and automatically paged the owning team, and they escalated to us.

That incident exposed a gap in how we notified. The ownership issues were landing in repositories, but nobody was getting notified directly. We fixed this by @-mentioning repository administrators and assigning all users with write access as a fallback on ownership issues. That way the issues couldn’t be buried or overlooked, and the people who could actually set ownership saw them immediately.

The second incident was a data reliability problem. While we were robust against a Service Catalog outage, we didn’t consider that it might return stale data or corrupted data. If bad data caused the app to think a batch of repositories had lost their Service Catalog entries when they hadn’t, we’d be mass-archiving repositories with perfectly valid owners.

To mitigate the risk of archiving legitimate repositories, we added a low water mark. During each run, prior to performing any actions, the app would tally how many archives it was about to perform and issues it was about to open. If the number exceeded a conservative threshold, it would bail out entirely and trigger a Datadog monitor rather than risk a bad run. If Service Catalog was unreachable, the job would skip Service Catalog validation and only check what it could verify independently.

Results, by the numbers

We finished with approximately 3,000 active repositories and 11,000 archived (up from about 3,000 archived at the start). The entire effort took under 45 days from the first (Saturday morning) run to steady state. Every active repository now has a validated owner, or it gets archived.

Many of those newly archived repositories hadn’t seen a commit in years: abandoned experiments, completed hackathon projects, and even one-person prototypes from 2008! Archiving them ultimately reduced our surface area and made the active repository inventory reflect reality.

Making ownership stick

Getting to 100% coverage is only useful if it stays at 100%, so we enforced ownership properties across every repository creation workflow, including the repository creation page (shown below), internal tooling and automation, making them mandatory for all new repositories.

GitHub repository creation page showing required ownership custom property fields.

We also tightened the enforcement loop: repositories that lose their ownership are now flagged within one hour rather than the original 30-day grace period.

Each ownership type has its own durability characteristics, and we designed around them. Service Catalog entries follow service lifecycle: when a service is deprecated, its repositories typically get archived too, and that’s the intended behavior. Teams are validated for having at least one member, and team existence and membership tend to be reasonably stable. Individual Hubber handles only become invalid when someone leaves the company, which usually means their personal repositories should be archived regardless. For any repository critical enough to outlast a single person, ownership should be a team or a service, not an individual.

What this means for you

You can implement a similar ownership model today using GitHub custom properties. Here’s the approach we’d recommend:

  • Define your ownership taxonomy. Decide which types of owners make sense for your organization. Services, teams, and individuals worked for us, but your categories might look different.
  • Create custom properties at the organization level. Set up an ownership-type property as a single-select with your allowed values, and an ownership-name property as text. Custom properties are queryable through the API and visible across the organization.
  • If you have a service catalog or asset inventory, sync it. Populating ownership for repositories you already track is the fastest way to get meaningful coverage before you start asking people to fill in gaps.
  • Enforce ownership at repository creation time. Make the properties required so the inventory stays clean going forward.
  • Build a grace-period workflow for existing repositories. Open issues with a reasonable deadline (we used 30 days), then archive repositories that go unclaimed. Because archiving is reversible and non-destructive, it’s a safe default.
  • Don’t run your first enforcement pass on a Saturday!
  • Build guardrails before you trust automation at scale. The low water mark and the @-mention fallbacks weren’t in the original design. They came from real incidents. If you’re building a system that archives repositories or opens issues at scale, assume your data sources will occasionally be wrong, and your notifications will sometimes get lost. Design for that from the start.

For more on custom properties, see the custom properties documentation.

Written by

Michael Recachinas

Michael Recachinas

@mrecachinas

Michael Recachinas is a Staff Security Engineer at GitHub, where he leads large-scale security programs focused on vulnerability management, secure development lifecycle tooling, and developer-first security automation. He has spent his career building systems that operate at scale and help teams make the secure choice the easy choice.

Related posts