Find out what your AI app downloads, or tries to reach, when the network goes away.
Why
A common assumption: point the LLM base URL at a local vLLM or Ollama instance and the app is ready for an offline network (an air-gapped VPC, a hospital or defense network, a container with no internet). In practice, many libraries fetch something on first use: tokenizer ranks, ONNX embedding weights, corpora, price maps, or telemetry. Offline, these calls either fail at first use or hang for tens of seconds on retry backoff.
None of this is a bug in those libraries. They download once and cache. It only becomes visible when the network is gone. Humanet airgap-check tries to surface it before you deploy.
What it does
- Manifest & lockfile scan. Inspects
requirements*.txt,pyproject.toml,Pipfile,package.json,Cargo.toml,go.mod, andGemfile(promoting[tool.uv] default-groups, preserving[tool.setuptools.dynamic.optional-dependencies], skippingdevDependencies/[dependency-groups]dev/test/lint/docs, and suppressingspacywhen a model wheel URL is pinned), and follows lockfiles (uv.lock,poetry.lock,package-lock.json,pnpm-lock.yamlv9 multi-doc includingpeerDependenciesMetaoptional edges,yarn.lock,Cargo.lock) with explicit scope tags (prod,transitive_lockfile,optional_extra). Reads these files with a built-in minimal TOML/YAML reader (no external parser), so unusual formatting may be missed. - Source patterns & heuristics. Flags known runtime fetch calls (
nltk.download(),spacy.load(),gtts.gTTS(),from_pretrained(),tiktoken.get_encoding(),whisper.load_model(),easyocr.Reader(),RapidOCR(),PaddleOCR(),FlagModel/FlagReranker,TextEmbedding(),next/font/google), skipping comments, docstrings (without inverting on multiline string expressions), Vertex AITextEmbeddingModel.from_pretrained(), non-runtime build/eval/test scripts (conftest.py), hash-verification tables, UI placeholders/links, andlocal_files_only=True. Also flags split-line asset URLs (.onnx,.safetensors,.gguf,model_prices_and_context_window.json), runtime pricing/update checks (models.dev,pypi.org, GitHub releases,updates.*),urlretrieve(), multilinesubprocessinstallers (pip install,curl,git clone,ollama pull), hardcoded~/.cachewrites, and direct stdlib telemetry endpoints (eu.i.posthog.com,otel.*,/v1/traces/ingest). Missing HTTPtimeout=calls are reported separately as advisoryrobustness_hints. - Python and Node
--run. Runs your app or probe insidedocker run --rm --network none(overriding bakedENTRYPOINT, logging to afsync-backed bind-mounted host file withdocker rm -fcleanup on timeout, and waiting briefly via/procfor detachedatexitchild senders) with a loopback sinkhole (UDP:53DNS,TCP:443TLS SNI,TCP:80HTTP Host/Path with query strings stripped,HTTP_PROXYfallback) and Python/Node socket hooks. --fix& custom rules. Prints DockerfileENVflags (includingOPENAI_AGENTS_DISABLE_TRACING=1,CHROMA_TELEMETRY_IMPL=none) and pre-warming commands, and supports repo overrides via.airgap-check.jsonor--rules..envkeys. Classifies.env*key names andgit ls-filesstatus only (excluding non-secret LLM token-limit knobs); secret values are never read or logged.
Go standard library only. --run requires Docker.
Install
# macOS and Linux curl -fsSL https://raw.githubusercontent.com/humanethq/airgap-check/main/install.sh | sh # With Go 1.22+ (also works on Windows) go install github.com/humanethq/airgap-check/cmd/airgap-check@latest # From source git clone https://github.com/humanethq/airgap-check.git cd airgap-check && make build && ./bin/airgap-check --help
Usage
# static scan airgap-check /path/to/repo # static scan plus a Dockerfile snippet airgap-check --fix /path/to/repo # run in a container with no network airgap-check --run /path/to/repo # auto-build the repo's Dockerfile first, then run inside --network none airgap-check --run --build --run-cmd "python3 -m pytest tests/smoke_test.py" /path/to/repo # use your own image, NXDOMAIN DNS mode, or a custom rules file airgap-check --run --dns-action nxdomain --image my-app:latest --run-cmd "node server.js" /path/to/repo airgap-check --rules ./airgap.custom.json /path/to/repo # machine-readable output (flags may appear before or after the path) airgap-check /path/to/repo --json
Example --fix output (abridged)
Read this before applying it: if your app loads Hugging Face models, pre-warm them at build time before setting HF_HUB_OFFLINE=1 and TRANSFORMERS_OFFLINE=1. Without a warm cache the app fails at startup instead of hanging.
# --- BEGIN HUMANET AIRGAP-CHECK REMEDIATION --- ENV DO_NOT_TRACK=1 \ NEXT_TELEMETRY_DISABLED=1 \ ANONYMIZED_TELEMETRY=False \ CHROMA_TELEMETRY_IMPL=none \ SCARF_NO_ANALYTICS=true \ HF_HUB_DISABLE_TELEMETRY=1 \ HF_HUB_OFFLINE=1 \ TRANSFORMERS_OFFLINE=1 \ LANGCHAIN_TRACING_V2=false \ LANGFUSE_ENABLED=false \ CREWAI_DISABLE_TELEMETRY=true \ OTEL_SDK_DISABLED=true \ DD_TRACE_ENABLED=false \ WANDB_MODE=offline \ WANDB_DISABLED=true \ GRADIO_ANALYTICS_ENABLED=False \ LITELLM_LOCAL_MODEL_COST_MAP=True \ STREAMLIT_BROWSER_GATHER_USAGE_STATS=false \ OPENAI_AGENTS_DISABLE_TRACING=1 \ XDG_CACHE_HOME=/app/.cache \ TIKTOKEN_CACHE_DIR=/app/.cache/tiktoken \ HF_HOME=/app/.cache/huggingface \ NLTK_DATA=/app/.cache/nltk_data \ EASYOCR_MODULE_PATH=/app/.cache/easyocr \ TORCH_HOME=/app/.cache/torch \ PLAYWRIGHT_BROWSERS_PATH=/app/.cache/ms-playwright \ PUPPETEER_CACHE_DIR=/app/.cache/puppeteer RUN mkdir -p /app/.cache/tiktoken /app/.cache/huggingface /app/.cache/nltk_data /app/.cache/easyocr /app/.cache/torch /app/.cache/ms-playwright /app/.cache/puppeteer # Pre-warm Python tiktoken encodings at build time RUN TIKTOKEN_CACHE_DIR=/app/.cache/tiktoken python3 -c "import tiktoken; [tiktoken.get_encoding(e) for e in ('cl100k_base', 'o200k_base')]" # Pre-warm ChromaDB's default ONNX embedding model at build time RUN HOME=/app XDG_CACHE_HOME=/app/.cache python3 -c "from chromadb.utils.embedding_functions import ONNXMiniLM_L6_V2; ef = ONNXMiniLM_L6_V2(); ef(['airgap-warmup'])" # --- END HUMANET AIRGAP-CHECK REMEDIATION ---
How --run works, and where it stops
--run starts a uniquely named ephemeral container (--rm --network none --entrypoint ""), mounts your repository read-only alongside a bind-mounted, fsync-flushed attempts.jsonl log, and injects an isolated loopback observer (runner.py in Python containers, or runner.js in Node-only containers; pass --build to build the repo's Dockerfile first):
- Loopback DNS sinkhole (
127.0.0.1:53/udp) with/etc/resolv.confupdated inside the ephemeral container. In the defaultsinkholemode (--dns-action sinkhole),Aqueries resolve to127.0.0.1so outbound connections continue to our loopback TLS/HTTP listeners rather than dying at DNS before showing their HTTP path or SNI hostname. Pass--dns-action nxdomainto fail immediately at DNS resolution. - TLS
ClientHelloSNI (127.0.0.1:443) & HTTP (127.0.0.1:80) sinkholes, plus anHTTP_PROXY/HTTPS_PROXYloopbackCONNECTfallback when running as a non-root container user without bind privileges on ports< 1024. Query strings are stripped from captured URLs. - Python (
sitecustomize.py) & Node (node_preload.js) socket hooks recordingsocket.getaddrinfo(including Pythonbyteshosts),socket.connect,dns.lookup/dns.promises.lookup, andnet.Socket.connect(covering Node 20fetch()/http/https). - Timeout & detached-process handling: After the main process exits, the runner polls
/procbriefly (up to 1 s) so detachedatexitbackground telemetry processes finish writing before log collection. On host timeout (TIMEOUT_EXCEEDED_IN_AIRGAP),airgap-checkrunsdocker rm -fon the named container and preserves attempts written prior to timeout via the bind-mounted log. Non-zero exit or caught silent egress setscrashed_in_airgap(withSILENT_EGRESS_ATTEMPTED_AND_DROPPEDwhen a command exits0after attempting outbound egress). Error-text fallback URL extraction ignores traceback documentation links (errors.pydantic.dev,cloud.google.com,sharp.pixelplumbing.com, etc.).
Limits:
- Loopback sinkholes (
runner.pyandrunner.js) were validated in realdocker run --network nonecontainers for Python (python:3.12-slim-bookworm), pure Node.js (node:20-bookworm-slim), and builtDockerfileimages. Other runtimes without Python or Node fall back toshell_onlyerror URL extraction and are not separately validated. v0.2.2--runhardening fixes (--entrypoint ""override,byteshost decoding, detachedatexitchild capture,docker rm -ftimeout cleanup, and Node 20fetch()tracking) were verified via realdocker run --rm --network noneintegration tests ingo test -v ./...; full upstream Benchmark v2 container runs were executed earlier on frozenv0.2.1(47b2ddc).- Traffic to hardcoded external IP addresses (not hostnames) fails with
ENETUNREACHin the kernel without hitting loopback SNI/HTTP listeners, thoughsitecustomize.py/node_preload.jscapture literal connect targets in Python/Node. DNS-over-HTTPS traffic skips the system resolver unless SNI reveals the DoH provider. - Non-root containers running compiled Go/Rust binaries that ignore
HTTP_PROXYand cannot bind ports 53/80/443 inside the unprivileged user namespace will logENETUNREACHrather than SNI unless run as root inside the throwaway test container. - Only the code paths you exercise are tested. Use
--run-cmdto run a smoke test that covers the paths you care about. - The sinkhole answers immediately. In a real enclave, packets may be dropped instead, so hangs can be longer than the times below.
Benchmark
We evaluated airgap-check across three probe-based phases (30 Python and TypeScript AI repositories documented in benchmark/REPOS.md: Phase 1 calibration, Phase 2 held-out v0.1.0 -> v0.2.0 design, and Phase 3 out-of-sample v0.2.1) plus an independent quickstart/strace validation suite (Benchmark v2, 10 pre-registered repositories + Backup B1 AutoGPT documented in benchmark/REPOS2.md):
- Phases 2 & 3 are probe-based tests: we did not start the applications themselves. A “true positive” means the flagged library fetches something when its call runs offline (or executes a runtime installer/download); it does not prove the repository reaches that call in normal operation. Telemetry findings (for example langsmith, posthog or sentry) and advisory
robustness_hints(timeout=omissions) are not scored in precision/recall. - Benchmark v2 removes that circularity by running both
(b)documented application startup/import/CLI commands underdocker run --rm --network none+ rawstrace -f(ENETUNREACHindependent ground truth) and(a)static-derived probes against frozenv0.2.1(47b2ddc), before fixing the exposed bugs inv0.2.2.
What the libraries do offline
| Library / call | Behaviour with no network | Host it tries | Observed |
|---|---|---|---|
fastembed TextEmbedding() |
Retries 3 times (3 s, 9 s, 27 s), then raises ValueError: Could not load model |
huggingface.co |
~39 s |
huggingface_hub hf_hub_download() |
Retries metadata checks with backoff, then raises LocalEntryNotFoundError |
huggingface.co |
~23.5 s |
chromadb collection.add() |
Default embedding function downloads the all-MiniLM-L6-v2 ONNX model |
chroma-onnx-models.s3.amazonaws.com |
fails in under 1 s |
tiktoken (Python) get_encoding() |
Fetches the BPE rank file on first use | openaipublic.blob.core.windows.net |
fails in under 1 s |
nltk nltk.download() |
Tries to fetch the index and corpus | raw.githubusercontent.com |
fails in under 1 s |
playwright chromium.launch() |
Fails if the browser binary was not baked into the image | playwright.azureedge.net |
fails in under 1 s |
litellm |
Fetches its model price map at startup unless LITELLM_LOCAL_MODEL_COST_MAP=True |
raw.githubusercontent.com |
fallback warning |
crewai, chromadb |
Background telemetry threads on startup unless disabled | telemetry.crewai.com:4319, us.i.posthog.com |
background timeouts |
Timings in the table above were measured with v0.1.0 (immediate NXDOMAIN); v0.2.0+ defaults to sinkhole mode (127.0.0.1), where connections reach a local loopback TLS/HTTP listener first and hit handshake/socket errors instead of immediate DNS errors, which changes retry behaviour across HTTP clients (for example, huggingface_hub on smolagents exited in 293 ms under sinkhole vs 477 ms under --dns-action nxdomain). Browser-side telemetry, such as posthog-js in a frontend bundle, is a separate case: server environment variables do not silence it, because the requests leave from the user's browser.
Phase 2 held-out results (v0.1.0, rules frozen at 8ce2f11)
| Repository | Probe result under --network none |
TP | FP | FN |
|---|---|---|---|---|
gpt-engineer |
exit 1 (tiktoken) |
1 | 0 | 0 |
gpt-researcher |
exit 1 (tiktoken, nltk) |
4 | 0 | 0 |
llama_index |
exit 1 (tiktoken) |
8 | 0 | 2 |
mem0 |
exit 1 (chromadb ONNX) |
6 | 0 | 0 |
vanna |
exit 1 (chromadb ONNX) |
4 | 0 | 0 |
quivr |
exit 1 (huggingface.co, ~23.5 s of retries) |
3 | 0 | 0 |
docling |
exit 1 (huggingface.co) |
3 | 1 | 2 |
litellm |
exit 1 (raw.githubusercontent.com) |
3 | 1 | 1 |
pydantic-ai |
exit 1 (tiktoken) |
3 | 2 | 0 |
chat-langchain |
exit 0, no asset downloads | – | 0 | 0 |
At component level (v0.1.0): 35 true positives, 4 false positives, 5 false negatives. That is 89.7% precision and 87.5% recall. This is a small sample, so treat the numbers as rough. At repository level, all 9 repositories with first-use downloads were flagged, and chat-langchain was correctly flagged as clean.
False positives (all 4 in v0.1.0): pyproject.toml was scanned as plain text, so dev and test dependency groups (playwright in docling, litellm, pydantic-ai) and an optional extra named posthog triggered warnings.
False negatives (all 5 in v0.1.0): OCR weight downloaders (easyocr, rapidocr, paddleocr), FlagEmbedding, and LiteLLM's startup fetch of model_prices_and_context_window.json were not in the frozen v0.1.0 rule table.
v0.2.0 design fixes (same Phase 2 commits, not out-of-sample)
These 10 Phase 2 repositories were used to design the
v0.2.0fixes, so re-run numbers on them are no longer out-of-sample. See the fresh Phase 3 held-out set below for out-of-samplev0.2.1validation.
- Addresses all 4
v0.1.0dev-group / key-name false positives (0dev/lint/key-name false positives; optional extras such asformat-html-renderindoclingor[extra: playwright]inpydantic_ai_harnessare tagged explicitly asoptional_extrarather thanprod). - Addresses the 5
v0.1.0false negatives (FlagEmbedding,paddleocr,easyocr,rapidocr, andlitellm's startupmodel_prices_and_context_window.jsonfetch — the latter caught generically by the behavioral URL scanner without requiring a package rule). - Surfaces transitive air-gap dependencies via lockfile traversal (
huggingface-hubviadatasetsingpt-engineer'spoetry.lock,spacyandrapidocrviauv.lockinllama_index,@huggingface/transformersviapnpm-lock.yamlinmem0,tiktokenviauv.lockinpydantic-ai).
Phase 3 held-out results (v0.2.1, rules frozen at 47b2ddc)
To measure v0.2.1 strictly out-of-sample, we froze all detection rules at commit 47b2ddc before cloning 10 previously unseen AI repositories (0.02 s – 0.59 s per scan):
| Repository | Probe result under --network none (sinkhole) |
TP | FP | FN |
|---|---|---|---|---|
smolagents |
exit 1 (huggingface.co, 293 ms) |
6 | 1 | 0 |
crawl4ai |
exit 1 (raw.githubusercontent.com, openaipublic.blob.core.windows.net, 345 ms) |
10 | 0 | 2 |
aider |
exit 1 (openaipublic.blob.core.windows.net, 358 ms) |
7 | 0 | 1 |
open-webui |
exit 1 (chroma-onnx-models.s3.amazonaws.com, 557 ms) |
14 | 0 | 2 |
browser-use |
exit 1 (openaipublic.blob.core.windows.net, 212 ms) |
1 | 0 | 0 |
markitdown |
exit 0, no runtime downloads (163 ms; magika bundles weights locally) |
0 | 0 | 0 |
crewAI |
exit 1 (chroma-onnx-models.s3.amazonaws.com, 592 ms) |
13 | 3 | 2 |
openai-agents-python |
exit 0, core runtime clean (419 ms; [extra: litellm] flagged) |
1 | 0 | 0 |
servers |
exit 0, no runtime downloads (153 ms, node:20-bookworm-slim) |
0 | 0 | 0 |
instructor |
exit 0, core runtime clean (417 ms; [extra: litellm] flagged) |
1 | 0 | 0 |
At finding level (v0.2.1, out-of-sample): 53 true positives, 4 false positives, 7 false negatives (93.0% precision, 88.3% recall). All 8 repositories containing runtime downloaders or optional-extra downloads were flagged (while dev-only playwright==1.63.0 inside openai-agents-python's [dependency-groups] dev produced 0 false positives), and both clean repositories (markitdown, servers) were flagged clean. Separating missing timeout= calls out of behavioral_findings into advisory robustness_hints prevented 37 HTTP-timeout hints (26 in open-webui alone) from dropping out-of-sample precision from 93.0% to 56.4%.
- False positives (4):
smolagentsmatched a docstring example (>>> tokenizer = AutoTokenizer.from_pretrained(...)in_function_type_hints_utils.py);crewAImatched Vertex AI SDK'sTextEmbeddingModel.from_pretrained(...)(genai_vertex_embedding.py), a private maintenance script (lib/devtools/src/crewai_devtools/litellm_catalog_update.py), andarxiv_paper_tool.py's user-triggeredurlretrieve(pdf_url). - False negatives (7):
crawl4ai/model_loader.pyhas a multi-linesubprocess.run(["git", "clone", ...])call (line-by-line regex missed it) andspacy.load();aider/models.pysplitsmodel_prices_and_context_window.jsonacross two adjacent string literals (thoughlitellminrequirements.txtsurfaced the host);open-webuimissedspacytransitively underunstructuredinuv.lockandkokoro-jsinpackage.json(kokoro.worker.tsfrom_pretrained()was caught);crewAImisseddocling([extra: docling]+DocumentConverter()) andspacyunder optional extras. Also note:aidermapsrequirements-help.txtandrequirements-playwright.txtvia[tool.setuptools.dynamic.optional-dependencies], so the minimal TOML reader tagged those 4 extras asprodrather thanoptional_extra, andbrowser-useimplements PostHog telemetry via raw stdliburllibrather than theposthogPyPI package (unscored).
Per-repository SHAs, manifests, lockfiles, scan durations, and probe transcripts are recorded in benchmark/REPOS.md.
Benchmark v2 held-out + quickstart/strace validation (v0.2.1, rules frozen at 47b2ddc)
To break probe circularity and test non-Python heavy apps, Node backends, Go rewrites, and negative controls (openai-agents-python, LibreChat), we froze v0.2.1 at 47b2ddc, pre-registered 10 target repos + Backup B1 AutoGPT in benchmark/REPOS2.md, and ran documented quickstarts/CLIs under raw strace -f inside --network none alongside static probes (ragflow was replaced dynamically by B1 AutoGPT because its multi-stage Dockerfile + multi-GB ragflow_deps image exceeded the 20-minute build cap):
| Repository | (b) Quickstart / CLI (--network none + strace) & (a) Probe |
TP | FP | FN |
|---|---|---|---|---|
open-webui |
uvicorn boots & seeds SQLite (22,481 ms, exit 137) trying HF embeddings (13,067 ms) & api.github.com release check; (a) crashes on tiktoken/chromadb/faster-whisper/unstructured/playwright/spacy (rapidocr blank-image clean) |
14 | 0 | 4 |
anything-llm |
Node server/index.js exits 0 (SILENT_EGRESS, 5,371 ms) fetching models.dev & raw.githubusercontent.com; (a) tesseract.js (cdn.jsdelivr.net), @xenova/transformers fail; js-tiktoken clean |
4 | 0 | 3 |
dify |
flask db --help / import app clean; check_version_update hits updates.dify.ai (9,572 ms); telemetry hits otel.dify.ai; (a) tiktoken/transformers/chromadb/nltk/spacy fail |
7 | 1 | 2 |
ragflow |
Container build blocked (>20 min ragflow_deps multi-stage). Go backend (localBpeLoader) is disk-only in source; all 5 v0.2.1 findings were FPs (build/eval scripts + Go hash map) |
0 | 5 | 0 |
haystack |
Default DocumentSplitter() clean aside from eu.i.posthog.com (1,582 ms); split_by="sentence" crashes (nltk); (a) tiktoken/nltk fail |
5 | 0 | 0 |
browser-use |
--help exits 0 cleanly (187 ms); ProductTelemetry().flush() hits eu.i.posthog.com (1,831 ms, SILENT_EGRESS); (a) tiktoken fails |
1 | 0 | 0 |
aider |
aider --no-git --yes --exit --model gpt-4o tries pypi.org (versioncheck.py) & raw.githubusercontent.com (models.py + litellm, 1,518 ms); (a) tiktoken fails |
7 | 0 | 2 |
Flowise |
bin/run start clean for 28 s (exit 137); (a) langchainhub (563 ms, api.hub.langchain.com), playwright, puppeteer fail; @dqbd/tiktoken clean (429 ms) |
3 | 0 | 2 |
openai-agents-python |
Negative control 1: Agent() clean (752 ms); Runner.run_sync() emits background tracing to api.openai.com/v1/traces/ingest unless OPENAI_AGENTS_DISABLE_TRACING=1 |
1 | 0 | 4 |
LibreChat |
Negative control 2: countTokens() clean (1,518 ms, 0 network syscalls); server connects only to loopback MongoDB (127.0.0.1:27017) |
0 | 0 | 0 |
AutoGPT (Backup B1) |
autogpt --help & import clean; autogpt run (TELEMETRY_OPT_IN=true) crashes via sentry_sdk->tiktoken; gtts hits translate.google.com; spacy wheel pin clean (exit 0) |
6 | 3 | 2 |
At finding level on frozen v0.2.1 (47b2ddc, strictly out-of-sample):
- Pre-registered Primary 10 (
#1–#10, includingragflowstatic):42 TP,6 FP,17 FN(87.5% precision, 71.2% recall) - Executable 10 (
#1–#3, B1, #5–#10, substituting B1AutoGPTfor blockedragflowcontainer):48 TP,4 FP,19 FN(92.3% precision, 71.6% recall) - All 11 Scanned (
#1–#10 + B1):48 TP,9 FP,19 FN(84.2% precision, 71.6% recall)
What independent (b) quickstarts/strace uncovered that static-only benchmarks missed:
- False positives (
9):difypnpm v9 optionalpeerDependenciesMetaedge (next@16.4.5->@playwright/test),ragflowbuild/dev scripts (download_deps.py,gen_tokenizer_oracle.py) + Go hash verification table (expectedBpeHashes), andAutoGPTeval CLI scripts (eval/prefix.py,eval_corpus.py) + pinneden-core-web-smwheel URL. - False negatives (
19): runtime API/update checks (models.dev/api.json,pypi.org/pypi/.../json,api.github.com/.../releases,updates.dify.ai), multiline Python expression strings (MODEL_INFO_URL = """...""") inverting docstring masking inaider,[tool.uv] default-groups,uv.lockoptional-extra BFS (spacyunderunstructured,huggingface-hub/tiktokenunderlitellm), multilinesubprocess.run(["git", "clone", ...]),gtts,tesseract.js,kokoro-js, andlangchainhub. - Unscored telemetry &
--runobserver bugs: custom stdlib senders without SDK packages (browser-use,dify,haystack,openai-agents/v1/traces/ingest),ENTRYPOINThijack on pre-built images, Pythonbyteshost formatting (b'...'), noisy traceback doc URLs (errors.pydantic.dev,sharp.pixelplumbing.com), and timeout / detached-atexitlog races.
v0.2.2 post-diagnosis fixes (same Benchmark v2 commits, NOT out-of-sample)
These 11 Benchmark v2 repositories were used to diagnose and design the
v0.2.2fixes, so static re-scan numbers below are in-sample verification, NOT out-of-sample generalization metrics.
- Fixes all 9
v0.2.1Benchmark v2 false positives and all 19 false negatives on the pinned Benchmark v2 snapshots (67 TP,0 FP,0 FNon static re-scan; plus 3.envtoken-limit FPs removed onragflowand custom stdlib telemetry URLs surfaced inbrowser-use,dify,haystack, andopenai-agents-pythonwhile filtering UI credential/placeholder forms andconftest.pytest files). - Fixes all 4
--runobserver issues (--entrypoint "", bind-mountedfsynclog +docker rm -fon timeout, Pythonbytesdecoding,/procdetached-child wait, andruntime_error_urlerror-context filtering). Verified via realdocker run --rm --network nonetests ingo test -v ./...(18/18passing); the 46 upstream Benchmark v2 application containers were not re-run onv0.2.2. Full details and per-repo deltas are inbenchmark/REPOS2.md.
Supported checks
| Category | Pattern | Host(s) | --fix |
|---|---|---|---|
| Runtime download | tiktoken (Python & tiktoken-rs) |
openaipublic.blob.core.windows.net |
pre-warms cl100k_base, o200k_base |
| Runtime download | chromadb default embedder |
chroma-onnx-models.s3.amazonaws.com |
pre-warms ONNXMiniLM_L6_V2 |
| Runtime download | fastembed, TextEmbedding() |
huggingface.co, cdn-lfs.hf.co |
pre-warms the model cache |
| Runtime download | sentence-transformers, transformers, @huggingface/transformers, from_pretrained(), hf_hub_download(), snapshot_download() |
huggingface.co, cdn-lfs.hf.co |
sets offline ENV flags; prints model snapshot template (skips local_files_only=True & Vertex AI SDK) |
| Runtime download | FlagEmbedding (FlagModel, FlagReranker, BGEM3FlagModel) |
huggingface.co, cdn-lfs.hf.co |
sets offline ENV flags; prints snapshot template |
| Runtime download | easyocr, rapidocr*, paddleocr |
github.com, releases.jaided.ai, huggingface.co, modelscope.cn, paddleocr.bj.bcebos.com |
pre-warms OCR weights / EASYOCR_MODULE_PATH |
| Runtime download | tesseract.js |
cdn.jsdelivr.net, tessdata.projectnaptha.com |
hints pre-baking *.traineddata & local langPath |
| Runtime download | whisper.load_model() / faster-whisper / mlx-whisper |
openaipublic.azureedge.net, huggingface.co |
pre-warms the checkpoint |
| Runtime download | gtts, gtts.gTTS(), kokoro, kokoro-js |
translate.google.com, huggingface.co, cdn-lfs.hf.co |
flags online TTS (gtts) or pre-bake requirement (kokoro*) |
| Runtime download | litellm startup cost map |
raw.githubusercontent.com |
sets LITELLM_LOCAL_MODEL_COST_MAP=True |
| Runtime download | playwright, puppeteer |
playwright.azureedge.net, storage.googleapis.com |
installs Chromium (python3 -m playwright / npx playwright / npx puppeteer) |
| Runtime download | nltk.download(), spacy.cli.download(), spacy.load() |
raw.githubusercontent.com |
pre-downloads corpora / model wheel (suppresses pinned wheel URLs) |
| Runtime download | langchainhub / @langchain/hub (hub.pull()), torch.hub.load(), timm, ultralytics, unstructured, surya-ocr, docling-ibm-models |
api.smith.langchain.com, github.com, download.pytorch.org, huggingface.co |
vendor prompt templates or pre-bake TORCH_HOME / HF_HOME |
| Build-time download | next/font/google |
fonts.googleapis.com, fonts.gstatic.com |
flagged only; suggests next/font/local |
| Behavioral heuristics | Remote .onnx/.safetensors/.gguf/cost-map URLs (incl. split-line strings), runtime update/pricing URLs (models.dev, pypi.org, GitHub releases, updates.*), urlretrieve(), archive fetch+unpack, runtime subprocess (pip install, curl, wget, git clone, ollama pull), hardcoded ~/.cache writes |
dynamic / inferred hosts | hints detected hosts (skips hash-verification tables) |
| Robustness hints (advisory) | Python requests/urllib HTTP calls omitting timeout= |
— | separated into robustness_hints; suggests timeout=(2, 10) |
| Server telemetry | chromadb, crewai, langsmith, langfuse, wandb, opentelemetry-exporter-otlp, ddtrace, gradio, scarf, openai-agents (/v1/traces/ingest), direct stdlib PostHog/OTLP URLs |
PostHog, telemetry.crewai.com, otel.*, api.smith.langchain.com, cloud.langfuse.com, api.wandb.ai, Datadog, api.gradio.app, scarf.sh, api.openai.com |
sets matching disable flags (OPENAI_AGENTS_DISABLE_TRACING=1, CHROMA_TELEMETRY_IMPL=none, etc.) |
| Browser telemetry | posthog-js, @sentry/*, frontend langsmith / direct URLs under frontend/ / web/ / packages/ui/ |
PostHog, Sentry, LangSmith | flagged only; disable the SDK in frontend code (UI placeholders/links ignored) |
.env keys |
OPENAI_*, ANTHROPIC_*, GEMINI_*, STRIPE_*, JWT_*, CLOUDINARY_*, AWS_SECRET* and others |
n/a | flagged only; classifies key names & git tracking status (excludes token-limit knobs) |
Custom rules (.airgap-check.json / --rules)
Drop .airgap-check.json in your repository root (or pass --rules path/to/rules.json) to add internal packages or suppress known-good components/hosts:
{
"rules": [
{
"package_or_pattern": "internal-embedder",
"component": "internal-embedder-sdk",
"category": "runtime_model_asset",
"severity": "CRITICAL",
"hosts": ["models.internal.corp"],
"impact": "Fetches internal weights at boot.",
"mitigation": "Bake weights into /app/models during build.",
"python_only": true
}
],
"ignore_components": ["gradio"],
"ignore_hosts": ["api.gradio.app"]
}Design principles
- Secret values are never read, stored or logged.
.envfiles are inspected for key names, non-empty presence, andgit ls-filesstatus. --runmounts your repository read-only.
Humanet airgap-check is not a security certification. It reports what it observed; a clean result does not prove that your application is safe to run in an isolated network.
Contributing
Found a library that quietly phones home, or a false positive on your repo? Open an issue with the package name, the call, and the host it tries to reach, and we will add or fix the rule.
About
Open-sourced by Humanet, which is also building Frame, an early-stage hosting platform.
License
MIT
