# How to Create a Custom Module in Holehe: Understanding the Plug-In Architecture

> Explore Holehe's plug-in module architecture. Learn to build custom modules by understanding how Holehe discovers and runs independent Python service files concurrently.

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

---

**Holehe uses a plug-in style module architecture where each email-checking service is an independent Python file in `holehe/modules/`, automatically discovered at runtime via `pkgutil.walk_packages()` and executed concurrently using Trio.**

The open-source OSINT tool Holehe (megadose/holehe) checks if an email address is registered on hundreds of sites. Its extensible design allows you to add new services without modifying core logic. Understanding the module architecture in Holehe reveals how the tool dynamically imports, orchestrates, and standardizes results from distributed service checks.

## How the Module Architecture Works

Holehe’s core engine ([`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py)) treats every service as a standalone module. The architecture relies on four distinct phases to discover and execute checks.

### Dynamic Discovery via import_submodules

The `import_submodules()` function traverses the package tree using `pkgutil.walk_packages()` to find every Python file under `holehe/modules/`【source】(https://github.com/megadose/holehe/blob/master/holehe/core.py#L37-L47). This occurs at runtime, meaning any new `.py` file dropped into the modules directory is automatically imported without explicit registration.

### Function Extraction and Registration

Once imported, `get_functions()` inspects each module and extracts the callable whose name matches the file name—for example, `twitter` from [`twitter.py`](https://github.com/megadose/holehe/blob/main/twitter.py)【source】(https://github.com/megadose/holehe/blob/master/holehe/core.py#L50-L63). Only functions matching their filename are collected into the execution list, enforcing a strict naming convention.

### Async Execution with Trio

The `launch_module()` function executes each discovered callable concurrently using **Trio**, passing three arguments: the target `email`, a shared `httpx.AsyncClient` instance, and a shared `out` list where results are appended【source】(https://github.com/megadose/holehe/blob/master/holehe/core.py#L66-L71). This asynchronous pattern allows Holehe to check hundreds of services simultaneously without blocking.

### Standardized Result Schema

Every module must append a dictionary to the `out` list with a fixed set of keys: `name`, `domain`, `method`, `frequent_rate_limit`, `rateLimit`, `exists`, `emailrecovery`, `phoneNumber`, and `others`. The core printer (`print_result`) consumes this schema to format terminal output and CSV exports【source】(https://github.com/megadose/holehe/blob/master/holehe/core.py#L106-L149).

## Creating a Custom Holehe Module

To add a new service, create a Python file in any sub-directory of `holehe/modules/` (e.g., [`holehe/modules/social_media/myservice.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/social_media/myservice.py)). The folder structure organizes code by category but does not affect execution logic.

### Required Function Signature

Define an **async** function whose name matches the filename exactly. It must accept three parameters:

```python
async def myservice(email: str, client, out: list):

```

- `email`: The target string to investigate
- `client`: A pre-configured `httpx.AsyncClient` for HTTP requests
- `out`: A shared list to which you append the result dictionary

### Implementing the Check

Use the shared `client` for all HTTP requests and import `ua` from `holehe.localuseragent` for a realistic User-Agent header. Handle exceptions gracefully to indicate rate limits or errors.

```python

# holehe/modules/social_media/myservice.py

from holehe.core import *
from holehe.localuseragent import *

async def myservice(email: str, client, out: list):
    """
    Checks whether email is registered on MyService.
    """
    name = "myservice"
    domain = "myservice.com"
    method = "register"               
    frequent_rate_limit = False

    try:
        resp = await client.get(
            "https://api.myservice.com/v1/users/exists",
            params={"email": email},
            headers={"User-Agent": ua}
        )
        exists = resp.json().get("taken", False)

        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": False,
            "exists": exists,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })
    except Exception:
        out.append({
            "name": name,
            "domain": domain,
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True,
            "exists": False,
            "emailrecovery": None,
            "phoneNumber": None,
            "others": None,
        })

```

### Key Implementation Rules

- **File location**: Place in any `holehe/modules/` subfolder (e.g., `products/`, `social_media/`)
- **Naming**: Function name must match filename (e.g., `myservice()` in [`myservice.py`](https://github.com/megadose/holehe/blob/main/myservice.py))
- **Async requirement**: Function must be declared with `async def`
- **Result schema**: Dictionary must include all required keys; set `rateLimit` to `True` when the service blocks requests
- **No registration**: `import_submodules()` automatically discovers the file at startup

After saving, run `holehe email@example.com` to see your custom module appear in the output and optional CSV export.

## Summary

- Holehe employs a **plug-in architecture** where service checks reside as independent Python modules under `holehe/modules/`
- **Dynamic discovery** via `pkgutil.walk_packages()` in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) eliminates manual registration
- Modules must expose an **async function** matching the filename that accepts `(email, client, out)` arguments
- A **strict result schema** (`name`, `domain`, `exists`, `rateLimit`, etc.) ensures consistent output formatting across all services
- **Trio-based concurrency** executes all modules in parallel, appending results to a shared list for the core printer to process

## Frequently Asked Questions

### What is the module architecture in Holehe?

Holehe implements a dynamic plug-in system where each service check is a standalone Python file. The `import_submodules()` function in [`core.py`](https://github.com/megadose/holehe/blob/main/core.py) uses `pkgutil.walk_packages()` to auto-discover every module under `holehe/modules/`, while `get_functions()` extracts callables matching their filenames for execution.

### How do I create a custom module in Holehe?

Create a `.py` file in any subfolder of `holehe/modules/` containing an async function with the same name as the file. The function must accept `(email, client, out)` parameters and append a result dictionary with the required schema to the `out` list. No manual registration is necessary.

### What is the required result schema for Holehe modules?

Every module must append a dictionary containing: `name`, `domain`, `method`, `frequent_rate_limit`, `rateLimit`, `exists`, `emailrecovery`, `phoneNumber`, and `others`. Boolean flags like `rateLimit` indicate if the service blocked the request, while `exists` shows registration status.

### Do I need to register my custom module manually?

No. Holehe performs runtime discovery of modules using `pkgutil.walk_packages()`. As long as your file is in `holehe/modules/` and your function name matches the filename, [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) will automatically import and execute it alongside built-in modules.