How Holehe Tracks Progress with tqdm: Async Instrumentation Explained

Holehe tracks scan progress by attaching a custom Trio Instrument (TrioProgress) to the event loop, which calls tqdm.update(1) every time a website-checking coroutine finishes.

Holehe is an open-source OSINT tool that checks email addresses against 120+ websites simultaneously. Because it runs all checks asynchronously using the Trio concurrency library, the developers needed a way to show users which sites have been processed without blocking the main execution. The solution pairs Trio's Instrument API with the tqdm progress bar library for lightweight, non-intrusive feedback.

How tqdm Progress Tracking Works in Holehe

The progress system consists of two coordinated parts: the TrioProgress instrument that detects task completion, and the main loop in holehe/core.py that registers it.

The TrioProgress Instrument (holehe/instruments.py)

The core logic lives in holehe/instruments.py, where a small class inherits from trio.abc.Instrument:

class TrioProgress(trio.abc.Instrument):
    def __init__(self, total):
        self.tqdm = tqdm(total=total)          # initialise tqdm with the number of sites

    
    def task_exited(self, task):
        # Trio names every started coroutine; the ones we care about end with

        # "launch_module". When such a task exits we advance the bar.

        if task.name.split(".")[-1] == "launch_module":
            self.tqdm.update(1)

Three key design decisions make this work:

  • Constructor receives total: The count of websites to check, passed from len(websites) in the caller
  • task_exited hook: Trio's built-in callback that fires automatically when any task ends—no manual instrumentation needed in the coroutines themselves
  • Name filtering: Only tasks ending with "launch_module" trigger updates, ignoring internal Trio tasks like the nursery manager

Integration in the Main Scan Loop (holehe/core.py)

The instrument is wired into the execution flow in holehe/core.py:

instrument = TrioProgress(len(websites))
trio.lowlevel.add_instrument(instrument)          # attach the instrument

async with trio.open_nursery() as nursery:
    for website in websites:
        nursery.start_soon(launch_module, website, email, client, out)
trio.lowlevel.remove_instrument(instrument)       # detach after all tasks finish

The sequence matters: register before opening the nursery, then clean up after all tasks complete. This ensures every launch_module task that starts also triggers the exit hook.

Why This Architecture Works

tqdm operates independently of Trio. It's thread-safe and accepts manual update() calls, so it doesn't care whether the caller is synchronous or async.

Trio's Instrument API provides lifecycle hooks without code modification. Alternative approaches would require:

  • Manual progress calls inside each module — breaks separation of concerns and risks missed updates on exceptions
  • Periodic polling of task status — adds overhead and complicates cancellation handling
  • Wrapping coroutines with decorators — requires metaprogramming that obscures stack traces

The chosen design keeps module code clean while guaranteeing accurate progress tracking even when individual site checks fail or timeout.

Running Holehe with Progress Output

From the command line, the tqdm bar appears automatically:

$ holehe user@example.com
[##########################] 120/120

The counter increments live as each asynchronous check returns, regardless of whether that site reported account found, not found, or rate-limited. Users see steady feedback without Terminal flicker or log spam.

Embedding Holehe's Progress Logic in Custom Code

You can reuse the same instrumentation pattern in your own Trio-based scripts:

import trio
from holehe.core import import_submodules, get_functions, launch_module
from holehe.instruments import TrioProgress
import httpx

async def run_scan(email):
    modules = import_submodules("holehe.modules")
    websites = get_functions(modules)
    client = httpx.AsyncClient()
    out = []
    prog = TrioProgress(len(websites))
    trio.lowlevel.add_instrument(prog)

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

    trio.lowlevel.remove_instrument(prog)
    await client.aclose()
    return out

trio.run(run_scan, "user@example.com")

The TrioProgress class requires no modification—it automatically detects any launch_module task in the current Trio event loop. This makes it portable across different Holehe-based workflows.

Summary

  • TrioProgress in holehe/instruments.py — Custom trio.abc.Instrument subclass that wraps tqdm and increments on each launch_module task exit
  • task_exited filtering — Uses task name suffix matching to ignore irrelevant Trio internals
  • Registration in holehe/core.py — Instrument attached via trio.lowlevel.add_instrument() before the nursery opens, removed after completion
  • Non-blocking feedback — tqdm updates without awaiting, preserving full async throughput
  • Reusable design — Same pattern works in standalone scripts importing Holehe internals

Frequently Asked Questions

Does Holehe's tqdm progress bar work on Windows terminals?

Yes. tqdm detects the terminal capabilities automatically. On Windows, it falls back from ANSI escape codes to the Windows console API. The tqdm dependency declared in setup.py handles cross-platform rendering without code changes in Holehe.

What happens if a site check hangs—does the progress bar stall?

The bar only advances when task_exited fires, so hung tasks don't increment the counter. However, Trio nurseries typically apply timeouts via trio.move_on_after() or async_timeout. When a timeout cancels a launch_module task, the task_exited hook still executes, ensuring the bar reaches the total even with failures.

Can I disable the progress bar in Holehe?

The command-line interface doesn't expose a --no-progress flag in the current release. To suppress output programmatically, instantiate TrioProgress with disable=True passed to tqdm: modify self.tqdm = tqdm(total=total, disable=True) in holehe/instruments.py or subclass the instrument in your own code.

Why does Holehe use Trio's low-level Instrument API instead of higher-level patterns?

Alternative approaches like anyio adapters or manual trio.Event signaling would require either an abstraction layer that reduces performance visibility, or invasive changes to every module. The Instrument API provides zero-cost observation—if no instrument is registered, the runtime skips all hook overhead entirely. This preserves Holehe's efficiency for headless automation use cases that don't need progress display.

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 →