Skip to main content

borderlint

PyPI GitHub Marketplace

Map and govern where your AI data and traffic flow — who can compel its disclosure, and whose model weights it runs.

A static, in-CI linter for AI data residency, sovereignty, and model provenance across APAC & EMEA, with first-class HK / GBA support. Scans Python, TypeScript/JavaScript, Java/Kotlin, C#, config files, and MCP server configs. Zero runtime dependencies.

Quick start

pip install borderlint

borderlint scan .        # inventory mode: where does this repo's AI data flow today?
borderlint init .        # short interview → residency.json, grounded in what the scan found
borderlint scan . --policy residency.json --classification customer-pii   # now it gates

A violation looks like this (--explain adds the arrow lines):

borderlint — AI data-flow & residency report
==============================================
[FAIL] DeepSeek -> Mainland China | sovereignty: Mainland China | weights: Mainland China
        app.py:2 (endpoint_reference: api.deepseek.com)
           ! jurisdiction outside the allow-list for this data class
             → DeepSeek resolves to Mainland China, which is not allowed for this data class.
               Update the policy allow-list or change the provider/endpoint.

Regimes implicated: PIPL
Reference: PIPL cross-border transfer — Mainland China cross-border PI: CAC security assessment, …
Summary: 1 fail, 0 warn, 0 waived, 0 ok

Exit code is non-zero on a violation, so the same command gates CI unchanged.

What it checks

Every detected AI flow is evaluated on three orthogonal axes:

  • Residencywhere the bytes rest: each flow resolves to a jurisdiction; outside the allow-list for the declared data class fails the build.
  • Sovereigntywhich government can compel disclosure: a US-headquartered provider is under the CLOUD Act regardless of endpoint region.
  • Provenancewhose model weights the flow runs: model references resolve to the developer's bloc.

One flow, three different answers: AWS Bedrock ap-east-1 serving DeepSeek-R1 is residency hk, sovereignty us, provenance cn — and self-hosted Qwen is local/local/cn. Declare a home base (HK, Macao, the GBA, Japan, Korea, Singapore, Australia, the UK, the EU, Malaysia) and flagged flows are tagged with the regime in play (PDPO, PIPL, APPI, PIPA, PDPA, the Privacy Act, GDPR …) and its cross-border reference. Western and Chinese providers are treated evenly.

Browse the full knowledge base — 100 providers, model developers, regimes — at iolairus.github.io/borderlint.

Use

python -m borderlint scan ./service --policy residency.json --classification customer-pii
  • No --policyinventory mode (lists flows + jurisdictions, exits 0).
  • --explain — plain-language explanation and remediation hint under each violation (text: a line per reason; json: an explanation field per finding). Advisory only.
  • --providers internal-endpoints.json — merge your own endpoint map (below).
  • --format <name> — see output formats.
  • diff <baseline.sbom> <current.sbom> — compare two SBOMs; exits 1 when the PR adds a new non-local flow (new egress), else 0.
  • init [path] — scaffold residency.json via a short interview (or non-interactively: borderlint init . --home hk --classes customer-pii,non-pii).
  • Accept a reviewed flow with an inline # borderlint: allow <reason> waiver (justification required; reported as waived, not hidden; can't override an explicit provider deny).
  • MCP configs are scanned too: .mcp.json, claude_desktop_config.json, and the Cursor / VS Code equivalents — one finding per configured server (details).

Policy (the eval-set)

residency.json maps each data class to the jurisdictions you accept, and optionally to the sovereignty and provenance blocs you accept for compelled-disclosure and model-origin exposure:

{
  "home_location": "hk",
  "classifications": {
    "customer-pii": ["hk", "CN-GBA", "sg"],
    "employee-pii": ["hk", "CN-GBA"],
    "non-pii":      ["hk", "CN-GBA", "cn", "mo", "sg", "us", "gb"]
  },
  "sovereignty": {
    "on_unknown": "warn",
    "classifications": { "customer-pii": ["eu", "uk", "local"] }
  },
  "provenance": {
    "on_unknown": "warn",
    "classifications": { "customer-pii": ["us", "eu", "uk"] },
    "deny_models": []
  }
}

Residency — deny-by-default: a flow to any code not on the list for the declared class fails — so sg is allowed but my is not, matching a PDPO agreed-locations EULA. GBA is shorthand for hk + CN-GBA.

Sovereignty — opt-in, orthogonal to residency. Residency says where the bytes rest; sovereignty says which government can compel disclosure — a US provider (AWS, Azure, GCP, OpenAI) is subject to the CLOUD Act regardless of the endpoint region. Add a sovereignty block to constrain it per class. Bloc vocabulary: us, eu, cn, uk, ru, in, il, ca, jp, kr, sg, au, ae, ch, local, unknown. Absent the block, behaviour is unchanged (sovereignty is reported as a column but never gates). local sovereignty is exempt (self-hosted = no external sovereign). See CAPABILITIES.md §3.1 for the full model.

Provenance — opt-in, orthogonal to both. Whose model weights a flow runs, resolved in two tiers: a model reference bound to the flow wins; absent one, a provider that serves only its own models (OpenAI, Anthropic, DeepSeek …) resolves to its org's bloc, while multi-model hosts (Bedrock, Vertex) and aggregators stay unknown. Aggregator-qualified IDs (deepseek/deepseek-r1 via OpenRouter) resolve provenance even where residency and sovereignty are unknown — the one axis routers don't obscure. The provenance block mirrors the sovereignty shape; vocabulary is the same minus local (weights always have a developer). Fine-tunes inherit the base family's bloc. A deny_models list of anchored model-id prefixes bans a family regardless of host or bloc — "deny_models": ["deepseek"] fails a Bedrock flow serving DeepSeek-R1 even where cn weights are otherwise allowed; denies match after the same normalization as the map (GGUF paths, redistributor repos, @-version pins can't dodge them), sit in the default failure set like the provider deny, and cannot be waived inline. See CAPABILITIES.md §3.2.

Declare your home_location — a GBA seat (hk/mo/CN-GBA) or an APAC/EMEA seat (jp, kr, sg, au, uk, eu, my) — and a flagged flow is tagged with the data-protection regime in play and linked to the relevant cross-border arrangement (the matching GBA Standard Contract variant, PIPL cross-border, GDPR, the UK IDTA, APPI Art. 28, PIPA Art. 28-8, PDPA s.26/s.129, APP 8) as reference links. (home_regime pdpo/pipl is still accepted.)

CI

Same command in any pipeline. GitHub Actions (composite action):

- uses: iolairus/borderlint@v1.14.1
  with: { path: ., policy: residency.json, classification: customer-pii }

Jenkins / anything else: pip install borderlint && borderlint scan . --policy residency.json --classification customer-pii — a non-zero exit fails the stage. Full examples in examples/ci/.

pre-commit — catch a bad flow before it's committed (.pre-commit-config.yaml):

- repo: https://github.com/iolairus/borderlint
  rev: v1.14.1
  hooks:
    - id: borderlint
      args: [--policy, residency.json, --classification, customer-pii]

The hook runs borderlint scan over the repo (the args are required for a real gate; without a policy it runs inventory mode and always passes).

Agentic coding & MCP

The fastest-growing source of unreviewed AI egress is the agent stack itself — the coding agent reaching for an SDK, and the MCP servers wired into it.

MCP config scanning: .mcp.json, claude_desktop_config.json, .cursor/mcp.json, and .vscode/mcp.json are parsed structurally — one finding per configured MCP server (kind mcp_server). Remote (url) servers resolve their host against the provider KB; stdio servers resolve their package against a bundled MCP-server map (data/mcp_servers.json); anything unmapped surfaces as an explicit unknown, and purely local servers (filesystem, memory, your own database) as local. Claude Desktop's config lives outside your repo — scan it directly: borderlint scan ~/Library/Application\ Support/Claude/claude_desktop_config.json.

Agent integrations: integrations/ ships an installable Claude Code plugin and copy-paste rules that make the agent run borderlint before adding an AI dependency, endpoint, or model id — and surface any new non-local flow in the conversation, before it is committed:

Advisory by design: the pre-commit hook and the SBOM diff gate stay on as the enforcing backstop, and accepted flows are recorded with the inline waiver rather than hidden.

Output formats

Format What you get
text (default) The human-readable report above
json Machine-readable findings, all three axes per flow
mermaid A data-flow map grouped by jurisdiction
sarif SARIF 2.1.0 for GitHub code scanning
sbom A deterministic AI data-flow SBOM (feeds the diff gate)
evidence A fileable markdown transfer inventory with audit envelope and regime annex
html One self-contained file to hand your DPO or General Counsel
badge shields.io endpoint JSON: pass/fail/flow-count
suricata A Suricata TLS-SNI alert ruleset compiled from the KB and policy — network-level detection of AI egress the code scan can't see

Exports (sbom, evidence, html, badge, suricata) are artifacts, not gates: they exit 0.

The evidence pack carries an audit envelope (git commit, policy SHA-256, KB review dates), all three governance axes with developer orgs, a waiver register, and a regime annex (PDPO, PIPL + GBA SC, Macao PDPA, PDPA-SG) that fills what the scan proves and leaves marked blanks for what only the organisation knows.

The badge renders green for clean, red for violations, yellow for warnings, blue for inventory mode (flow count):

{"schemaVersion": 1, "label": "borderlint", "message": "clean", "color": "green"}

The suricata ruleset is derived from the KB and policy, not from scan findings — it covers providers your code doesn't use yet, so runtime egress (an env-configured base URL, an agent's tool call) still trips an alert at the network boundary. One rule per KB endpoint host whose jurisdiction the policy disallows (region-selectable clouds always alert, marked region-dependent); without a policy, every known AI endpoint alerts. Alert posture — the header documents converting to drop for inline IPS. sids are deterministic per KB version; regenerate rather than diff. Validated against Suricata 8.

Publish the badge by writing the JSON to any static host (GitHub Pages, a gist) on every push:

- run: borderlint scan . --policy residency.json --classification customer-pii --format badge > badge.json

then reference https://img.shields.io/endpoint?url=https://your-host/badge.json in your README.

Internal endpoints

Map your own regional endpoints to jurisdictions; they merge with the bundled provider KB (your entries win on conflict):

{ "endpoints": { "llm-cn.acme.internal": "cn", "llm-hk.acme.internal": "hk", "llm-sg.acme.internal": "sg" } }
borderlint scan . --providers internal-endpoints.json --policy residency.json --classification customer-pii

A configuration wired to the wrong regional endpoint — e.g. the CN endpoint for HK-only customer PII — then fails the build, so you can't ship a service pointed at the wrong region.

A runnable end-to-end example is in examples/gba-resident-app/ — a GBA-resident app (internal Shenzhen endpoint → CN-GBA, plus Mainland / Western / local fallbacks). Run it under residency-hk.json vs residency-mo.json and the surfaced GBA Standard Contract flips between the (Mainland, Hong Kong) and (Mainland, Macao) variant, and the regime tag between PDPO and Macao PDPA:

borderlint scan examples/gba-resident-app \
  --providers examples/gba-resident-app/internal-endpoints.json \
  --policy examples/gba-resident-app/residency-hk.json --classification customer-pii

The same scan renders to a data-flow map grouped by jurisdiction — Mermaid source in dataflow.mmd, rendered to PNG:

borderlint AI data-flow map for the GBA-resident sample app, grouped by jurisdiction

Capabilities

  • Languages: Python (AST), TypeScript/JavaScript (import / require / dynamic import()), Java/Kotlin (import / import static, incl. LangChain4j and Spring AI as runtime-routed aggregators and the official OpenAI/Anthropic/Bedrock/Vertex/Azure JVM SDKs), and C# (using directives incl. global/static/alias forms — the official OpenAI/Anthropic/Azure/Bedrock/Google .NET SDKs, plus Semantic Kernel and Microsoft.Extensions.AI as runtime-routed aggregators), plus endpoint references in config/text files (incl. env-style keys like MYAPP_LLM_SERVER_URL in .env, compose, and settings files) and OpenAI-compatible /v1/chat/completions calls — even to a runtime-configured host (resolved to unknown, so on_unknown: fail gates it).
  • Providers: 100+ across the east-west boundary — OpenAI, Anthropic, Google (Gemini + Vertex AI), Azure, Bedrock, Mistral, Cohere, Groq, Together, Perplexity, xAI, Cerebras, Fireworks, Replicate, SambaNova, Meta Llama, AWS SageMaker, Snowflake Cortex + Tencent, Alibaba, DeepSeek, Moonshot, Zhipu/Z.ai, Baidu, Volcengine, MiniMax, Huawei ModelArts, plus AI21 (IL), Jina (DE), Voyage, GigaChat (RU), Sarvam (IN), Scaleway & OVHcloud (FR/EU) and region-selectable clouds (IBM watsonx, Oracle OCI, Cloudflare Workers AI, Herokuunknown until you pin a region) — with Python and JS/TS package names and the Vercel AI SDK (@ai-sdk/*).
  • MCP configs: .mcp.json, claude_desktop_config.json, Cursor / VS Code — one mcp_server finding per configured server, resolved via the provider KB and the bundled MCP-server map.
  • Image / video / speech: generation (Stability AI, Black Forest Labs/Flux, Runway, Recraft) and speech-to-text / TTS (ElevenLabs, Deepgram, AssemblyAI, Soniox, Amazon Polly) — tagged with their category and governed for residency like any other flow.
  • Vector stores (data sinks): Pinecone, Weaviate Cloud, Qdrant Cloud, Zilliz/Milvus — flagged as vector_store and governed for residency (region is per-cluster, so default unknown).
  • Aggregators / routers: litellm, langchain, LlamaIndex, aisuite, OpenRouter, AI/ML API, Vercel AI core & Gateway → unknown (runtime-routed), so on_unknown: fail blocks them for sensitive classes.
  • Jurisdictions: ccTLD/ISO codes + CN-GBA / GBA; AWS / Azure / GCP-Vertex region resolved from the endpoint host where present (e.g. bedrock-runtime.ap-east-1… and asia-east2-aiplatform.googleapis.comhk).
  • Sovereignty: a per-flow bloc (us, eu, cn, uk, ru, in, il, ca, jp, kr, sg, au, ae, ch, local, unknown) derived from the provider's home legal regime — orthogonal to residency. Opt-in policy block; reported in every output format; host-level overrides for ring-fenced subsidiaries (e.g. AWS China / Sinnet → cn).
  • Provenance: whose model weights a flow runs — a third orthogonal bloc resolved from model references in code (anthropic.claude-…, qwen2.5-72b, deepseek/deepseek-r1, Qwen/…, version-pinned claude-3-5-haiku@20241022) or the provider's first-party default. Local LLM usage resolves too: GGUF/MLX redistributor repos (TheBloke/…, mlx-community/…), .gguf file paths, and Ollama tags (llama3.2, qwen2.5). Opt-in provenance policy block, same shape as sovereignty, plus a deny_models family ban with provider-deny semantics; findings name the developer organisation when the map knows it.
  • Policy: classification-keyed JSON eval-set, deny-by-default, provider allow/deny, configurable failure set, declared home regime — scaffolded interactively by borderlint init (or non-interactively with --home/--classes for CI).
  • Regimes & arrangements: declared home location → data-protection regime tag + the cross-border mechanism reference for a flagged flow (context only, never adjudicated). GBA seats hk/mo/CN-GBA → PDPO / Macao PDPA / PIPL + the matching GBA Standard Contract; APAC/EMEA seats jp (APPI), kr (PIPA), sg/my (PDPA s.26 / s.129), au (APP 8), uk (UK IDTA), eu (GDPR) → their transfer mechanism. PIPL cross-border and GDPR are also surfaced for those destinations.
  • Output & CI: text / JSON / Mermaid / SARIF / SBOM / evidence / HTML / badge, an SBOM diff gate for new egress, inline waivers, exit codes, GitHub Action + Jenkins.
  • Agentic coding: an installable Claude Code plugin (this repo is its own marketplace — /plugin marketplace add iolairus/borderlint) and Cursor rules that make the agent scan before adding an AI dependency, endpoint, or model id.

Scope

For HK / CN / GBA / MO plus JP / KR / SG / AU / UK / EU / MY home bases (regime tags + cross-border references). Not yet: AE / IN / ID (cross-border instruments not yet operational); other jurisdictions; CycloneDX / SPDX SBOM export and optional LLM enrichment. Per-capability status — shipped vs. next vs. later — is tracked in CAPABILITIES.md.

Keeping the KB fresh

Browse the KB at iolairus.github.io/borderlint — one page per provider and per model developer (residency, sovereignty, provenance, regime and cross-border references), generated straight from the bundled JSON on every KB change.

A weekly GitHub Action (.github/workflows/kb-refresh.yml) checks freshness on every axis: providers we don't yet cover (diffed against litellm's registry), model families the provenance map doesn't resolve (aggregated, so the issue lists families to curate rather than thousands of model IDs), sovereignty-map completeness, and each bundled KB's last-reviewed date. It maintains a single standing review issue, updated in place. Route aliases and out-of-scope names are recorded in scripts/kb_drift_aliases.json; jurisdictions and blocs are assigned by hand, never auto-merged. borderlint --version shows the KB's last-reviewed date. To add or correct a provider, see CONTRIBUTING.md (KB schema + PR workflow).

Development

borderlint is built spec-first with OpenSpec: every change is a reviewed proposal (specs + design + tasks) gated by a spec-reviewer agent before any code is written. To bootstrap the same workflow into another repo:

scripts/opsx-init.sh [--no-jira] /path/to/your/repo

It scaffolds AGENTS.md, .claude/ (slash commands + the spec-reviewer gate), an empty openspec/, and workflow.yaml. --no-jira trims it to the core loop — propose → review → apply → commit → ship.

AI assistance

The AI coding agents used to build borderlint, scored on borderlint's own three axes (residency / sovereignty / provenance):

Model Route Residency Sovereignty Provenance
Claude Opus 4.8 Anthropic, first-party us us us (Anthropic)
Claude Fable 5 Anthropic, first-party us us us (Anthropic)
Kimi K3 Moonshot, first-party cn cn cn (Moonshot)
Qwen 3.6 27B local, self-hosted hk - My desk, Hong Kong, China local cn (Alibaba)
GLM 5.2 OpenRouter → z.ai cn cn (+ us exposure at the router hop) cn (Zhipu)
Hunyuan 3 OpenRouter → Novita AI unknown us (Novita, San Mateo; + router hop) cn (Tencent)
Muse-Glimmer 30B local, self-hosted hk - My desk, Hong Kong, China local us (Meta)

License

MIT © 2026 Iolaire McKinnon. Vendor-neutral by design.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

borderlint-1.14.1.tar.gz (470.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

borderlint-1.14.1-py3-none-any.whl (60.4 kB view details)

Uploaded Python 3

File details

Details for the file borderlint-1.14.1.tar.gz.

File metadata

  • Download URL: borderlint-1.14.1.tar.gz
  • Upload date:
  • Size: 470.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for borderlint-1.14.1.tar.gz
Algorithm Hash digest
SHA256 4643db388ac7ae25c77ae9086073126fb0df6539eae9d7b078830628fc53a531
MD5 4d19021f549d1ec9bf220042e2b1480b
BLAKE2b-256 57e196beb94bbaf93a2d1ef709ed366ae80b45a655a109a9477bb3d0852c1d4f

See more details on using hashes here.

Provenance

The following attestation bundles were made for borderlint-1.14.1.tar.gz:

Publisher: release.yml on iolairus/borderlint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file borderlint-1.14.1-py3-none-any.whl.

File metadata

  • Download URL: borderlint-1.14.1-py3-none-any.whl
  • Upload date:
  • Size: 60.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for borderlint-1.14.1-py3-none-any.whl
Algorithm Hash digest
SHA256 4fa97c528c3dd913968492a1e3b5b7b3fdcb09bcffa030714d918dc062a02f6f
MD5 869a5178b720581e96edd297a2f503cb
BLAKE2b-256 b2820b7de2747753c09a05ba228a34df0c07bd5a79e3b246e732c5fe2201b8b7

See more details on using hashes here.

Provenance

The following attestation bundles were made for borderlint-1.14.1-py3-none-any.whl:

Publisher: release.yml on iolairus/borderlint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

1.14.1 This release

2 files

1.14.0

2 files

1.13.0

2 files

1.12.0

2 files

1.11.0

2 files

1.10.0

2 files

1.9.0

2 files

1.8.0

2 files

1.7.0

2 files

1.6.0

2 files

1.5.0

2 files

1.4.0

2 files

1.3.2

2 files

1.3.1

2 files

1.3.0

2 files

1.2.1

2 files

1.2.0

2 files

1.1.4

2 files

1.1.3

2 files

1.1.2

2 files

1.1.1

2 files

1.1.0

2 files

1.0.1

2 files

1.0.0

2 files

0.12.1

2 files

0.12.0

2 files

0.11.0

2 files

0.10.1

2 files

0.10.0

2 files

0.9.3

2 files

0.9.2

2 files

0.9.1

2 files

0.9.0

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page