Holehe OSINT Tool Core Execution Flow: How Email Probes Run Asynchronously
Holehe's core execution flow is an asynchronous pipeline managed by Trio, starting from holehe/core.py's main() and maincore() functions, which parse CLI arguments, validate the target email, dynamically load probe modules, and concurrently execute over 200 site-specific checks using httpx.AsyncClient and trio.open_nursery().
Holehe is an open-source OSINT (Open Source Intelligence) tool that checks whether an email address is registered on hundreds of online services. Understanding its core execution flow helps security researchers, bug bounty hunters, and OSINT practitioners customize probes, debug failures, or integrate the tool into larger workflows. This article breaks down exactly how the tool runs, from command-line invocation to final result export, based on the actual source code in megadose/holehe.
Entry Point: From main() to trio.run(maincore)
The execution begins in holehe/core.py. The main() function serves as the synchronous entry point and immediately hands control to Trio's event loop:
# holehe/core.py (simplified conceptual flow)
def main():
# Version banner and argument setup
trio.run(maincore) # Delegates to async core
The maincore() async function contains the complete asynchronous execution flow. It orchestrates all subsequent steps: parsing, validation, module discovery, and concurrent probing.
Phase 1: Argument Parsing and Email Validation
CLI Argument Processing (lines 80–96)
maincore() builds an ArgumentParser to handle command-line flags:
--only-used— Filter results to show only confirmed registrations--csv— Export results to a timestamped CSV file- Timeout configuration for HTTP requests
The target email is extracted from positional arguments: email = args.email[0].
Email Format Validation (lines 94–105)
Before any network activity, Holehe validates the input using the is_email() function:
from holehe.core import is_email
if not is_email(email):
# Fail fast with clear error; no wasted probes
raise ValueError(f"Invalid email format: {email}")
This regex-based check prevents useless API calls against malformed addresses.
Phase 2: Dynamic Module Discovery and Loading
Holehe's modularity allows new site checks to be added without modifying core logic. Two functions in holehe/core.py handle this:
import_submodules() (lines 37–47)
This function recursively imports all Python modules under holehe/modules/, the directory containing service-specific probes:
from holehe.core import import_submodules
modules = import_submodules("holehe.modules")
# Returns dict mapping module names to module objects
Each module (e.g., holehe/modules/social_media/twitter.py) contains one or more async functions that query a specific service.
get_functions() (lines 50–64)
After loading modules, get_functions() filters and returns a list of callable probe functions:
from holehe.core import get_functions
sites = get_functions(modules)
# List of async functions like twitter, github, instagram, etc.
This dynamic discovery makes Holehe extensible—dropping a new Python file into holehe/modules/ automatically includes it in the next run.
Phase 3: Async HTTP Client and Progress Instrument Setup
Shared httpx.AsyncClient (line 13)
Holehe creates a single httpx.AsyncClient instance with configurable timeout:
client = httpx.AsyncClient(timeout=args.timeout)
Using one shared client across all probes reduces connection overhead and enables connection pooling.
TrioProgress Instrument (lines 16–18, holehe/instruments.py)
To provide user feedback during potentially long-running checks, Holehe attaches a custom Trio instrument:
from holehe.instruments import TrioProgress
trio.lowlevel.add_instrument(TrioProgress(len(sites)))
This integrates tqdm progress bars with Trio's task scheduling, showing live completion status across all probes.
Phase 4: Concurrent Probing with Trio Nurseries
The heart of Holehe's execution flow happens in lines 19–21 of holehe/core.py:
async with trio.open_nursery() as nursery:
for site in sites:
nursery.start_soon(launch_module, site, email, client, results)
launch_module() Wrapper (lines 66–78)
Each site function runs inside launch_module(), which provides standardized error handling:
- Success: The probe returns a dict with
name,domain,method,frequent_rate_limit,rateLimit,exists,emailrecovery,phoneNumber,others - Exception: Any failure is caught and converted to a standardized error result, preventing one failing probe from crashing the entire scan
# Conceptual structure of launch_module
async def launch_module(module, email, client, out):
try:
result = await module(email, client)
out.append(result)
except Exception as e:
# Standardized error capture
out.append(create_error_result(module.__name__, str(e)))
This structure ensures fault tolerance across 200+ external services with varying reliability.
Phase 5: Result Collection, Sorting, and Reporting
After all nursery tasks complete:
Result Sorting (line 23)
out.sort(key=lambda x: x['name']) # Alphabetical by module name
print_result() Formatting (lines 6–49)
Results display with color-coded symbols:
| Symbol | Meaning |
|---|---|
🟢 [+] |
Email used on this service |
🔴 [-] |
Email not used |
🟡 [x] |
Rate limited or blocked |
⚪ [!] |
Error during probe execution |
Optional CSV Export (lines 54–64)
When --csv is specified, export_csv() writes timestamped output:
holehe user@example.com --csv
# Creates: results_20240115_143022.csv
Phase 6: Cleanup and Termination
Lines 25–33 handle graceful shutdown:
await client.aclose() # Close HTTP connections
trio.lowlevel.remove_instrument(instrument) # Detach progress bar
# main() returns; program exits
Complete Programmatic Example
This Python snippet replicates the core execution flow for custom integrations:
import trio
import httpx
from holehe.core import import_submodules, get_functions, launch_module
async def run_holehe_probe(email: str, timeout: int = 10):
"""
Execute Holehe's full async pipeline programmatically.
Mirrors the internal flow in holehe/core.py.
"""
# Phase 1: Load and discover modules
modules = import_submodules("holehe.modules")
sites = get_functions(modules)
# Phase 2: Setup async infrastructure
client = httpx.AsyncClient(timeout=timeout)
results = []
# Phase 3: Concurrent execution via Trio nursery
async with trio.open_nursery() as nursery:
for site in sites:
nursery.start_soon(launch_module, site, email, client, results)
# Phase 4: Cleanup and return
await client.aclose()
results.sort(key=lambda x: x['name'])
return results
# Usage
email_results = trio.run(run_holehe_probe, "target@example.com")
for r in email_results:
print(f"{r['name']}: exists={r.get('exists')}")
Key Source Files and Their Roles
| File | Responsibility in Execution Flow |
|---|---|
holehe/core.py |
Entry point (main/maincore), argument parsing, email validation, module loading, nursery orchestration, result formatting and export |
holehe/instruments.py |
TrioProgress class; bridges Trio task scheduling with tqdm progress bars |
holehe/modules/**/*.py |
Individual async probe functions; each queries one service and returns standardized dict |
holehe/localuseragent.py |
User-Agent rotation utilities used by probes |
setup.py |
Package metadata and dependency specifications |
Summary
- Holehe's execution flow starts synchronously in
main(), then delegates to asyncmaincore()viatrio.run() - The pipeline validates the target email, dynamically discovers probe modules from
holehe/modules/, and executes all checks concurrently usingtrio.open_nursery() - Each probe runs inside
launch_module()with standardized error handling, populating a shared results list - Output is sorted alphabetically, printed with color-coded status symbols, and optionally exported to CSV
- The architecture is extensible (new modules auto-load), resilient (individual failures don't crash the scan), and efficient (shared
httpx.AsyncClientwith connection pooling)
Frequently Asked Questions
What is the purpose of trio.open_nursery() in Holehe's execution flow?
trio.open_nursery() creates a structured concurrency context where all probe tasks run simultaneously. It ensures that if any task raises an unhandled exception, all other tasks are cancelled and cleaned up properly. This is more robust than asyncio.gather() because it provides deterministic cleanup guarantees, which matters when running 200+ network-dependent probes that may hang or fail unpredictably.
How does Holehe discover new site modules without code changes?
The import_submodules() function in holehe/core.py (lines 37–47) uses Python's importlib to recursively import every Python file under holehe/modules/. The get_functions() helper then inspects each imported module for callable async functions. Adding a new probe is as simple as creating a new .py file in the appropriate submodule directory—no registry updates or core modifications required.
Why does Holehe use httpx.AsyncClient instead of requests or aiohttp?
httpx.AsyncClient provides native async/await support with modern HTTP/2 capabilities, connection pooling, and a clean API consistent with the synchronous requests library. According to the source code, Holehe instantiates one shared client (line 13 of holehe/core.py) to minimize connection overhead across hundreds of concurrent requests, with configurable timeout handling exposed via CLI arguments.
Can Holehe's execution flow be customized to run only specific site checks?
Yes. While the default get_functions() returns all discovered probes, you can filter the sites list before passing it to the nursery. For example, import the module list, then filter by function name or module path: sites = [s for s in get_functions(modules) if 'twitter' in s.__module__]. The modular architecture in holehe/core.py makes selective execution straightforward without modifying probe code.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →