
Blocking-IO Guard
FreeProtect your async code from blocking I/O issues.
Free · Opens the source repo
What Blocking-IO Guard does
The Blocking-IO Guard skill is designed to assist developers in ensuring that their asynchronous backend code remains non-blocking. It provides a structured approach to identify and mitigate blocking I/O calls that could disrupt the asyncio event loop. By using this skill, contributors can confidently ship changes to the backend of their applications while maintaining the integrity of the asynchronous architecture. The skill operates by scanning for potential blocking I/O candidates in the code, allowing developers to address these issues before they become problematic.
When utilizing the Blocking-IO Guard, developers can choose between two modes: assessing their own changes (Mode A) or performing a comprehensive review of the entire codebase (Mode B). The skill's scanning process is deterministic, enabling developers to identify new blocking I/O calls introduced by their changes or existing issues that need to be triaged. This proactive approach not only improves code quality but also enhances the reliability of the application by preventing regressions related to blocking I/O.
The skill includes a series of steps that guide users through the process of evaluating and addressing blocking I/O candidates. From determining the scope of the changes to applying necessary fixes and verifying the effectiveness of those fixes, the Blocking-IO Guard skill streamlines the workflow for developers. Additionally, it emphasizes the importance of creating runtime anchors to ensure that future changes do not reintroduce blocking behavior.
Overall, the Blocking-IO Guard skill is an essential tool for developers working with asynchronous Python applications. It provides the necessary framework to maintain a responsive event loop and ensures that the application can handle I/O operations efficiently without compromising performance.
When to use it
Use this skill when modifying backend code that may impact asynchronous operations, or when conducting a full codebase triage for blocking I/O issues.
When not to use it
This skill may not be necessary for projects that do not utilize asynchronous programming or for codebases where blocking I/O is not a concern.
What you can build with it
Assessing New Changes
Use the skill to scan for blocking I/O candidates after making changes to the backend code, ensuring the new code does not disrupt async operations.
Conducting Codebase Triage
Perform a full repository scan to identify existing blocking I/O issues, prioritizing fixes based on severity before merging changes.
Creating Runtime Anchors
Generate runtime anchors for blocking I/O calls to prevent future regressions, ensuring that the event loop remains protected.
How to install Blocking-IO Guard
View source1. Install with the skills CLI
npx skills add bytedance/deer-flow/blocking-io-guard --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by bytedanceBlocking-IO Guard Skill
Help a contributor ship backend async changes together with the runtime anchor that lets DeerFlow's blocking-IO CI gate actually see the new code. The dynamic detector only catches blocking IO on paths a test executes — this skill closes that gap, either for your own diff or for a repo-wide triage round.
Read references/good-anchor-rules.md before writing any anchor.
Only read references/sop-skeleton.md when generalizing this SOP to another
detector domain — it is not needed to execute the steps below.
When to use
- Your change touches Python under
backend/app/,backend/packages/harness/deerflow/, orbackend/scripts/and may run on the async event loop (Mode A). If unsure, run Step 0 — it answers deterministically. - You are doing a maintenance triage round over the existing codebase (Mode B).
SOP (router)
Step 0 — Scope (deterministic)
Mode A — your own diff (default, pre-PR). From repo root:
uv run --project backend python scripts/scan_changed_blocking_io.py --base origin/main
Lists blocking-IO candidates your change introduces: findings on lines the
diff added, plus findings that are new versus the merge base — the latter
catches a new async caller exposing an old sync helper whose blocking line is
not in the diff. The diff is <base>...HEAD, so commit your work first —
uncommitted lines are not selected.
If the list is empty, this change introduces no blocking-IO surface that the
static detector can see in the changed files. One residual blind spot
remains: reachability is same-file only, so a new async caller of a sync
helper defined in another file is invisible to both selections. If your
diff adds an async call into a helper that lives elsewhere, check that helper
manually (codegraph or git grep) before stopping.
Mode B — full-repo triage round. From repo root:
make detect-blocking-io
Prints a summary and writes the complete structured finding list to
.deer-flow/blocking-io-findings.json. Work HIGH priority first; do not start
MEDIUM until every HIGH is dispositioned (fixed, guarded, or recorded
NO-ACTION).
Batching policy (PR sizing). One fix unit per PR while any HIGH remains: a fix unit is one root cause — usually a single HIGH, but two HIGHs resolved by the same one-place fix belong together. Once no HIGH remains, MEDIUM/LOW may be batched (about five per round, grouped by module or by disposition) so each PR stays reviewable. A new Blockbuster rule is never batched with anything — it always ships alone (see Step 5).
Both modes emit the same JSON shape per finding: priority, location
(path/line/function), blocking_call (category/operation/symbol),
event_loop_exposure, reason, code. Priority is a deterministic review
ordering, not proof of a bug — Step 1 makes the actual call.
Step 1 — Judge each candidate (router)
Read the code around each candidate and route it:
- Already offloaded (
asyncio.to_thread,run_in_executor, async client) → GUARD: add/extend an anchor that locks the offload so a future edit cannot move it back onto the loop. - On the loop, not offloaded → FIX+ANCHOR: offload the production code (your fix), then add an anchor that guards it.
- Not actually exposed / acceptable (rare: scanner false positive, startup-only code) → NO-ACTION: record one line of why.
- Cross-file caveat: the scanner's async reachability is same-file only
(
ASYNC_REACHABLE_SAME_FILE). If the candidate is a sync helper, check for async callers in other files (codegraph orgit grep) before deciding NO-ACTION.
Step 2 — Apply the fix, then re-scan (FIX+ANCHOR only)
Offload the blocking call in production code, then re-run the Step 0 scan and
confirm the candidate no longer appears. If the offloaded call sits in a
finally / cleanup path, keep it best-effort and bounded (swallow-and-log,
asyncio.wait_for) so a failing or hung cleanup cannot mask the primary
exception. Match by the stable key
(path, function, symbol) — line numbers shift after edits, so never
compare by line.
- The finding must disappear. If it still shows, the fix did not remove the blocking pattern (e.g. the call is still a direct call, not offloaded) — go back before touching any test.
- GUARD / NO-ACTION routes skip this step: a residual finding there is expected (the raw call still exists inside a sync helper with the offload at the caller, or the exposure was judged acceptable).
This is pattern-level feedback in seconds; it complements but never replaces Step 5 — only the runtime gate proves the event loop is actually protected.
Step 3 — Check existing anchors
Look in backend/tests/blocking_io/ for a test that drives the production async
entry point reaching this candidate's branch.
- Covers this branch already → go to Step 5 (re-verify teeth).
- Covers the entry point but not this branch (e.g. happy path covered, cleanup/404/409 not) → extend that anchor.
- None → create one from
templates/anchor.template.py.
Step 4 — Generate / extend the anchor
Follow references/good-anchor-rules.md. Drive the specific branch (e.g. force
the create failure that hits the cleanup shutil.rmtree). Never bypass the
blocking surface with a test-only asyncio.to_thread wrapper.
Step 5 — Verify teeth (mandatory; also the anchor-vs-rule discriminator)
- Reintroduce the block (GUARD: temporarily revert the offload; FIX+ANCHOR: run against the pre-fix code).
- Run
cd backend && make test-blocking-io(or target the one test). It must go RED. - Restore the fix. It must go GREEN.
A real block that stays GREEN means Blockbuster has no rule for that
primitive — that is the RULE route; see references/good-anchor-rules.md
for the admission criteria before adding one.
Step 6 — Deliver
Commit the anchor(s) with your change; make test-blocking-io green. In the PR,
note: candidates found, each disposition, the re-scan result (Step 2), and
the teeth evidence (red→green). Include the reason for any NO-ACTION. A new
Blockbuster rule, if any, goes in its own commit with the evidence from Step 5.
Frequently asked questions about Blocking-IO Guard
Similar skills
Python PyPI Package Builder
Streamline the process of creating and publishing Python packages.
Minecraft Plugin Development
Streamline your Minecraft server plugin creation.
MCP Server Builder
Easily build .NET MCP servers with the latest standards.
CommunityToolkit.Mvvm Messenger
Decoupled communication for ViewModels in .NET applications.
MVVM Toolkit DI
Streamline ViewModel integration with Dependency Injection in .NET.
MCP Apps Builder
Essential guidelines for MCP server development.
