Holehe Module Structure: A Complete Developer Guide

A holehe module is a self-contained Python file that implements an async function to probe a specific online service for email existence, following a strict contract defined in the megadose/holehe repository.

Holehe is an open-source OSINT (Open Source Intelligence) tool that checks whether email addresses are registered on hundreds of online platforms. Understanding the holehe module structure is essential for security researchers and developers who want to extend the tool with new services or debug existing email verification probes.

File Location and Package Organization

All holehe modules reside within the holehe.modules package, organized by thematic categories such as social_media, software, and shopping.

The standard file path follows this pattern:


holehe/modules/<category>/<service>.py

For example, the Instagram probe lives at holehe/modules/social_media/instagram.py, while e-commerce checks reside in holehe/modules/shopping/. The holehe/modules/__init__.py file marks this directory as a Python package and enables the dynamic import mechanism used by the core engine to discover probes at runtime.

Required Imports

Every module must begin with two specific wildcard imports:

  • from holehe.core import * – Imports shared utilities and base exceptions from the core framework
  • from holehe.localuseragent import * – Imports the User-Agent rotation dictionary

These imports provide access to random.choice(ua["browsers"]["chrome"]) for rotating User-Agent strings and the shared exception handling required by the framework.

The Async Function Contract

Function Signature

The core of every holehe module is a single async function matching the filename. In holehe/modules/social_media/instagram.py, the function is defined as:

async def instagram(email, client, out):

The function accepts three parameters:

  • email – The target email address as a string
  • client – A shared httpx.AsyncClient instance for concurrent HTTP requests
  • out – A mutable list where the module appends its results

Static Metadata Fields

Inside the function, you must define four static variables that describe the service:

name = "instagram"
domain = "instagram.com"
method = "register"
frequent_rate_limit = True
  • name – The service identifier used in reporting
  • domain – The primary domain of the service
  • method – The verification approach, typically "register" or "password_reset"
  • frequent_rate_limit – Boolean flag indicating whether the service aggressively rate-limits requests

Implementing the Network Probe

Crafting Requests

Modules craft HTTP headers and payloads that mimic legitimate browser behavior. Use the random User-Agent helper to avoid detection:

headers = {
    "User-Agent": random.choice(ua["browsers"]["chrome"]),
    "Accept": "application/json, text/plain, */*",
    "Origin": "https://www.instagram.com",
    "DNT": "1",
    "Connection": "keep-alive",
}

For services requiring CSRF tokens or session cookies, perform preliminary GET requests to extract these values before submitting the email verification request.

Executing HTTP Calls

Use the provided client parameter for all network operations to ensure proper connection pooling and timeout handling:

freq = await client.get(
    "https://www.instagram.com/accounts/emailsignup/", 
    headers=headers
)
token = freq.text.split('{\\"config\\":{\\"csrf_token\\":\\"')[1].split('\\"')[0]

Submit the probe request (typically POST) with the target email embedded in the payload:

data = {
    "email": email,
    "username": "".join(random.choice(string.ascii_lowercase) for _ in range(8)),
    "first_name": "",
    "opt_into_one_tap": "false",
}
headers["x-csrftoken"] = token

check = await client.post(
    "https://www.instagram.com/api/v1/web/accounts/web_create_ajax/attempt/",
    data=data,
    headers=headers,
)

Handling Responses and Building Output

Parse the HTTP response to determine the email status. Append a standardized dictionary to the out list with exactly these nine keys:

out.append({
    "name": name,
    "domain": domain,
    "method": method,
    "frequent_rate_limit": frequent_rate_limit,
    "rateLimit": False,        # True if blocked by rate limiting

    "exists": True,            # True if email is registered

    "emailrecovery": None,     # Recovery email if exposed

    "phoneNumber": None,       # Phone number if exposed

    "others": None             # Dictionary for additional metadata

})

Error Handling and Rate Limits

Wrap all network logic in try-except blocks to prevent individual module failures from crashing the entire scan. When exceptions occur or unexpected HTTP responses are received, append a rate-limit entry:

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
    })

This ensures the main engine in holehe/core.py can continue execution even if a specific service is offline or has changed its API.

Complete Module Example

Here is the full structure of holehe/modules/social_media/instagram.py, demonstrating the complete implementation pattern:


# holehe/modules/social_media/instagram.py

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

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

    headers = {
        "User-Agent": random.choice(ua["browsers"]["chrome"]),
        "Accept": "application/json, text/plain, */*",
        "Origin": "https://www.instagram.com",
        "DNT": "1",
        "Connection": "keep-alive",
    }

    # Obtain CSRF token

    freq = await client.get(
        "https://www.instagram.com/accounts/emailsignup/", headers=headers
    )
    token = freq.text.split('{\\"config\\":{\\"csrf_token\\":\\"')[1].split('\\"')[0]

    data = {
        "email": email,
        "username": "".join(
            random.choice(string.ascii_lowercase + string.digits) 
            for _ in range(random.randint(6, 30))
        ),
        "first_name": "",
        "opt_into_one_tap": "false",
    }
    headers["x-csrftoken"] = token

    # Submit registration attempt

    check = await client.post(
        "https://www.instagram.com/api/v1/web/accounts/web_create_ajax/attempt/",
        data=data,
        headers=headers,
    )
    check = check.json()

    # Interpret response and record result

    if check["status"] != "fail":
        if "email" in check["errors"]:
            if check["errors"]["email"][0]["code"] == "email_is_taken":
                out.append({
                    "name": name, 
                    "domain": domain, 
                    "method": method,
                    "frequent_rate_limit": frequent_rate_limit,
                    "rateLimit": False, 
                    "exists": True,
                    "emailrecovery": None, 
                    "phoneNumber": None, 
                    "others": None
                })
            else:
                out.append({
                    "name": name, 
                    "domain": domain, 
                    "method": method,
                    "frequent_rate_limit": frequent_rate_limit,
                    "rateLimit": False, 
                    "exists": False,
                    "emailrecovery": None, 
                    "phoneNumber": None, 
                    "others": None
                })
    else:
        out.append({
            "name": name, 
            "domain": domain, 
            "method": method,
            "frequent_rate_limit": frequent_rate_limit,
            "rateLimit": True, 
            "exists": False,
            "emailrecovery": None, 
            "phoneNumber": None, 
            "others": None
        })

How the Core Engine Loads Modules

The orchestration logic in holehe/core.py discovers and executes modules dynamically using introspection:


# Dynamic module discovery

modules = import_submodules("holehe.modules")
websites = get_functions(modules, args)

# Concurrent execution with shared client

client = httpx.AsyncClient(timeout=args.timeout)
out = []

async with trio.open_nursery() as nursery:
    for website in websites:
        nursery.start_soon(launch_module, website, email, client, out)

The import_submodules("holehe.modules") function walks the package tree to find all Python files, while get_functions() extracts the callable probe functions. The engine uses Trio for structured concurrency, passing the shared httpx.AsyncClient to each module to enable efficient connection reuse across hundreds of simultaneous checks.

Summary

  • File Location: Place modules in holehe/modules/<category>/<service>.py following the existing directory structure
  • Function Signature: Implement exactly one async function accepting (email, client, out) parameters
  • Static Metadata: Define name, domain, method, and frequent_rate_limit variables inside the function
  • Output Contract: Append dictionaries with nine specific keys (name, domain, method, frequent_rate_limit, rateLimit, exists, emailrecovery, phoneNumber, others) to the out list
  • Error Handling: Catch all exceptions and report them as rateLimit: True entries to ensure scan continuity
  • Discovery: The core engine uses import_submodules() and get_functions() in holehe/core.py to find and execute modules automatically

Frequently Asked Questions

What is the exact function signature for a holehe module?

Every holehe module must expose exactly one async function with the signature async def servicename(email, client, out). The function name must match the filename (e.g., instagram.py contains async def instagram(...)). The email parameter receives the target string, client provides a shared httpx.AsyncClient, and out is a list where results are appended.

How does holehe handle concurrent execution of modules?

The core engine in holehe/core.py uses the Trio library for structured concurrency. It creates an open nursery with trio.open_nursery() and starts each module as a separate task using nursery.start_soon(launch_module, ...). All modules share a single httpx.AsyncClient instance passed as the client parameter, enabling efficient connection pooling across hundreds of simultaneous probes.

What fields are required in the holehe module output dictionary?

Each module must append a dictionary to the out list containing exactly nine keys: name (service identifier), domain (service domain), method (check type), frequent_rate_limit (boolean flag), rateLimit (boolean indicating if blocked), exists (boolean indicating if email is registered), emailrecovery (extracted recovery email or None), phoneNumber (extracted phone or None), and others (dictionary for additional data or None).

Where should I place a new custom holehe module?

Create your new module file in holehe/modules/<category>/your_service.py, where <category> is a thematic folder like social_media, shopping, or software. The holehe/modules/__init__.py file enables Python to treat this as a package, and the dynamic importer in holehe/core.py will automatically discover your module as long as it follows the standard file structure and function naming conventions.

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 →