Back to blog
Micro Post

Linking Emails to GitHub Accounts

| osintgithub

"It is a capital mistake to theorise before one has data." — Sherlock Holmes

If you have someone's email address and want to know their GitHub account, GitHub will actually tell you, you just have to ask in the right way.

GitHub automatically links every commit to a user profile by matching the author email against its internal index of verified email addresses. This feature exists so that contributors are properly credited when their code is merged into repositories. But the same mechanism works in reverse: if you craft a commit with a target email address and push it to a repository, GitHub will attempt to resolve it to a user account, effectively turning the platform into a reverse email lookup service.

How auto-linking works

When a commit is pushed to GitHub, the platform's backend asynchronously checks the author email against an internal mapping of verified email addresses to user accounts. If a match is found, the commit's API response is populated with the linked account's login, id, and avatar_url fields.

This resolution happens after the push, not during it, which is why the technique requires polling. The first API call immediately after pushing may not have the link resolved yet, as GitHub needs a moment to process the commit and match the email against its index.

The technique exploits this by reversing the intended flow: instead of pushing someone else's commit and waiting for GitHub to credit them, you craft a commit with the target email and let GitHub tell you who it belongs to.

The technique

sequenceDiagram
    participant You
    participant GH as GitHub API

    You->>GH: Create temporary private repo
    You->>GH: Push commit with target email as author
    loop Poll (up to 8 attempts, 1s intervals)
        You->>GH: GET /repos/{owner}/{repo}/commits/{sha}
        GH-->>You: author.login populated?
    end
    You->>GH: Delete temporary repo

The process involves five steps:

  1. Create a temporary private repository via the GitHub API
  2. Push a commit where author.email is set to the target email address
  3. Poll the commit endpoint repeatedly until author.login is populated, or until you hit the timeout
  4. Record the linked account if one is found
  5. Delete the temporary repository to clean up after yourself

The commit itself doesn't need to contain anything meaningful. A single file with a dummy message is sufficient, because what matters is the author email field in the commit metadata, not the content of the changes.

Implementation

import requests, base64, time, random, string

API = "https://api.github.com"

def probe_email(token, email):
    headers = {
        "Authorization": f"Bearer {token}",
        "Accept": "application/vnd.github+json",
    }

    # Create temp repo
    repo = f"tmp-{''.join(random.choices(string.ascii_lowercase, k=8))}"
    requests.post(f"{API}/user/repos", headers=headers,
                  json={"name": repo, "private": True, "auto_init": True})

    owner = requests.get(f"{API}/user", headers=headers).json()["login"]

    # Commit with target email
    result = requests.put(
        f"{API}/repos/{owner}/{repo}/contents/probe.txt",
        headers=headers,
        json={
            "message": "probe",
            "content": base64.b64encode(b"probe").decode(),
            "author": {"name": "Probe", "email": email},
        },
    ).json()

    sha = result["commit"]["sha"]

    # Poll for auto-link resolution
    username = None
    for _ in range(8):
        data = requests.get(
            f"{API}/repos/{owner}/{repo}/commits/{sha}",
            headers=headers
        ).json()
        if data.get("author", {}).get("login"):
            username = data["author"]["login"]
            break
        time.sleep(1)

    # Clean up
    requests.delete(f"{API}/repos/{owner}/{repo}", headers=headers)
    return username

The function creates a throwaway private repository, pushes a single commit authored with the target email, polls the commit endpoint until GitHub resolves the link (or times out after 8 attempts), and then deletes the repository to clean up. The use of a private repository ensures that the probe commit is never visible to anyone other than the account owner.

Requirements and limits

This technique requires a GitHub Personal Access Token with repo and delete_repo scopes. The API rate limit allows roughly 800 to 1,000 probes per hour, which makes it practical for batch lookups across a list of target emails.

Each individual probe takes around 8 to 10 seconds due to the polling interval, so throughput is limited more by the asynchronous resolution time than by API rate limiting itself.

Countermeasures

The most important step is to enable GitHub's "Keep my email addresses private" setting under your account's email preferences. Auto-linking only resolves against verified, non-private email addresses, so if your email is set to private, this technique will not match it to your account.

It's also worth reviewing which email addresses are currently verified on your GitHub account under Settings > Emails. Any verified email that isn't set to private can potentially be used to link back to your profile using this method, even if it doesn't appear publicly on your account page.

Be aware that even with private emails enabled, your noreply alias (username@users.noreply.github.com) still identifies your GitHub account, so the privacy protection is specifically about preventing resolution of your real email addresses, not about making your account undiscoverable.


In Project Eyrie, ReverseKraken automates this flow with retry logic, random commit dates, and full profile data retrieval.

Part of Project Eyrie — by notalex.sh

Open-source tooling for intelligence, investigations, and OSINT.