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 fromlen(websites)in the caller task_exitedhook: 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
TrioProgressinholehe/instruments.py— Customtrio.abc.Instrumentsubclass that wraps tqdm and increments on eachlaunch_moduletask exittask_exitedfiltering — Uses task name suffix matching to ignore irrelevant Trio internals- Registration in
holehe/core.py— Instrument attached viatrio.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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →