Install a stable, codesigned macOS TCC launcher (python-tcc) into a uv/venv
Project description
tcc-venv
Give a uv/venv Python app a stable, codesigned macOS TCC identity — so Full Disk
Access / Automation grants survive uv sync and Python upgrades, and the permission
dialog shows a recognizable per-project name instead of python3.12.
macOS only for the privacy benefit. On Linux/Windows it degrades to a no-op passthrough so the same launcher name works everywhere.
The problem
macOS TCC (the privacy system behind Full Disk Access, Automation, Calendar,
etc.) grants permissions to a specific binary identity — its path plus its
code-signing identity. A Python interpreter created by uv lives at a churning,
version-pinned path:
~/.local/share/uv/python/cpython-3.12.7-macos-aarch64-none/bin/python3.12
Grant that binary Full Disk Access, then run uv python upgrade or let uv sync
pull a new patch release, and the path changes. To TCC it's now a different app:
the grant silently stops applying and your tool starts getting "Operation not
permitted" until you re-grant it by hand. The dialog also just says "python3.12",
giving the user no idea which project is asking.
tcc-venv fixes both by interposing a tiny signed C launcher with a stable
identity that you grant once:
<venv>/bin/python-tcc-<project> # signed trampoline — the stable TCC identity
<venv>/bin/python-tcc # -> python-tcc-<project> (uniform name for shebangs/control)
The launcher self-locates its venv, spawns <venv>/bin/python with your arguments,
forwards signals, and propagates the exit code — staying the parent process so TCC
attributes everything to it. Re-running wrap after uv sync restores the
identical signed bytes from a local cache, so the cdhash — and the grant — is
unchanged.
What it is not
It does not bypass or weaken TCC. You still grant access explicitly in System Settings; this only stops the identity from moving out from under that grant. It relies on undocumented TCC responsible-process inheritance and ad-hoc cdhash determinism, both of which can change across macOS releases — treat it as unofficial and verify on the macOS versions you ship to.
Requirements
- macOS with the Xcode Command Line Tools (
xcode-select --install) — the trampoline is compiled withccclient-side atwraptime. - Python ≥ 3.11.
- A venv with a
pyvenv.cfg(uv,python -m venv, pdm-in-venv mode — all fine).
Install
uvx tcc-venv wrap # run without installing
# or
pipx install tcc-venv
# or
uv tool install tcc-venv
Usage
Run wrap once per venv, then point your launchers / shebangs at python-tcc.
With uv
cd my-project
uv sync # creates ./.venv
uvx tcc-venv wrap # defaults to ./.venv
Because uv sync recreates the interpreter, re-run wrap after a sync — it restores
the same identity from cache, so you do not re-grant:
uv sync && uvx tcc-venv wrap
With pdm
pdm can manage an in-project venv. Enable it, install, then wrap:
pdm config python.use_venv true
pdm install # creates ./.venv
uvx tcc-venv wrap .venv
With a bare venv
python3 -m venv .venv
.venv/bin/pip install -e .
uvx tcc-venv wrap .venv
Tip: python -m venv --prompt myapp .venv sets the friendly name used in the
identity and the TCC dialog (otherwise the venv's parent directory name is used).
Grant the permission (once)
After wrap, the binary path is printed. Add that binary to the relevant pane:
System Settings → Privacy & Security → Full Disk Access → + → select
<venv>/bin/python-tcc-<project>
Automation / Calendar / Reminders prompts appear on first use. From then on, run your app through the shim:
.venv/bin/python-tcc my_app.py
# or in a shebang: #!/path/to/.venv/bin/python-tcc
Commands
| Command | What it does |
|---|---|
tcc-venv wrap [venv ...] |
Install/refresh the launcher (idempotent). Defaults to ./.venv. Accepts multiple venvs. |
tcc-venv wrap --rebuild |
Force a fresh build + sign (new cdhash — you'll need to re-grant FDA). |
tcc-venv wrap --identifier-prefix PREFIX |
Override the codesign identifier prefix (default local.tcc-venv). |
tcc-venv status [venv] |
Show the installed shim, target, cdhash, and expected identifier. |
The identifier is <prefix>.<project>.<hash8>, where <prefix> defaults to
local.tcc-venv (override with --identifier-prefix or $TCC_VENV_IDENTIFIER_PREFIX)
and <hash8> is derived from the venv's real path so two projects with the same name
never share a grant.
How it works (short version)
- Compile
trampoline.conce per architecture (cached, unsigned). - Copy it to
<venv>/bin/python-tcc-<project>and ad-hoc codesign it with a stable--identifier. - Cache the signed bytes by identifier; on re-wrap, copy them back so the cdhash is byte-identical regardless of codesign version drift.
- Symlink
python-tcc -> python-tcc-<project>.
At runtime the trampoline resolves its own path → venv, refuses to run if a stray
$VIRTUAL_ENV disagrees, posix_spawns the venv's python, forwards signals, and
returns the child's exit status (128 + signo on signal death). On non-macOS it's a
plain exec of the venv python with no signing.
See AGENTS.md for the full design and invariants.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file tcc_venv-0.1.0.tar.gz.
File metadata
- Download URL: tcc_venv-0.1.0.tar.gz
- Upload date:
- Size: 15.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
994e098ae64b00bad192b5768475e0b957b3b3fc6e5aee60deac483ad10581f5
|
|
| MD5 |
bd2406bada54f606f75d3c5f44ae26be
|
|
| BLAKE2b-256 |
35f542a849061050c319b6d3ba03580d24f99cbf197e3e5fca3f58237abe4f2f
|
Provenance
The following attestation bundles were made for tcc_venv-0.1.0.tar.gz:
Publisher:
publish.yml on mo22/tcc-venv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tcc_venv-0.1.0.tar.gz -
Subject digest:
994e098ae64b00bad192b5768475e0b957b3b3fc6e5aee60deac483ad10581f5 - Sigstore transparency entry: 1733049372
- Sigstore integration time:
-
Permalink:
mo22/tcc-venv@b44c72cb1f64f88b8ff7d441cc2335b56f692ef1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mo22
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b44c72cb1f64f88b8ff7d441cc2335b56f692ef1 -
Trigger Event:
release
-
Statement type:
File details
Details for the file tcc_venv-0.1.0-py3-none-any.whl.
File metadata
- Download URL: tcc_venv-0.1.0-py3-none-any.whl
- Upload date:
- Size: 12.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
cf8d5c3db0c25420573e6ad60d9d45dd795778970c56b04062a4e016de42e161
|
|
| MD5 |
349a7023b22e873d1e276e0b68502cdd
|
|
| BLAKE2b-256 |
97fecab053d7285a301d1262de704654aca562fee86e2ab735136ecb6e68b974
|
Provenance
The following attestation bundles were made for tcc_venv-0.1.0-py3-none-any.whl:
Publisher:
publish.yml on mo22/tcc-venv
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
tcc_venv-0.1.0-py3-none-any.whl -
Subject digest:
cf8d5c3db0c25420573e6ad60d9d45dd795778970c56b04062a4e016de42e161 - Sigstore transparency entry: 1733049420
- Sigstore integration time:
-
Permalink:
mo22/tcc-venv@b44c72cb1f64f88b8ff7d441cc2335b56f692ef1 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/mo22
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b44c72cb1f64f88b8ff7d441cc2335b56f692ef1 -
Trigger Event:
release
-
Statement type: