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

> Explore Holehe's modular architecture with dynamic plugin discovery and concurrent execution. Learn how this Python plug-in system enhances functionality without core logic changes.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: internals
- Published: 2026-08-17

---

**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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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:

```bash

# 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:

```python
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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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`](https://github.com/megadose/holehe/blob/main/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.