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

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 async maincore() via trio.run()
  • The pipeline validates the target email, dynamically discovers probe modules from holehe/modules/, and executes all checks concurrently using trio.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.AsyncClient with 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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →