結論
-
pip installしたパッケージに.pthファイルが入っていると、その中のimportで始まる行は、パッケージを一度も import しなくても Python を起動するたびに実行されます(Python公式siteドキュメント・筆者の環境で確認)。python -c passでも、pip --versionでも動きました。 - 実行されるコードは、そのプロセスの環境変数(
OPENAI_API_KEYなど)をそのまま読めます。ダミーのDUMMY_API_KEYを置いた状態で、.pthから_KEYで終わる変数の数を数えられることを確かめました。 - 確かめ方は2つ。入れる前は wheel の中に
.pthがあるかをpython -m zipfile -lで見る。入れた後は site-packages の.pthからimport行を探す(スクリプトは下にあります)。setuptoolsのように正規のパッケージもimport行入りの.pthを持つので、見つかったら中身で判断します。
この記事は「AIエージェントと秘密情報」の検証シリーズです。新しい検証は X(@supportapp3_lab)でもお知らせしています。
X で「litellm の特定のバージョンを pip install した場合、マルウェア感染の可能性がある」という投稿が広く読まれていました。この件の中身はこの記事では扱いません。ここでは、一般論として「pip で入れただけのパッケージが、いつコードを動かせるのか」を、ダミーのパッケージで確かめます。AIエージェントに pip install を任せている人ほど、知っておくと点検しやすくなります。
検証環境と範囲
- Debian 13.7 / Python 3.13.5 / pip 25.1.1
-
/tmpの一時ディレクトリに venv を作り、.pthを1つだけ含む wheel を手作りして入れました。.pthの中身は「起動したらマーカーファイルに1行書く」だけです。 - キーはダミーの文字列で、値は表示も保存もしていません。Windows・macOS・conda、Python 3.15 以降は試していません。
使った .pth(1行):
import os; open(os.environ.get('MARK','/dev/null'),'a').write('pth ran: pid=%d keys_seen=%d\n' % (os.getpid(), sum(1 for k in os.environ if k.endswith('_KEY'))))
1. いつ動いたか
| 操作 |
.pth のコードが動いたか |
|---|---|
pip install ./demo_pth-0.1-py3-none-any.whl(wheel を入れる処理そのもの) |
動かなかった |
python -c pass(何も import しない) |
動いた |
pip --version(pip も Python なので) |
動いた |
python -I -c pass(隔離モード) |
動いた(venv の site-packages は対象のまま) |
python -S -c pass(site を読み込まない) |
動かなかった |
pip uninstall の後に python -c pass
|
動かなかった(.pth も消えた) |
マーカーファイルに書かれた行:
pth ran: pid=13777 keys_seen=1
keys_seen=1 は、ダミーの DUMMY_API_KEY を数えられたということです。数える代わりに値を読むことも、同じ1行で書けます。
Python公式の site ドキュメントには、.pth の行のうち import(とスペースかタブ)で始まるものは exec() で実行され、使うかどうかに関係なく Python の起動時に必ず動く(-S で site 自体を無効にした場合を除く)、と書かれています(site — Site-specific configuration hook)。
なお、この環境(Debian の python3.13 で作った venv)では、1回の起動でマーカーが2行ずつ増えました。python -v で見ると site-packages の処理が2回行われていましたが、理由は調べきれていません。システムの python3 とユーザーの site-packages の組み合わせでは1回でした。
2. wheel を入れる処理は動かないが、安心材料にはならない
表のとおり、wheel を入れる処理の間は .pth は動きませんでした。pip はすでに起動していて、.pth はその後に置かれたからです(起動時に読まれるという公式の説明からの解釈です)。ただし、次に Python が起動した瞬間に動くので、実質的な違いはほとんどありません。
wheel ではなくソースパッケージ(sdist)しかない場合は、入れるときにビルドのためのコードが動きます。pip の --only-binary :all: はソースパッケージを使わない指定です(pip install)。今回は sdist の動きは試していません。
3. 入れる前に、wheel の中に .pth があるか見る
pip download で wheel だけを取ってきて、中のファイル名を見ます。
pip download --no-deps --only-binary :all: パッケージ名==バージョン -d /tmp/dl
python -m zipfile -l /tmp/dl/*.whl | grep -E '\.pth|\.start|customize\.py'
手作りの wheel では次のように出ました。
demo_pth_hook.pth (日時・サイズの列は省略)
何も出なければ、その wheel は .pth を持っていません。依存パッケージも見たいときは --no-deps を外します。
4. 入れた後に、起動時に動くものを一覧にする
今使っている Python(venv なら有効にした状態で)で実行します。読み取りだけで、中身は先頭80文字までしか出しません。
#!/usr/bin/env python3
"""読み取り専用: いま使う Python が起動時に実行するもの(.pth の import 行、.start、sitecustomize/usercustomize)を一覧にする。
ファイルの中身は先頭80文字だけ表示(鍵が書かれていても全文は出さない)。"""
import site, sys, os, glob
dirs = list(dict.fromkeys(site.getsitepackages() + [site.getusersitepackages()]))
print("python:", sys.executable)
found = 0
for d in dirs:
if not os.path.isdir(d): continue
for p in sorted(glob.glob(os.path.join(d, "*.pth"))):
with open(p, encoding="utf-8", errors="replace") as f:
for i, line in enumerate(f, 1):
if line.startswith(("import ", "import\t")):
found += 1; print(f"[pth-import] {p}:{i}: {line.strip()[:80]}")
for p in sorted(glob.glob(os.path.join(d, "*.start"))):
found += 1; print(f"[start] {p}")
for n in ("sitecustomize.py", "usercustomize.py"):
p = os.path.join(d, n)
if os.path.exists(p): found += 1; print(f"[customize] {p}")
print(f"起動時に動くもの: {found} 件(見る場所 {len(dirs)} か所)")
ダミーのパッケージを入れた venv での出力(パスは一部省略):
python: /tmp/pthrepro.XXXX/v/bin/python
[pth-import] /tmp/pthrepro.XXXX/v/lib/python3.13/site-packages/demo_pth_hook.pth:1: import os; open(os.environ.get('MARK','/dev/null'),'a').write('pth ran: pid=%d k
起動時に動くもの: 1 件(見る場所 5 か所)
.start と sitecustomize.py・usercustomize.py も対象にしているのは、どれも起動時に実行されるからです(.start は Python 3.15 で追加された仕組みで、3.13 の今回の環境にはありません。公式ドキュメントによる)。
正規のパッケージでも出ます。 たとえば setuptools 84.2.1 の wheel には distutils-precedence.pth が入っていて、import os; var = 'SETUPTOOLS_USE_DISTUTILS'; ... で始まる行がありました。見つかったら「どのパッケージのファイルか」「中で何をしているか」を見て判断します。知らないパッケージの .pth が、ネットワークやファイルを扱うコードを読み込んでいたら要注意です。このスクリプトは怪しいかどうかまでは判定しません。
5. 見つかってしまったら
-
pip uninstallで.pthごと消えました(今回の wheel では RECORD に載っていたため)。ただし、すでに動いたコードが何をしたかは消えません。 - その Python を起動した環境にあった環境変数のキー、
~/.sshなどのファイルは、読まれた前提で再発行・作り直しを考えます(動いたコードの内容が分からない場合の一般的な考え方で、何が読まれるかはコード次第です)。 - AIエージェントに
pip installを任せる場合は、キーを入れたシェルとは別の環境(使い捨ての venv やコンテナ)で動かすと、読まれる範囲を小さくできます(推測を含む一般論で、今回は試していません)。
まとめ
-
.pthのimport行は、import しなくても Python の起動のたびに動く(実機・Python公式)。pip --versionでも動いた。 - そのコードは環境変数のキーを読める(ダミーで実機確認)。
- 入れる前は
pip download+python -m zipfile -lで.pthを探す。入れた後はcheck_pth.pyで一覧にする。 - 正規のパッケージ(setuptools など)も
.pthを持つので、中身を見て判断する。
AIエージェントの手元で .env やシェル履歴が読める状態かどうかは、ai-secret-check(npx github:supportapp3-lab/ai-secret-check scan、値は表示せず通信もしません)で点検できます。.pth はまだ点検対象ではありません。
関連記事:
- APIキーを export するとシェル履歴に平文で残る
- .npmignoreを置くと.gitignoreは使われない:.envと鍵がnpmパッケージに入る
- Claude Codeに.envを読ませない設定
参考(公式):
※ 仕様は 2026-10-24 時点の公式ドキュメントに基づきます。検証は Python 3.13.5 / pip 25.1.1 で、Linux の使い捨て環境でのみ行いました。
更新情報やちょっとしたTipsは X(@supportapp3_lab)で発信しています。