How Holehe's Modular Architecture Works: Dynamic Plugin Discovery and Concurrent Execution

Holehe employs a plug-in architecture where every online service checker resides in its own Python module under holehe/modules/, enabling dynamic discovery and concurrent execution via Trio without modifying core logic.

Holehe, an open-source email OSINT tool maintained by megadose/holehe, leverages a modular architecture that separates service-specific logic from orchestration code. This design allows researchers to add new platform checks by simply dropping Python files into the modules directory, while the core system handles discovery, concurrency, and result aggregation automatically.

Plugin Structure and Module Organization

Service-Specific Modules in holehe/modules/

Each supported platform exists as an independent Python file within category subdirectories (e.g., holehe/modules/social_media/). According to the source code, every module defines a single async function matching the filename, such as facebook(email, client, out) in facebook.py (lines 6-12). This function receives an email string, an HTTP client instance, and a shared output list, returning a standardized dictionary describing whether the email exists on that service.

The Output Contract

All modules append dictionaries with consistent keys to the shared out list, including name, domain, exists, rateLimit, and error fields (as implemented in holehe/core.py lines 23-31 and 166-176). This normalization allows the core system to aggregate results from disparate services into a unified format for CLI display or CSV export.

Dynamic Discovery and Loading

Runtime Module Discovery with import_submodules()

The core orchestration logic in holehe/core.py (lines 37-46) implements import_submodules(), which uses pkgutil.walk_packages() to traverse the entire holehe.modules package tree at runtime. This approach eliminates manual registration; the system automatically detects new modules as files are added to the directory structure.

Function Extraction via get_functions()

Following discovery, get_functions() (lines 50-63 in holehe/core.py) inspects each imported module's namespace to extract the async callable. The function filters modules based on optional CLI flags like --no-password-recovery, building a list of service-specific functions ready for execution.

Concurrent Execution with Trio

Async Concurrency via launch_module()

Holehe utilizes the Trio async framework for concurrent checking. For each discovered service function, the system spawns a nursery task through launch_module() (defined in holehe/core.py lines 66-73). This wrapper function awaits the service-specific check and captures any exceptions, ensuring that one failed module does not crash the entire scan.

Progress Reporting with TrioProgress

To provide user feedback during long-running scans, Holehe implements a custom Trio instrument class TrioProgress in holehe/instruments.py (lines 4-10). This instrument hooks into Trio's task scheduling to update a tqdm progress bar each time a module completes its execution, giving real-time visibility into the scan status across potentially hundreds of services.

Extending Holehe with Custom Modules

Adding support for new platforms requires no changes to core files. Create a new file in the appropriate holehe/modules/<category>/ directory containing an async function matching the filename. The function must accept (email, client, out) parameters and append the standard result dictionary to the out list. The next runtime automatically discovers and executes the new module alongside existing services.

Practical Usage Examples

Command-Line Interface

Execute comprehensive email checks using the CLI entry point:


# Check every supported site

holehe user@example.com

# Show only sites where the email exists

holehe user@example.com --only-used

# Export results to CSV

holehe user@example.com --csv

Programmatic Integration

Integrate Holehe into Python applications using the core API:

import asyncio
import httpx
import trio
from holehe.core import import_submodules, get_functions, launch_module

async def check_email(email: str):
    # Load all plugins dynamically

    modules = import_submodules("holehe.modules")
    
    # Extract async service functions

    services = get_functions(modules)
    
    # Execute concurrently with shared output list

    out = []
    async with httpx.AsyncClient(timeout=10) as client:
        async with trio.open_nursery() as nursery:
            for svc in services:
                nursery.start_soon(launch_module, svc, email, client, out)
    
    # Return sorted results

    return sorted(out, key=lambda r: r["name"])

# Usage

results = asyncio.run(check_email("user@example.com"))
for r in results:
    status = "FOUND" if r["exists"] else "NOT FOUND"
    print(f"{r['domain']} → {status}")

Summary

  • Dynamic Discovery: import_submodules() in holehe/core.py uses pkgutil.walk_packages() to auto-detect modules under holehe/modules/ without manual registration.
  • Standardized Interface: Each service implements an async function matching its filename that accepts (email, client, out) parameters and returns normalized dictionaries with keys like exists and rateLimit.
  • Trio Concurrency: The launch_module() wrapper executes service checks concurrently within Trio nurseries, isolating failures via exception handling.
  • Zero-Code Extensions: New services are added by creating Python files in category subdirectories, with the core system handling the rest automatically.

Frequently Asked Questions

How does Holehe discover new modules without configuration files?

Holehe uses Python's pkgutil.walk_packages() in holehe/core.py (lines 37-46) to traverse the holehe.modules package tree at runtime, importing every submodule it finds. This eliminates the need for manual registration lists or configuration files when adding new service checks.

What happens if one service module fails during execution?

The launch_module() function in holehe/core.py (lines 66-73) wraps each service call in a try-except block. If a module raises an exception, it captures the error and returns a standardized result dict with the error details, allowing other concurrent checks to continue unaffected.

Can I filter which services run during a scan?

Yes. The get_functions() method in holehe/core.py (lines 50-63) supports optional filtering based on command-line flags such as --no-password-recovery. When calling the function programmatically, you can filter the returned list of functions before passing them to the Trio nursery.

What is the required function signature for creating a new Holehe module?

New modules must define an async function matching the filename (e.g., async def newsite(email, client, out):) that accepts three parameters: the email string, an HTTP client object (typically httpx.AsyncClient), and a shared list for output. The function should append a dictionary with standard keys (name, domain, exists, rateLimit, etc.) to the out list before returning.

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 →