# How to Add a Custom Module to Holehe: Complete Developer Guide

> Easily add a custom module to Holehe without core code changes. Follow our developer guide to create a Python file and extend Holehe's capabilities seamlessly.

- Repository: [Palenath/holehe](https://github.com/megadose/holehe)
- Tags: how-to-guide
- Published: 2026-08-26

---

**Holehe discovers every available check by walking the `holehe.modules` package at runtime using `import_submodules()` in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py), allowing you to add new capabilities by simply creating a Python file with a properly named async function and no core code changes.**

Holehe is an open-source email OSINT tool that identifies where an email address is registered across hundreds of services. When you need to probe a site that isn't in the default module library, adding a custom module to Holehe lets you extend the tool's reach without forking or patching the central execution logic.

## How Holehe Discovers Custom Modules

The module discovery system relies on two key functions in **[[`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py)](https://github.com/megadose/holehe/blob/master/holehe/core.py)**.

First, **`import_submodules()`** (lines 37–48) uses `pkgutil.walk_packages` to recursively import every sub-package under `holehe.modules`. This means any Python file you add to the modules directory automatically becomes part of the runtime environment.

Second, **`get_functions()`** (lines 50–63) inspects the imported modules and extracts any callable that matches the filename. For example, a file named [`example.py`](https://github.com/megadose/holehe/blob/main/example.py) must expose an async function named `example`.

The system requires no registration files or configuration updates—simply placing a valid module file in the correct directory makes it available the next time you run the CLI.

## Step-by-Step Guide to Creating a Custom Module

### Choose a Category and Create the File

Create a new Python file under `holehe/modules/<category>/`. Use an existing category like `shopping` or `social`, or create a new directory such as `custom`.

Ensure the directory contains an **[`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py)** file. This marker file is essential because `import_submodules()` relies on Python's package structure to locate submodules.

The filename determines the function name you must use later. For a service named `example`, create [`holehe/modules/custom/example.py`](https://github.com/megadose/holehe/blob/main/holehe/modules/custom/example.py).

### Define the Async Function Signature

Each module must expose a single **asynchronous function** whose name matches the filename exactly. The function signature must accept three parameters:

```python
async def example(email, client, out):

```

- **`email`**: The target email address string.
- **`client`**: An `httpx.AsyncClient` instance for making HTTP requests.
- **`out`**: A list that your function must populate with a result dictionary.

### Populate the Output Dictionary

Your function must append a dictionary to the `out` list following the standard Holehe schema. The dictionary requires these specific keys:

- **`name`**: Service identifier (e.g., `"example"`).
- **`domain`**: The service domain (e.g., `"example.com"`).
- **`method`**: HTTP method used (e.g., `"login"`, `"register"`).
- **`frequent_rate_limit`**: Boolean indicating if the site frequently rate-limits.
- **`rateLimit`**: Boolean set to `True` if the current request hit a rate limit.
- **`exists`**: Boolean indicating if the email is registered (`True`/`False`).
- **`emailrecovery`**: String with recovery email hint, or `None`.
- **`phoneNumber`**: String with recovery phone hint, or `None`.
- **`others`**: Dictionary for additional metadata, or `None`.

## Complete Custom Module Example

Below is a production-ready template you can adapt. This module checks a fictional `example.com` service by attempting a login request and parsing the error response.

```python

# File: holehe/modules/custom/example.py

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

async def example(email, client, out):
    """
    Checks whether the supplied email exists on example.com.
    """
    name = "example"
    domain = "example.com"
    method = "login"
    frequent_rate_limit = False

    headers = {"User-agent": random.choice(ua["browsers"]["chrome"])}
    try:
        # Load the login page to obtain any hidden fields

        resp = await client.get("https://example.com/login", headers=headers)
        soup = BeautifulSoup(resp.text, "html.parser")

        # Build the POST payload

        data = {"email": email}
        for hidden in soup.select('form input[type="hidden"]'):
            if hidden.get("name") and hidden.get("value"):
                data[hidden["name"]] = hidden["value"]

        # Submit the login request

        resp = await client.post("https://example.com/login", data=data, headers=headers)
        body = BeautifulSoup(resp.text, "html.parser")

        # Determine existence based on error messages

        if body.find("div", {"class": "error"}):
            exists = False
        else:
            exists = True

        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:
        # Handle network-level problems (rate-limits, timeouts, etc.)

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

```

## Running Your Custom Module

After saving the file, execute Holehe normally:

```bash
holehe user@example.com

```

The CLI automatically imports `holehe.modules.custom.example` and includes its results alongside built-in checks. As implemented in megadose/holehe, the `get_functions()` loader will discover your new module during the initialization phase and queue it for execution with the other probes.

## Summary

- **No core modifications required**: The `import_submodules()` function in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) automatically discovers new files placed under `holehe/modules/`.
- **Strict naming convention**: The async function name must match the filename (e.g., [`example.py`](https://github.com/megadose/holehe/blob/main/example.py) contains `async def example`).
- **Three required parameters**: All module functions must accept `email`, `client`, and `out`.
- **Standard output schema**: Populate the `out` list with a dictionary containing `name`, `domain`, `method`, `frequent_rate_limit`, `rateLimit`, `exists`, `emailrecovery`, `phoneNumber`, and `others`.
- **Helper utilities available**: Import `from holehe.core import *` and `from holehe.localuseragent import *` to access shared HTTP utilities and random user-agent lists.

## Frequently Asked Questions

### Do I need to modify [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) to add a new module?

No. According to the Holehe source code, the `import_submodules()` function dynamically walks the `holehe.modules` package at runtime using `pkgutil.walk_packages`. As long as your file resides in the modules directory and follows the naming convention, it will be imported automatically without any changes to the core files or setup configuration.

### What is the exact function signature required for a custom module?

Your module must expose an async function with the signature `async def <module_name>(email, client, out)`, where `<module_name>` matches the filename. For example, a file named [`twitter.py`](https://github.com/megadose/holehe/blob/main/twitter.py) must contain `async def twitter(email, client, out)`. The parameters provide the target email, an HTTP client for requests, and a list to populate with the results.

### Can I reuse Holehe's existing utilities in my custom module?

Yes. You can import shared utilities using `from holehe.core import *` to access base functions and `from holehe.localuseragent import *` to utilize the `ua` dictionary containing randomized browser user-agents. This ensures your custom check behaves consistently with built-in modules regarding request headers and error handling patterns.

### Where should I create a new category for my custom modules?

Create a new directory under `holehe/modules/` (e.g., `holehe/modules/enterprise/`) and place an [`__init__.py`](https://github.com/megadose/holehe/blob/main/__init__.py) file inside it to make it a valid Python package. The `import_submodules()` logic in [`holehe/core.py`](https://github.com/megadose/holehe/blob/main/holehe/core.py) recursively searches all subdirectories, so your new category will be discovered alongside existing ones like `shopping` or `social`.