How to Implement a New holehe Module: A Complete Developer Guide

To implement a new holehe module, create an async Python function with the signature async def service_name(email, client, out) inside a Python file under holehe/modules/<category>/, use the shared httpx.AsyncClient to check email registration status, and append a standardized result dictionary to the out list.

The megadose/holehe framework automatically discovers and executes email correlation modules without requiring manual registration. Understanding how to implement a new holehe module involves grasping the auto-discovery mechanism, adhering to the async function contract defined in holehe/core.py, and structuring HTTP requests using the shared infrastructure.

Understanding the Auto-Discovery Mechanism

The framework discovers every available check by recursively scanning the holehe.modules package at runtime. In holehe/core.py, the import_submodules function uses pkgutil.walk_packages to traverse the directory tree and import every Python file under holehe/modules/.

The get_functions helper then extracts callable objects (your async check functions) from each imported module. When the CLI executes, launch_module starts each callable in a Trio nursery, passing the shared httpx.AsyncClient and output list. Because modules are discovered dynamically, no registration step is required—simply dropping a new file into the appropriate sub-directory makes it available immediately.

Required Function Signature and Parameters

Every holehe module must expose a top-level async function with this exact signature:

async def service_name(email, client, out):
  • email (str): The target email address to investigate.
  • client (httpx.AsyncClient): A shared async HTTP client configured with global timeouts, connection pooling, and proxy settings.
  • out (list): A mutable list where your function appends result dictionaries.

The file must reside in a sub-package of holehe/modules/ (e.g., holehe/modules/music/spotify.py). The directory must contain an __init__.py file (even if empty) for pkgutil.walk_packages to recognize it as a package.

Step-by-Step Implementation Guide

1. Define Service Metadata

Begin by declaring identification variables that the printer uses to categorize results:

name = "spotify"
domain = "spotify.com"
method = "register"  # or "login"

frequent_rate_limit = False  # Set True if service aggressively rate-limits

2. Configure HTTP Headers

Import the random User-Agent generator from holehe/localuseragent.py to avoid detection signatures:

from holehe.localuseragent import *

headers = {
    "User-Agent": random.choice(ua["browsers"]["chrome"]),
    "Accept": "application/json, text/plain, */*",
    "Accept-Language": "en-US,en;q=0.5",
    "DNT": "1",
    "Connection": "keep-alive",
}

3. Execute the Request

Use the injected client parameter for all HTTP operations. Do not create separate client instances, as this bypasses the shared configuration:

params = {"email": email, "validate": "1"}
try:
    resp = await client.get(
        "https://api.example.com/check",
        headers=headers,
        params=params,
    )
    data = resp.json()
except Exception:
    # Network or parsing errors trigger rate-limit flag

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

4. Parse the Response

Interpret the service's response to determine account existence. Logic varies by service—some return JSON status codes, others return specific HTTP status codes or HTML patterns:

if data.get("status") == 20:  # Email found

    exists = True
    rate_limit = False
elif data.get("status") == 1:  # Email not used

    exists = False
    rate_limit = False
else:  # Unknown state or rate limit

    exists = None
    rate_limit = True

5. Append Standardized Results

Push a dictionary to out containing exactly these keys:

out.append({
    "name": name,
    "domain": domain,
    "method": method,
    "frequent_rate_limit": frequent_rate_limit,
    "rateLimit": rate_limit,
    "exists": exists,
    "emailrecovery": None,  # Populate if service exposes recovery email

    "phoneNumber": None,    # Populate if service exposes phone number

    "others": None,         # Populate with additional metadata dict if available

})

Complete Production Module Example

Below is a functional skeleton implementing a fictional service check. Place this file at holehe/modules/category/example.py:

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

async def example(email, client, out):
    name = "example"
    domain = "example.com"
    method = "register"
    frequent_rate_limit = False

    headers = {
        "User-Agent": random.choice(ua["browsers"]["chrome"]),
        "Accept": "application/json",
    }

    params = {"email": email}
    
    try:
        r = await client.get(
            "https://api.example.com/v1/email_check",
            headers=headers,
            params=params
        )
        data = r.json()
        
        if data.get("registered") is True:
            exists = True
            rate_limit = False
        else:
            exists = False
            rate_limit = False
            
        out.append({
            "name": name, "domain": domain, "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": rate_limit, "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": None,
            "emailrecovery": None, "phoneNumber": None, "others": None,
        })

Directory Structure and Deployment

Organize modules by service category to maintain the codebase structure:

  • Choose a category: music, shopping, programming, social, etc.
  • Create the file: holehe/modules/<category>/<service>.py
  • Ensure __init__.py exists: The category folder must contain an __init__.py file (can be empty) for pkgutil.walk_packages to traverse it.

Because import_submodules in holehe/core.py performs dynamic imports, your module is automatically registered on the next CLI execution without modifying any central registry files.

Testing and Verification

After creating your module, validate it by running the holehe CLI:

python -m holehe test@example.com --no-color

Your new module will appear in the output list alongside built-in checks. If the module fails to appear, verify:

  • The file is saved under holehe/modules/ with a .py extension.
  • The directory contains an __init__.py file.
  • The function definition uses the async keyword and accepts exactly three parameters.
  • No syntax errors exist in the file (tracebacks appear in the CLI output on failure).

Summary

  • Auto-discovery: The import_submodules function in holehe/core.py automatically imports all Python files under holehe/modules/ using pkgutil.walk_packages, requiring no manual registration.
  • Function contract: Modules must define an async function accepting (email, client, out) parameters.
  • HTTP client: Always use the injected httpx.AsyncClient (client) to ensure shared timeout, proxy, and connection pool configuration.
  • Result format: Append dictionaries to out containing name, domain, method, frequent_rate_limit, rateLimit, exists, emailrecovery, phoneNumber, and others.
  • File placement: Create files at holehe/modules/<category>/<service>.py with accompanying __init__.py files in category directories.

Frequently Asked Questions

What is the exact function signature required for a holehe module?

The function must be asynchronous and accept three positional parameters: email (the string being investigated), client (an httpx.AsyncClient instance), and out (a list to which you append results). The signature must look like async def service_name(email, client, out):. Deviating from this signature prevents get_functions in holehe/core.py from correctly extracting your callable during the discovery phase.

How does holehe discover new modules automatically without registration?

During initialization, the import_submodules function in holehe/core.py uses pkgutil.walk_packages to iterate through every sub-package of holehe.modules. It imports each module and get_functions introspects them for callable objects. As long as your file is a valid Python module within that tree and contains the properly named async function, it is detected and executed without requiring imports in central configuration files.

How should I handle rate limiting and errors in my module?

Set rateLimit to True and exists to None whenever the service returns HTTP 429, blocks the request, or when network exceptions occur. Wrap your HTTP calls in try/except blocks to catch httpx exceptions and parsing errors, appending the rate-limit result dictionary to out before returning. Set frequent_rate_limit = True in the metadata for services known to aggressively block requests.

Where should I place my new module file within the repository?

Place the file inside a category folder under holehe/modules/, such as holehe/modules/music/newservice.py. The category folder must contain an __init__.py file for Python to recognize it as a package. If no existing category fits the service, create a new directory with an __init__.py and place your module there; the auto-discovery mechanism will include it regardless of the directory name.

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 →