Dependencies and AI-written code can open connections you never meant them to. tethered checks each one made through Python's socket layer against your allow list, before it opens. One function call, zero dependencies, no proxy.
This is the 2nd launch from tethered. View more

tethered 1.0
Launched this week
List the servers your app may talk to, in one line of Python. Any other connection Python makes is refused before it opens, whether it comes from your own code, a dependency or code an AI wrote. Free and open source, with no proxy to run.







Free
Launch Team / Built With

CloudflareStill paying to deploy your apps? Try Cloudflare Workers for free
Promoted





Hi Product Hunt! I'm Sergii, and I build tethered. It was here in March as version 0.1. This is 1.0, and most of what follows is new since then.
Your code, your dependencies and code an AI assistant wrote all run in one Python process, and any of them can open a connection to a host you never meant it to reach: a logging library that phones home, a hijacked release that sends your environment variables somewhere, a script an agent wrote that calls an API nobody expected. Firewalls, network policies and egress proxies can stop that, but they take a platform team, a separate service or admin rights.
tethered takes one line of Python, for example:
From then on, every connection made through Python's socket layer is checked against that list before it opens, and anything else raises tethered.EgressBlocked. For the libraries that resolve through Python (requests, httpx, urllib3, aiohttp), the lookup of a name that is not on the list is refused before it is sent. It works from inside the process, through Python's audit hooks (PEP 578): no proxy, no sidecar, no monkey-patching, no runtime dependencies.
What it does
Narrows one code path. tethered.scope(allow=[...]), as a decorator or a with block, holds a request handler, a job or a library call to the hosts it needs. A scope only narrows the process policy, never widens it.
Follows your workers. multiprocessing and ProcessPoolExecutor workers, gunicorn and uvicorn workers and subprocess.run([sys.executable, ...]) start with the policy in place. Other programs are logged, or refused with external_subprocess_policy="block".
Rolls out without surprises. uv run python -m tethered check lists what it would refuse or not see in your environment, and changes nothing. Then enforce it in your test suite, watch production with log_only=True, enforce, and lock.
Locks. With locked=True, replacing or switching off the policy takes a token, and monkey-patching tethered or swapping its policy is detected and fails closed.
Measured. uvicorn, gunicorn, Django, Celery and 19 more servers, frameworks, task queues and pools, and 70 client-library arrangements, each run in containers without a network, with every connection counted where it arrives.
What it isn't
tethered is not a sandbox. It sees what goes through Python's socket module. Code that goes below it (ctypes, a C extension making its own system calls) and libraries that connect in their own C code (psycopg2, pycurl, grpcio) are not checked, and the compatibility page lists what was measured, those included. Under asyncio it needs the standard event loop: next to uvloop it refuses to run rather than run unenforced. Where you need a guarantee, combine it with a control below Python, such as an egress proxy or a network policy.
It's free and open source (MIT):
I'd love your feedback. Where would an allow list have caught something for you? And which server or client library should the compatibility suites run next?
GitHub: https://github.com/shcherbak-ai/tethered
Docs: https://tethered.shcherbak.ai
The first thing I'd lock is the small Python scripts our agents wrote and run every evening, they should only ever reach our CRM and a couple of feeds. Would you build that list from a week of log_only and not from check? A script that runs once a day doesn't show much in a single run