Clearing a Commit-Mined Contact Batch for Lawful Outreach
Run any commit-mined export through a documented clearance funnel and end with a per-segment go/hold/drop decision, intermediate counts, and a defensible paper trail.
You have an export of contacts pulled from public GitHub commits, and before it reaches the team you have to decide, record by record, which addresses are lawful and safe to send. This guide is for the person answerable for how that batch was gathered: recruiting operations, revenue operations, or whoever signs off the paper trail. It carries one real commit-mined export through a full clearance funnel, keeps the counts that survive each stage, names the wrong turns, and ends with a per-segment go, hold, or drop you can defend in an audit.
The top search results on this topic argue whether scraping is legal in the abstract. That is not the job in front of you. The job is a filtering sequence and a disposition record for the batch on your screen, so that is what this hands you.
What "cleared" means for a commit-mined batch
A commit-mined batch is cleared when every surviving record is reachable, correctly attributed, jurisdiction-tagged, covered by a filed legitimate-interest assessment, checked against suppression, and paired with an Article 14 notice where the law requires one. Anything short of that is not cleared; it is a liability with a send button.
Start from the constraint that governs the whole exercise. GitHub's Acceptable Use Policies state that you may not use information from the Service, "whether scraped, collected through our API, or obtained otherwise", for spamming purposes, "including for the purposes of sending unsolicited emails to users or selling personal information, such as to recruiters, headhunters, and job boards." That sentence names your exact use case. It does not make every send impossible, but it means "I found the email in public" is never a sufficient answer on its own.
The funnel below exists to convert a raw export into records where you can show your work. The paper trail is the deliverable. If a regulator or your own legal team asks why a given person received an email, the answer has to be a stored record, not a memory of a judgement you made.
The clearance funnel for a commit-mined export
- 100%Raw deduped rows
one row per distinct author email
- after no-reply dropReachable mailboxes
users.noreply.github.com removed
- after bot removalHuman authors
automation identities gone
- after web-flow/squash holdAttribution-clean
quarantined rows set aside
- after LIA + suppressionSendable segment
go/hold/drop assigned
The dossier is honest about one gap, and so am I: no published benchmark gives the stage-by-stage dropout percentages for a commit-mined export. The qualitative signal is strong, though. For accounts created today the commit email defaults to the id+login no-reply form and the public profile email is empty; older accounts that opted in still expose addresses but are a minority. So expect the no-reply and bot stages to remove a large, unpredictable share. Measure your own batch and record the counts. Do not import a percentage from anywhere, including here.
Stage 1: strip the addresses that route nowhere
Any address on the hostname users.noreply.github.com is unreachable and belongs in DROP, because those addresses are not routed anywhere. This is the single highest-yield filter and the one most often skipped.
There are two no-reply formats, and you should match both. If an account was created after 18 July 2017, its no-reply email is ID+USERNAME@users.noreply.github.com. If an account was created before that date with "Keep my email address private" enabled, the format is USERNAME@users.noreply.github.com. You do not need to distinguish them for disposition, because both drop. You only need the hostname.
The first failure mode in the dossier is treating these as reachable. They bounce or vanish, they inflate your deliverable count, and they make every downstream metric a lie. Match the hostname before any send and before you report a batch size to anyone.
# route to DROP (unreachable): .*@users\.noreply\.github\.com$ # extract the username for later cross-reference: # from ID+USERNAME@users.noreply.github.com -> USERNAME (after the +) # from USERNAME@users.noreply.github.com -> USERNAME (before the @)
Match on the hostname, not the local part. Anchor to the end of the string so you do not catch look-alikes.
One warning that comes straight from a practitioner in the dossier: anchor the regex. A filter author who wrote "noreply" without anchoring rejected legitimate Dependabot security updates and called it embarrassing. An unanchored match will also catch strings you did not mean to catch. Anchor to the hostname, log every drop, and keep the log.
Stage 2: remove bots and role accounts
Automation identities are not people and cannot lawfully receive prospecting email, so filter the [bot] suffix, dependabot, github-actions, and noreply@github.com committers before you look at anything else. The catch is that bot commits are difficult to detect, so pattern-matching alone is not enough.
Two concrete bot patterns show why. Dependabot commits as dependabot[bot]@users.noreply.github.com, which the no-reply filter already catches, but the identity itself is worth flagging so it never resurfaces. The GitHub Actions bot uses 41898282+github-actions[bot]@users.noreply.github.com. Both hide behind the no-reply hostname, which is convenient, but not every automation identity does.
Do not lean on the GitHub REST API's User-versus-Bot field to do this for you. Researchers found the User endpoint sometimes returns incorrect values for that field. So combine the pattern match with a manual check of your top committers by volume, since those are the accounts most likely to be automation and the ones whose misclassification does the most damage.
| Pattern | Disposition | Source |
|---|---|---|
| ID+USERNAME@users.noreply.github.com | Drop (unreachable) | GitHub email-addresses reference |
| USERNAME@users.noreply.github.com | Drop (unreachable) | GitHub email-addresses reference |
| noreply@github.com (committer) | Hold (web-flow, authorship unreliable) | iter.ca commit-signing writeup |
| dependabot[bot]@users.noreply.github.com | Drop (bot) | dependabot-core issue 2369 |
Stage 3: verify who actually wrote the commit
Git records two identities per commit: the author, who wrote it, and the committer, who applied it. When those disagree in specific ways, the author field may not point at a real, reachable person, and the record belongs in HOLD until you resolve it.
Two operations rewrite these fields. A commit created on GitHub.com through the web interface is signed with GitHub's web-flow key and carries a committer of GitHub <noreply@github.com>. A squash merge alters the committer and commit date to the person who pressed the merge button, alters the author date to match, and alters the author email to the default address associated with the account. In both cases, the author email you mined may be a system default rather than the person's live mailbox.
This is the second and third failure mode in the dossier, and they are the subtle ones. Trusting web-flow authorship means emailing an address that was never a mailbox. Squash-merge misattribution is worse: it credits a real person for a commit whose author fields were rewritten, so you send to the wrong human with confidence. Several commits have lost attribution to the original author because of the squash button.
The check is mechanical. Compare author against committer, and author date against commit date. Where the committer is the web-flow identity, or where the fields disagree in the squash pattern, quarantine the row.
Author versus committer disposition
If you want to confirm a web-flow signature locally rather than infer it, GitHub publishes its web-flow public key so you can verify the signature against it. That is the ground truth when a record is worth the effort.
Stage 4: segment by jurisdiction and file the assessment
Once you know which records are reachable and correctly attributed, split them by jurisdiction and complete a legitimate-interest assessment for each segment, because the legal obligations diverge sharply at the EU border. This is where a mechanical data problem becomes a compliance one.
GDPR Article 14 governs data you obtained about a person from somewhere other than the person, which is exactly a commit-mined address. The controller must provide the required information "within a reasonable period after obtaining the personal data, but at the latest within one month; if the data are to be used for communication with the data subject, at the latest at the time of the first communication." In plain terms: for EU records, the first email is your deadline for the notice, not a milestone you catch up on later.
The lawful basis for that first contact is typically legitimate interest, and the ICO is explicit that you must do the three-part test before you start using the personal information and document the outcome. Passing it in your head carries no weight. If a regulator asks, thinking it through counts for nothing unless you can show your work, and the LIA is how you show it. Store the completed assessment in a shared compliance folder that RevOps and legal can both reach.
Two edges to watch. First, B2B is not a blanket exemption. Under UK PECR, consent rules apply to "individual subscribers", and sole traders count as individual subscribers even when they look like a business. Verify the entity type before you rely on legitimate interest. Second, the ordering question is genuinely contested. The ICO position implies the LIA should precede any use of the data, which would put it first. Practitioner workflows run the mechanical filters first so the LIA only has to cover the records that survive. Both are defensible; what is not defensible is a send with no written LIA behind it.
Sizing the segment before the LIA is also where a search index earns its keep. Rather than mining a repo and discovering afterwards how many reachable EU contacts you actually have, Refolk lets you ask for the segment directly and see the count before you commit to the assessment work.
The step-by-step procedure
Run the export through these eight stages in order, recording the surviving count after each. The output is a per-segment go, hold, or drop with the intermediate counts attached.
Commit-mined batch clearance funnel
- Ingest and dedupe the raw exportNormalize every author email to lowercase, key on the email, and count the raw rows. Done when you have one row per distinct author email, each with a stable record ID.
- Strip no-reply addressesRegex-match the hostname users.noreply.github.com and route every hit to DROP as unreachable. Done when every surviving row has a real, non-noreply mailbox.
- Remove bot and role accountsFilter the [bot] suffix, dependabot, github-actions, and noreply@github.com committers, then manually check the top committers because bot commits are hard to detect. Done when no automation identity remains.
- Verify authorship integrityFlag any record whose committer is GitHub noreply@github.com or whose author email was rewritten by a squash merge. Done when web-flow and squash-derived rows are quarantined for HOLD.
- Segment by jurisdictionRoute likely-EU records into the Article 14 track and tag the rest non-EU. Done when every row carries an EU or non-EU tag.
- Run the legitimate-interest assessment per segmentComplete the three-part test for each campaign segment and file it in the shared compliance folder. Done when a recorded LIA exists per segment before any send.
- Suppression-list matchMatch every surviving row against prior opt-outs and do-not-contact records and drop the hits. Done when no suppressed address survives.
- Attach Article 14 notice and opt-outFor EU rows, include the source-of-data disclosure in the first email and a functional unsubscribe link on every send. Done when you hold a per-segment go/hold/drop disposition with counts and a defensible paper trail.
The paper trail is the deliverable. A send you cannot explain is a liability with a button attached.
A worked example: sizing the segment before you mine
Here is the fork the funnel is built to expose. Suppose the campaign targets backend engineers who work in Rust, and you are deciding between the German and US markets before you spend a day mining commits and clearing them.
In Refolk's index, 611 people in the United States match "Software Engineer/Backend Engineer" plus Rust, against 92 in Germany. That is roughly a 6.6x gap. The point is not that the US is bigger; it is that you can see the reachable universe before you commit to the clearance work, rather than mining a repo and discovering the surviving count only after seven filtering stages.
| Market | Matching people | Multiple vs Germany |
|---|---|---|
| United States | 611 | 6.6x |
| Germany | 92 | 1.0x |
The employer mix tells you where those people actually sit, which shapes how you approach them and which corporate domains you will see in the reachable mailboxes.
| Rank | Germany | United States |
|---|---|---|
| 1 | Helsing (3) | Google (4) |
| 2 | Wolt (2) | Oxide Computer Company (3) |
| 3 | Trade Republic (1) | Meta (3) |
One honest limit from the dossier: a Senior-only band of this pool returned zero rows in both markets, so I cannot give you a seniority comparison here. When a slice comes back empty, that is data too. It tells you the band does not exist in the index at that granularity, and you should widen the query or drop the seniority filter rather than assume the tool failed.
How this goes wrong
Most of the damage in a commit-mined batch comes from a handful of specific, repeatable errors. Each one has a check that catches it, and the checks are cheap compared to the cost of getting caught.
- Treating no-reply addresses as reachable. They bounce or vanish because they are not routed anywhere. Check: match users.noreply.github.com before any send and before you report a count.
- Trusting web-flow commit authorship. The committer reads GitHub noreply@github.com and the author email may be a default, not a live mailbox. Check: inspect the committer field and quarantine web-flow rows.
- Squash-merge misattribution. A real person is credited for a commit whose author fields were rewritten, so you email the wrong human. Check: compare author against committer, and author date against commit date.
- Unanchored bot regex. An unanchored pattern rejected legitimate Dependabot security updates in the wild. Check: anchor every regex and log every rejection.
- Relying on the API bot flag alone. The User endpoint has returned incorrect User/Bot values. Check: cross-verify your top committers manually.
- Assuming public data needs no Article 14 notice. A common documented error is assuming publicly available data is exempt and relying on legitimate interest without an assessment. Check: confirm a filed LIA and a notice plan before send.
- Passing the LIA in your head. Thinking it through carries no weight unless you can show your work. Check: the written LIA must exist before send, not after an objection.
- B2B/B2C confusion. PECR treats sole traders as individual subscribers, so a business-looking contact may need consent. Check: verify entity type before relying on legitimate interest.
The regulatory risk here is not hypothetical. The CNIL has sanctioned SaaS vendors over B2B prospecting from purchased lists under Article 14. The mechanism that caught them, obtaining data indirectly and contacting people without a notice or a documented basis, is the same mechanism a careless commit-mined send triggers.
The pre-send checklist
Before the batch leaves your hands, walk this list. If any item fails, the batch is not cleared, and the segment that fails goes to hold or drop, not go.
Before this batch goes to the team
- Every surviving row has a non-noreply mailbox and users.noreply.github.com is fully removed.
- No [bot], dependabot, github-actions, or noreply@github.com committer identity remains, and top committers were checked by hand.
- Web-flow and squash-derived rows are quarantined in HOLD, not sitting in the send list.
- Every row is tagged EU or non-EU.
- A written legitimate-interest assessment is filed per segment in the shared compliance folder.
- The batch was matched against suppression and do-not-contact lists, with hits dropped.
- EU rows carry an Article 14 source-of-data notice in the first email.
- Every send includes a functional, clearly labeled unsubscribe link.
- Intermediate counts after each stage are recorded, so the funnel is reconstructable in an audit.
Keeping the clearance current
A cleared batch is a snapshot, not a permanent status, so the disposition record needs a review date and a refresh trigger. Two things change underneath you: the source rules and the people.
The source rules shift when GitHub updates its Acceptable Use Policies or its email-address behaviour, or when a data protection authority publishes new guidance on indirect collection. Do not memorise the current text. Instead, note the mechanism and re-read the primary source before each new campaign: the GitHub AUP for the use restriction, the GitHub email-addresses reference for the no-reply formats, Article 14 for the notice deadline, and the ICO's legitimate-interest guidance for the LIA requirement. Those are the four documents that govern the whole funnel.
The people change constantly. Addresses go stale, opt-outs accumulate, and today's default no-reply commit email means fewer real mailboxes surface from commits over time. Re-run the suppression match before every send, and treat any batch older than your review interval as unverified until it passes the funnel again. When the reachable pool from commits thins out, the faster path is to query for the segment directly and let the reachability and attribution filters apply before you ever see a list, rather than mining and clearing a repo by hand each time.
The whole method reduces to one discipline: never let "I found it in public" stand in for a decision you can show. The funnel turns a raw export into a set of records where every send has a reason on file, and that file is what protects the team when someone asks how the data was gathered.
Questions practitioners ask
Is scraping GitHub commit emails legal?
The legality depends on how you use the data, not just how you got it. GitHub's Acceptable Use Policies explicitly ban using Service information, whether scraped or collected through the API, for sending unsolicited emails or for selling personal information to recruiters, headhunters, and job boards. Even where collection is technically possible, that policy plus GDPR obligations can make outreach unlawful. Clear each record through the funnel before you send.
How do I filter GitHub noreply emails from a list?
Match the hostname users.noreply.github.com and drop every row that ends in it. Two formats exist: ID+USERNAME@users.noreply.github.com for accounts created after 18 July 2017, and USERNAME@users.noreply.github.com for older opt-in accounts. Both route nowhere, so both are unreachable and belong in DROP. Anchor your regex to the hostname so you do not accidentally catch or miss adjacent patterns.
Do I need an Article 14 notice for data I found in public commits?
Yes. A common documented error is assuming publicly available data needs no notification. GDPR Article 14 applies whenever you obtain personal data indirectly rather than from the person, and public visibility does not exempt you. You must notify at the latest within one month, or at the first communication if that comes sooner. File a legitimate-interest assessment and plan the notice before you send.
How do I detect misattributed commit authors?
Compare the author against the committer and the author date against the commit date. Web-flow commits carry a committer of GitHub noreply@github.com, and squash merges rewrite the committer, both dates, and the author email to a default. When those fields disagree or the committer is the web-flow identity, the author field may not identify a real, reachable person. Quarantine those rows for HOLD rather than sending.
Should the legitimate-interest assessment come before or after the mechanical filters?
Sources disagree. The ICO says the LIA must be completed before you use the data, which argues for doing it first. Practitioner workflows run the mechanical filters first so the LIA covers only the records that survive, which keeps the assessment scoped and cheaper. Either order is defensible as long as a written LIA exists before the first send, not after an objection arrives.
Try it on your own search
Stop building boolean strings. Just describe the person.
Type one sentence and I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web live, then hand back a ranked shortlist with the reasoning behind every name. No filters to learn, no export to clean up, no sales call to sit through.
- One sentence in, a ranked shortlist out. No boolean, no filters, no seat to buy.
- Read live at search time, not from a database that went stale last quarter.
- Watch every step as it runs, and see why each name made the list.
- Staff backend engineers in NYC who shipped Rust in production
- Series A fintechs in SF under 50 people, growing headcount this year
- Maintainers of fast-growing Rust web frameworks on GitHub
500 free credits on sign-up. No card, no demo call. See real searches.