hydroid
Finds blocking calls that run on the event loop in async FastAPI (and any asyncio) code — at any call depth, across modules, through callbacks, decorators, context managers, properties and subclass overrides — and shows the whole chain.
error[blocking-http]: `requests.api.get` blocks the event loop
--> app/main.py:12:12 in `app.main.get_user`
= reached from route GET /users/{user_id}: app.main.get_user (app/main.py:11)
= blocking chain (3 calls):
app/main.py:12:12 services.load -> app.services.load
app/services.py:7:22 fetch -> app.repo.fetch
app/repo.py:5:16 http_get -> requests.api.get
= help: use an async client (`httpx.AsyncClient`, `aiohttp`) or `await asyncio.to_thread(...)`
New here? Read the usage guide. Adding it to CI, or letting a coding agent set it up: CI/CD and agent setup.
Written in Rust on top of ty's semantic model (types, imports,
go-to-definition). Polar's server (1,854 files, 108k call sites) takes about as long as ty needs
to type-check it the first time (~10 s on 4 vCPUs), then ~0.35 s while nothing changed: extracted
facts are cached in .hydroid_cache/. HYDROID_TIMINGS=1 prints where the time goes.
Install and run
With uv, using the prebuilt binaries of the latest release (macOS, Linux, Windows; no Rust needed):
uvx --from hydroid-cli --find-links https://github.com/agusmdev/hydroid/releases/expanded_assets/v0.1.0 hydroid .
Or straight from the source on main (builds once with a Rust toolchain, then cached):
uvx --from git+https://github.com/agusmdev/hydroid hydroid .
To install the hydroid command permanently, replace uvx --from with uv tool install
(e.g. uv tool install git+https://github.com/agusmdev/hydroid), then run hydroid . anywhere.
From a clone: cargo install --path crates/hydroid. More in the guide.
hydroid path/to/project # finds .venv / VIRTUAL_ENV like ty
hydroid . --python .venv --format sarif > hydroid.sarif
| Flag | |
|---|---|
--python PATH |
virtualenv or interpreter to resolve imports (default: discovered) |
--follow-libs |
also analyze third-party function bodies |
--cpu |
also report CPU-bound calls (bcrypt, pbkdf2, scrypt) |
--strict |
report calls on the loop whose target is unknown, and fail on them |
--exclude GLOB |
skip files (repeatable) |
--no-cache |
do not read or write .hydroid_cache/ |
--format human|json|sarif |
output |
Exit status: 0 clean, 1 findings (or unresolved calls with --strict), 2 error.
Suppress a line with # hydroid: ignore.
Configuration in pyproject.toml (flags win):
[tool.hydroid]
python = ".venv"
exclude = ["migrations/**"]
strict = false
[[tool.hydroid.sink]] # your own blocking functions (defining qualified names)
category = "sdk"
advice = "use the async client"
functions = ["acme.client.Client.fetch"]
CI/CD and agent setup
hydroid exits 1 when it finds blocking calls, so it can gate a build as is. On GitHub,
--format sarif puts each blocking chain on the pull request through code scanning:
# .github/workflows/hydroid.yml
name: hydroid
on: [push, pull_request]
jobs:
hydroid:
runs-on: ubuntu-latest
permissions:
security-events: write
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
- run: uv sync # hydroid needs the project's dependencies installed
- run: >-
uvx --from hydroid-cli --find-links https://github.com/agusmdev/hydroid/releases/expanded_assets/v0.1.0
hydroid . --format sarif > hydroid.sarif || [ $? -eq 1 ]
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: hydroid.sarif
|| [ $? -eq 1 ] reports findings without failing the job (exit 2, an error, still fails it);
drop it and the upload step to fail the build instead.
docs/AGENT_SETUP.md has:
- a prompt to paste into a coding agent (Claude Code, Cursor, Codex…) that installs hydroid
in your repository, configures
[tool.hydroid], triages the existing findings (fix,# hydroid: ignorewith a reason, or report to you) and adds the CI job; - configs for GitLab CI, pre-commit, monorepos, projects not using uv, and other CI systems;
- a path for adopting it on a code base with existing findings: report-only first, then gate.
How it works
- Facts (
crates/hydroid_ty, the only crate that touches ty): every function body is walked in parallel; each call is resolved to its definitions through ty (types, then go-to-definition), including implicit calls (with,forand comprehensions, properties and their setters, descriptors, operators likex[k]andk in xon user classes,__getattr__, iterators'__next__, constructors with__post_init__and pydantic validators, builtins andcontextlibhelpers that iterate, enter or call their arguments likesorted(xs, key=f)orstack.enter_context(cm),singledispatchimplementations). Callables passed as arguments are recorded as flows (to_thread(f),Depends(dep),on_startup=[f], decorators), and so are callables stored onselfand returned by functions. When ty does not know a value's type because it comes from an unannotated function, the class that function returns is used. The facts of an unchanged project are reused from.hydroid_cache/(keyed by the binary, the options, the installed packages and every source file's contents). - Analysis (
crates/hydroid_core, plain graph code): where code runs is a property of the call, so "does callingfblock?" is computed once per function by a backwards BFS from blocking functions — shortest witness chains, no recursion, any depth. Diagnostics are the call sites inside loop code (async defbodies, sync callbacks scheduled on the loop) whose callee blocks. Offload APIs cut the graph; callbacks are attributed to the call site passing them; overrides are found by class-hierarchy analysis (test doubles only stand in for real classes in test code); calling a generator function runs nothing until the generator is iterated; a decorated function is reached through its decorator's wrappers only when they call it. Starting an event loop (asyncio.run) is only reported when loop code does it directly: sync bridges reaching it almost always check for a running loop first. - Catalog (
crates/hydroid_core/catalog.toml): blocking functions, offload APIs, loop callback APIs and FastAPI entry points, by defining qualified name. A test checks every name exists in a real environment.
Sync def endpoints and dependencies are not reported: FastAPI runs them in a threadpool.
Limits
Python is dynamic: calls through getattr, untyped parameters, or containers of callables cannot
always be resolved. They are never silently dropped — --strict lists every one reachable on the
loop. Not modeled yet: getattr(obj, "name"), containers of callables (HANDLERS[kind]()),
metaclasses, and .pyi-only libraries under --follow-libs. A change to any source file re-extracts the whole
project (no per-file incremental cache yet).
Development
cargo test --workspace # needs `uv`; syncs tests/fixtures/.venv from uv.lock
scripts/corpus.sh [names] # Polar, mealie, lnbits, dispatch, litellm, open-webui, template
Fixture cases live in tests/fixtures/cases/*; expectations are inline comments:
# expect: time.sleep via route GET /x, # expect: * (any blocking function), # expect-unresolved.
tests/adversarial/ is a suite of cases written to break hydroid, split into a training half
(required to pass, by cargo test) and a held-out half that is only scored:
scripts/hillclimb/score.py --split train # misses and false positives, per case
scripts/hillclimb/score.py --split holdout --quiet # aggregate only: don't tune on it
scripts/hillclimb/polar_time.sh target/release/hydroid 5
uv run --with anthropic scripts/hillclimb/adversary.py --count 5 # Claude proposes new cases
scripts/hillclimb/oracle.py PROJECT report.json # naive syntactic scan: candidate misses
ty's crates are pinned to exact versions (=0.0.15); they have no API stability promise.
The bundled typeshed stubs are Apache-2.0; hydroid is MIT.
Releases: docs/RELEASING.md.
Metadata
Release files for hydroid-cli 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| hydroid_cli-0.2.0.tar.gz | 74.9 kB | Details |
Built distributions (wheels)
| File | Reset | |||
|---|---|---|---|---|
| hydroid_cli-0.2.0-py3-none-win_amd64.whl | Python 3 | none | Windows x86-64 | Details |
| hydroid_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl | Python 3 | none | Linux musl 1.2+ x86-64 | Details |
| hydroid_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl | Python 3 | none | Linux musl 1.2+ ARM64 | Details |
| hydroid_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl | Python 3 | none | Linux glibc 2.17+ x86-64 | Details |
| hydroid_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl | Python 3 | none | Linux glibc 2.17+ ARM64 | Details |
| hydroid_cli-0.2.0-py3-none-macosx_11_0_arm64.whl | Python 3 | none | macOS 11.0+ ARM64 | Details |
| hydroid_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl | Python 3 | none | macOS 10.12+ x86-64 | Details |
Total release size: 71.3 MB
Release files / hydroid_cli-0.2.0.tar.gz
| Download URL | hydroid_cli-0.2.0.tar.gz |
|---|---|
| Size | 74.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
a119324af3978b07f031150e5f24b5c316b18f3e3a7d4aad5e078745e28459af
|
|
BLAKE2b-256 checksum How to use checksums |
48c68c21ac231f91b2f85eed19a49780a0863a9b6d961d874177c1791f2f0dcc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-win_amd64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-win_amd64.whl |
|---|---|
| Size | 10.6 MB |
| Tags | Python 3 Windows x86-64 |
|
SHA-256 checksum How to use checksums |
81798c8726dac20ef978db7aab91fe5be68e12c479d00fc4a6ebee436b5a9d07
|
|
BLAKE2b-256 checksum How to use checksums |
29410aa2c9272fd1544a974ea4ea010854fb288242658bfeb09a1bdd493bfceb
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-musllinux_1_2_x86_64.whl |
|---|---|
| Size | 10.6 MB |
| Tags | Linux musl 1.2+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
7806ecb845ad7268d8eb19a2f7660740f1c30e8473c8c115e3aa836e9937b479
|
|
BLAKE2b-256 checksum How to use checksums |
a599d706e180a6ccc14475348273e11451a7ada72617cfa902a7ce739e95337e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-musllinux_1_2_aarch64.whl |
|---|---|
| Size | 9.8 MB |
| Tags | Linux musl 1.2+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
be9fa756dac2738250a5e62333a64d396559cc33c1a2770b2db2367f424f9f6d
|
|
BLAKE2b-256 checksum How to use checksums |
98d0ea0f5c535eca0f2680d7ed4fc6a83347da56334693d21152716be5f7938c
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl |
|---|---|
| Size | 10.4 MB |
| Tags | Linux glibc 2.17+ x86-64 Python 3 |
|
SHA-256 checksum How to use checksums |
a1ccdc4a4a4a14f80408c2aa355ecaa4d5dd40ea338b07d183bec24d383a2566
|
|
BLAKE2b-256 checksum How to use checksums |
5ab31f2f9af8f852e7bdc9dd769b9f3053c0997e6c9e08f2c1518223275ba76f
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl |
|---|---|
| Size | 9.9 MB |
| Tags | Linux glibc 2.17+ ARM64 Python 3 |
|
SHA-256 checksum How to use checksums |
185b5c2c8ee9fbcb76f8240719282501e9649788c51a9401b850e804d443efa8
|
|
BLAKE2b-256 checksum How to use checksums |
660d61c552f1f4890f80b1555cf1543a40ff02eec744557540e9fc0c5b0f5569
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-macosx_11_0_arm64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-macosx_11_0_arm64.whl |
|---|---|
| Size | 9.8 MB |
| Tags | Python 3 macOS 11.0+ ARM64 |
|
SHA-256 checksum How to use checksums |
e2bd2e3d16010c2dc6c387f2ec39c3adb5e4fb0c1abc3fc8def2c9611dd837d2
|
|
BLAKE2b-256 checksum How to use checksums |
64fca65835c8a771a3d55004a8977ce28be9462aa3d99feea08c1a294ee2ba30
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency logRelease files / hydroid_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl
| Download URL | hydroid_cli-0.2.0-py3-none-macosx_10_12_x86_64.whl |
|---|---|
| Size | 10.3 MB |
| Tags | Python 3 macOS 10.12+ x86-64 |
|
SHA-256 checksum How to use checksums |
dabbac41b2029cc45cbd9d5c0c98809a197f6f2c0413c988a0c195789049673c
|
|
BLAKE2b-256 checksum How to use checksums |
1c7d135b87a92aace614d1382b73b30b9d7768af322a198caadfdc2282b3441e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 12, 2026.
Transparency log