Diagnosing a Commit-Mined Contact Batch That Bounced
You can take one bounced commit-mined batch, sort every address into a named cause with a count, recover the deliverable subset, and fix the miner so the next batch clears the threshold.
Key takeaways
- Because GitHub's privacy setting is on by default, a modern repo's author list is dominated by unroutable noreply addresses, and every one you send hits a dead domain.
- Bot accounts author real addresses that must never be contacted: dependabot signs commits as 49699333+dependabot[bot] and github-actions as 41898282+github-actions[bot], both on users.noreply.github.com.
- The events-API recovery path has a hard calendar cliff: it only surfaces a real email if the person committed publicly this calendar year with exposure on, so an inactive contributor is structurally unrecoverable.
- A verifier verdict of 'valid' is not 'deliverable' on developer domains, where catch-alls run up to 30% in B2B and roughly 23% of unverified catch-alls hard bounce.
- The operational safe zone is hard bounces under 1% and total bounces under 2%; Amazon SES reviews accounts at 5% and may pause sending above 10%.
- Market size decides whether salvage is worth it: Refolk's index holds 3,384 Rust profiles in the US against 967 in Germany, so a thin salvaged German-Rust batch is better re-mined than rescued.
You mined author emails from a repository's commit history, sent to them, and the bounce rate came back high enough to threaten your sending domain. This guide is for the person answerable for that batch - recruiting ops, revenue ops, or the deliverability owner - and it walks one already-sent, already-bounced list backward to root cause. By the end you can sort every address into a named bounce bucket with a real count, recover the deliverable subset, suppress what must never re-send, and fix the miner so the next batch clears the threshold.
Most guides on this topic explain how to set your own noreply address. That is not the job here. The job is auditing someone else's mined list after it has already bounced, and that means naming each bucket, running the real git commands, and knowing which wrong turns waste a day.
What a commit-mined bounce actually tells you
A high bounce on a commit-mined batch is rarely a data-decay problem. It is usually a structural one: most of the addresses were never routable to begin with. Because GitHub applies its noreply form by default when "Keep my email addresses private" is on, a modern repo's author list is dominated by addresses that point at a dead domain. Every one you sent hit users.noreply.github.com, which accepts no mail, and every one counted as a bounce.
So before you touch a verifier or blame list age, you need to know the composition of what you sent. The bounce log tells you what failed. The re-extracted repo tells you why. The reconciliation of the two is the whole diagnosis.
Here is the shape of the problem. A commit-mined list narrows sharply from raw addresses to humans you can actually reach, and each narrowing step is a bounce cause you can name and count.
Where a commit-mined batch loses addresses
- 100Raw author addresses
everything git log emits
- 55After noreply removed
masked addresses dropped
- 48After bot/role removed
dependabot, github-actions, support@ removed
- 40After verify
catch-alls and dead mailboxes cut
The stage figures above are an illustrative shape, not measured counts. Your own numbers come out of the reconciliation in the step-by-step. The point is directional: the largest single drop is the noreply layer, and it happens before any verifier runs.
The bounce thresholds you are being judged against
The threshold that matters is not one number but a set of provider ceilings, and you want to sit well under all of them. Amazon SES reviews accounts at a 5% bounce rate and may pause sending above 10%, so the risk to your domain is real and automatic, not a warning you can ignore.
Hard bounces are the ones that hurt. A hard bounce is a permanent failure from an invalid address, a non-existent domain, or a disabled mailbox, and it immediately damages sender reputation because it signals poor list hygiene. A soft bounce is temporary, but it does not stay harmless: Mailchimp converts a soft bounce to a hard bounce after 7 to 15 occurrences depending on engagement, and SendGrid retries a deferred message for up to 72 hours before blocking. Treat three or more consecutive soft bounces to one address as hard.
| Provider | Hard-bounce / total threshold | Action |
|---|---|---|
| SendGrid | hard bounce under 5% attempted | reputation/deliverability risk |
| Amazon SES | 5% review, over 10% pause | account paused |
| Practitioner target | hard under 1%, total under 2% | operational safe zone |
Read this table as a floor-and-ceiling. The provider rows are where enforcement kicks in. The practitioner row is where you want to live so that one bad batch does not tip you into enforcement. If your bounced batch put you above 5%, the domain is already at risk and step one is not optional.
The four buckets every mined address falls into
Every address in a commit-mined batch sorts into exactly one of four buckets, and the bucket decides its fate before you ever verify it. Getting the sort right is the difference between a clean recovery and a second bounce.
The two noreply formats depend on account age. Accounts created after July 18, 2017 get an ID number and username, in the form ID+USERNAME@users.noreply.github.com. Accounts created before that date, with privacy enabled before that date, get USERNAME@users.noreply.github.com. The distinction matters for recovery: the ID form maps reliably to a GitHub account by its user-ID part, while the username-only form breaks if the person later renamed their account.
| Bucket | Deliverable as-is? | Recovery path |
|---|---|---|
| Real mailbox | yes, after verify | SMTP verify |
| ID+username noreply | no | events API / .patch, then verify |
| Legacy username noreply | no | login known, mailbox often lost |
| bot/role ([bot], support@) | no, never | suppress, never contact |
The bot bucket is its own hazard. Bot commits carry bracketed-login noreply addresses: dependabot authors as 49699333+dependabot[bot]@users.noreply.github.com, and github-actions authors as 41898282+github-actions[bot]@users.noreply.github.com. The general app pattern is USERID+APP-NAME[bot]@users.noreply.github.com. In an active repo, these bots can author a large fraction of commits, so a raw mine skews toward addresses that must never be contacted. They inflate the batch without inflating the count of reachable humans.
Extract the git addresses cleanly
Re-clone the repo and pull a counted, deduplicated author-email list with one command. That list, not the ESP export, is your source of truth for what you actually sent. The ESP knows what bounced; only the repo knows what each address was.
The workhorse is git shortlog -sne, where -s suppresses descriptions for a count-only summary, -n sorts by number of commits per author, and -e shows the email address. Authors are unique by name plus email, so that gives you a deduplicated author-plus-email-plus-count list in one pass.
# Counted, deduplicated author-email list git log --all --pretty=format:'%ae' | sort | uniq -c | sort -rn > authors.txt # Or the canonical per-author summary git shortlog -sne --all # Masked (noreply) addresses grep 'users.noreply.github.com' authors.txt > bucket_noreply.txt # Bot / role addresses (require BOTH conditions) grep '\[bot\]@users.noreply.github.com' authors.txt > bucket_bot.txt grep -E 'support@github.com|no-reply@github.com' authors.txt >> bucket_bot.txt # Real mailboxes = everything not already bucketed grep -v 'users.noreply.github.com' authors.txt | \ grep -viE 'support@github.com|no-reply@github.com' > bucket_real.txt
Run inside a fresh clone. The first line is the counted list; the greps split it into buckets.
One caution on canonicalisation. The --use-mailmap flag coalesces alias names and emails to canonical identities, which is useful, but it can also collapse two different people or hide an alias. Check the raw %ae output alongside the mailmapped view so a mailmap does not quietly merge or mask an address you needed to see.
If the salvage math comes out thin, this is the exit. Rather than nurse a broken batch back to health, Refolk lets you ask for the people you want in plain English and get back addresses that are already verified, which is often cheaper than recovering a masked German-Rust list of a few dozen contributors.
The teardown: run the batch backward to root cause
This is the procedure. Follow it on your own case as you read. The counts in the walkthrough below are worked examples on a hypothetical 100-address batch; substitute yours at each fork.
Diagnose one bounced commit-mined batch
- Freeze and quarantine the batchStop the campaign and pull the ESP bounce log with SMTP codes. Done when every sent address carries a recorded bounce or delivered status and a 5xx versus 4xx code.
- Re-extract the source repoRe-clone and run git log --all --pretty=format:'%ae' | sort | uniq -c | sort -rn. Done when you have a counted, deduplicated author-email list that matches the sent batch.
- Bucket every address by patternGrep for users.noreply.github.com, [bot], and known role domains. Done when every address is tagged noreply-id, noreply-legacy, bot/role, or real-mailbox.
- Reconcile buckets against bounce codesJoin the bucket list to the bounce log so each bounce has a named cause. Done when every bounced address sits in exactly one cause bucket with a count.
- Attempt recovery on the salvageable subsetFor noreply and masked addresses, resolve the login via the events API or the .patch trick, then verify surviving real addresses by SMTP. Done when you have a deliverable subset and an unrecoverable subset, both counted.
- Suppress permanentlyAdd every hard bounce and every noreply or bot address to the ESP suppression list. Done when those addresses can never re-send.
- Fix the mining stepAdd noreply, bot, and role filters to the extractor before it ever emits a list. Done when a dry run on the same repo produces zero noreply or bot addresses.
- Re-verify and re-projectRun the cleaned list through a verifier and recompute the projected bounce rate. Done when projected hard bounce is under 1% and total under 2%.
Worked example, address by address
Take a batch of 100 sent addresses that came back at 8% hard bounce - already past the SES review line.
Step 1, freeze. Pull the log. Say 62 delivered, 38 bounced, of which 30 are 5xx (hard) and 8 are 4xx (soft). That 30% hard is the headline, but the campaign-level 8% was diluted by earlier clean batches; this one is worse than the average implied.
Step 2, re-extract. The repo yields 100 unique author emails matching the sent list. Good - the mine and the send agree, so the problem is in the addresses, not in a join error.
Step 3, bucket. The greps split it: 40 on users.noreply.github.com (34 ID-form, 6 legacy), 7 bot/role (5 dependabot and github-actions, 2 support@github.com), 53 real mailboxes.
Step 4, reconcile. Join buckets to bounce codes. The result is the payoff table: 40 noreply and 7 bot/role addresses account for 30 hard bounces between them (some noreply addresses had been silently dropped by the ESP and never counted, which is why the numbers do not sum cleanly - note that and move on). Of the 53 real mailboxes, 8 soft-bounced and the rest delivered. Now every bounce has a named cause.
Step 5, recover. The 40 noreply addresses are the salvage candidates. This is where the calendar cliff bites, covered in the next section. Suppose 12 of the 34 ID-form addresses resolve to a real email via the events API and pass SMTP verify; the other 22 do not. The 6 legacy addresses give you logins but no mailbox. Deliverable subset from recovery: 12. Unrecoverable: 28.
Step 6, suppress. All 30 hard bounces, all 47 noreply-and-bot addresses, and any address that failed verification go on the suppression list. They can never re-send.
Step 7, fix the miner. Add the noreply and bot filters from the template above so the extractor never emits those addresses again. Dry-run the same repo: it should now emit only the real-mailbox bucket plus recovered addresses.
Step 8, re-verify and re-project. Run the surviving real mailboxes plus the 12 recovered addresses through a verifier. If catch-alls and dead mailboxes cut that to, say, 50 clean addresses, project the bounce rate on those and confirm it lands under 1% hard and 2% total before you re-send anything.
The bounce log tells you what failed; the re-extracted repo tells you why; the reconciliation of the two is the whole diagnosis.
Recovering a real email, and where recovery hits a wall
Recovery works for a masked address only through public traces outside git, and it fails silently more often than it succeeds. The ID+username noreply form already yields the GitHub ID and login, but never a mailbox. To get a real address you fall back to the public events endpoint at api.github.com/users/USERNAME/events/public, or you append .patch to a commit URL to reveal the committer's email - and the .patch trick only works if the user had not set their email to private.
The hard limit is a calendar cliff. The events fallback only surfaces an email if the person made a commit to a public repo during the current calendar year and had their email exposed when they made it. An inactive contributor is structurally unrecoverable regardless of tooling, so check the date of their last public commit before you conclude anything. A stale events window returns nothing and looks identical to a genuine dead end.
Should you recover this masked address or re-mine?
Market size sets that judgement. In Refolk's index, the same skill is far larger in one market than another, and that decides whether salvage is worth the hour.
| Segment | Count | Derived |
|---|---|---|
| Rust, Germany | 967 | baseline |
| Rust, United States | 3,384 | 3.50x Germany |
| Go, Germany | 4,840 | 5.01x Rust-Germany |
The counts above come from Refolk's index; the multiples are derived from them. Read them as a re-mine-or-rescue signal. A thin salvaged German-Rust batch sits against only 967 addressable people, so rescuing 12 masked addresses may be worth the hour. The same effort on a US-Rust or German-Go segment, which are 3.5x and 5x larger, is usually wasted: re-mine instead.
How this goes wrong: the false positives that waste a day
Every step above has a failure mode that produces a confident wrong answer. These are the ones that cost you a second bounce or a dropped real contributor, so treat this section as the checklist behind the checklist.
- Regex over-matches bots. A bare
[bot]filter also blocks a human whose login literally contains "bot", dropping a real contributor. Check that the domain isusers.noreply.github.comand the login ends in[bot]before you drop it. - Spoofed author. Email is a user-controlled field, so if someone commits in another person's name it resolves to the wrong person. Cross-reference the login against the commit's verified signature before you trust the identity.
- Catch-all "valid" verdicts. A catch-all server accepts all mail, so a verifier cannot confirm a mailbox exists. That produces a "valid" that hard bounces. Catch-alls run up to 30% in B2B and about 23% of unverified catch-alls bounce, so treat every catch-all as risky, not deliverable.
- Hashed events email. Some historical events emit a SHA-1-hashed local part, for example 40 hex characters
@gmail.com. Send to it and it bounces. Reject any local part matching^[0-9a-f]{40}$. - Soft bounce misread as recoverable. A repeatedly deferred mailbox is effectively dead. Treat three or more consecutive soft bounces as a hard bounce and suppress it.
- Mailmap masking duplicates.
--use-mailmapcan collapse two people or hide an alias. Check the raw%aeoutput alongside the mailmapped view. - Stale events window. The events fallback returns nothing if the last public commit predates this calendar year, producing a false "unrecoverable". Check the last-commit date before you conclude the mailbox is gone.
The catch-all trap deserves the most weight because it survives every earlier filter. Your buckets are clean, your recovery worked, your verifier passed the address - and it still bounces, because a catch-all domain accepted the SMTP probe without a mailbox behind it. On developer domains this is common enough to breach the 2% total-bounce line on its own. When a segment is heavy with catch-alls, the honest move is to hold those addresses out of the near-threshold batch rather than gamble the domain on them.
Before you call the batch salvaged
Run this list before you resend anything. Each item is a checkable state, not a topic, and skipping one is how a "fixed" batch bounces again.
Salvage sign-off
- Every sent address has a recorded bounce or delivered status with a 5xx or 4xx code.
- The re-extracted repo list matches the sent batch, confirmed against raw %ae output not just the mailmap.
- Every address sits in exactly one bucket: noreply-id, noreply-legacy, bot/role, or real-mailbox.
- Every bounce is joined to a named cause with a count.
- Every hard bounce and every noreply or bot address is on the ESP suppression list.
- No recovered address has a 40-hex-character local part.
- Catch-all addresses are held out of the resend, not counted as deliverable.
- The miner has noreply, bot, and role filters and a dry run emits zero of them.
- The projected hard bounce is under 1% and total under 2% on the cleaned list.
Keep the fixed pipeline honest
The fix is not the suppression list; it is the filter that runs before the extractor emits a single address. Once the miner drops noreply and bot addresses by default, the noreply layer - your largest single bounce cause - never reaches a send again. That is the difference between diagnosing one batch and never having this batch again.
Two things drift and need a re-check. First, contact data decays roughly 22% to 30% per year, so a real mailbox verified today is not verified in twelve months; re-verify before any list older than a quarter goes out. Second, the recovery calendar cliff resets every January: an address that was unrecoverable last year because the person went quiet may resolve again if they commit publicly this year. Re-run recovery on your unrecoverable subset once per calendar year before writing those contacts off for good.
When the salvage math is thin - a small segment, a heavy noreply share, a catch-all-dense domain - the honest call is to stop rescuing and start over from a source that returns verified addresses. Asking Refolk for the people you want by role, skill, and place gives you a list that has already cleared the deliverability threshold, which beats another pass over a batch that was mostly unroutable to begin with.
Questions practitioners ask
How do I tell a hard bounce from a soft bounce in the log?
Read the SMTP code. A 5xx code is a hard bounce: a permanent failure from an invalid address, a non-existent domain, or a disabled mailbox, and it immediately damages sender reputation. A 4xx code is a soft, temporary failure. Treat repeated soft bounces as effectively hard: SendGrid retries a deferred message for up to 72 hours before blocking, and three or more consecutive soft bounces to one address should be handled as dead.
What bounce rate actually puts my sending domain at risk?
Amazon SES places accounts under review at a 5% bounce rate and may pause sending above 10%, and SendGrid recommends keeping hard bounces under 5% of attempted messages. Those are ceilings, not targets. The practitioner safe zone is hard bounces under 1% and total bounces under 2%, with top performers under 0.5% hard. Aim for the safe zone, not the ceiling, because a single bad batch can push you past it.
Can I recover a real email from a GitHub noreply address?
Sometimes, and only for the mailbox, not always. The ID+username form gives you the GitHub ID and login reliably, but not a mailbox. To find a real address you fall back to the public events API or to appending .patch to a commit URL, and both only work if the person committed to a public repo this calendar year with their email exposed at the time. If they always committed under the masked address, the mailbox is unrecoverable from git alone.
Why did a verifier mark an address valid and it still bounced?
Almost always a catch-all domain. Catch-all servers accept all mail, so a verifier cannot confirm a specific mailbox exists behind the address. Catch-alls run between roughly 8.6% and 15.25% of typical lists and up to 30% in B2B, and about 23% of unverified catch-alls hard bounce. Treat any catch-all verdict as risky, not deliverable, and keep those addresses out of a batch that is already near the threshold.
How do I detect bot commits programmatically without dropping real people?
Match on the [bot] marker in the author name or email, but require two conditions, not one. Confirm the address is on users.noreply.github.com and the login ends in [bot]. The single-marker regex over-matches: it will block a human whose login literally contains the string bot. The documented bot addresses, such as 49699333+dependabot[bot] and 41898282+github-actions[bot], both satisfy the stricter test.
Try it on the search you came here for
Stop building boolean strings. Just describe the person.
Type one sentence. I plan the search, read GitHub, public LinkedIn and Crunchbase records, and the open web as it is right now, and hand back a ranked list with the reason next to every name.
01Describe them
One plain sentence. Role, city, stack, stage, whatever matters to you.
02I read the web live
GitHub, public LinkedIn and Crunchbase records, the open web. Not a database that went stale last quarter.
03You read the shortlist
Ranked, with the reasoning under every name. Open a profile, ask a follow-up, narrow it down.
- 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
- No boolean, no filters, no seat to buy. One box.
- Read at search time, so a profile updated yesterday counts today.
- Every step visible as it runs, every name with its reason.
500 free credits on sign-up. No card, no demo call. See real searches.