Claude Codeでuvプロジェクトを開発する — uv.lockとpip直叩き禁止の設定
uvプロジェクトではuv runとuv addの経路に作業を集めると、依存の記録が崩れません。CLAUDE.mdの検証コマンドと、pip直叩きを止めるpermissionsの書き方を示します。
uvのプロジェクトをClaude Codeで開発するときは、依存の追加と実行を uv add と uv run に集めます。pip install を直接叩かせると、環境には入るのに pyproject.toml と uv.lock に残らない依存が生まれるからです。
この記事では、CLAUDE.md に書く検証コマンド、pip 系を止める permissions の設定、依存を足すときの流れを順に示します。設定例はコピーして使える形にしてあります。
uvプロジェクトでClaude Codeに守らせる3つのファイル
uvのプロジェクトは、uv init で作った時点で pyproject.toml と .python-version を持ちます。uv run、uv sync、uv lock のどれかを初めて実行すると、ルートに .venv と uv.lock が加わります。
Claude Codeに任せる作業を決めるには、この3つの役割の違いを押さえるのが近道です。
Claude Codeが触るファイルと触り方
pyproject.toml
依存の大まかな要件を書く場所です。手で編集することも、
uv addとuv removeで書き換えることもできます。Claude Codeには後者を使わせます。uv.lock
解決済みの正確なバージョンを持つ、プラットフォーム共通のロックファイルです。バージョン管理に含める対象で、uvが管理するため手で編集しません。
.venv
依存が入る仮想環境です。バージョン管理には含めません。uvが内部の
.gitignoreで自動的にgit管理から外します。
.python-version は、.venv を作るときに使うPythonのバージョンをuvに伝えるファイルです。Pythonのバージョンを変える依頼が来たときも、.venv を直接いじらせず、このファイルと pyproject.toml の記述を入口にします。
uv.lock を手で直させない点は、CLAUDE.md にも書いておく価値があります。ロックファイルは人間が読めるTOMLですが、書き換えの窓口は uv add や uv lock に限られます。
CLAUDE.mdに書く検証コマンド
Claude Codeは、CLAUDE.md に書かれたコマンドを作業の完了判定に使います。uvプロジェクトでは、コマンドの頭を uv run で揃えます。
## 開発コマンド(uvプロジェクト)
- 実行と検証は必ず `uv run` 経由で行う。環境を有効化して素の `python` を呼ばない
- テスト: `uv run --locked pytest`
- 依存の追加: `uv add <パッケージ>`(開発用は `uv add --dev <パッケージ>`)
- 依存の削除: `uv remove <パッケージ>`
- `pip install` / `uv pip install` は使わない。依存を変えるときは上の `uv add` を使う
- `uv.lock` は手で編集しない。変更が必要なら `uv lock` を実行する
- 作業完了の条件: `uv lock --check` と `uv run --locked pytest` が通ることここで効いているのが --locked です。uvの uv run は、既定ではロックと環境を実行のたびに同期します。--locked を付けると、ロックファイルが pyproject.toml と食い違っているときにエラーで止まります。
uv lock --check も同じ判定を、実行を伴わずに行う書き方です。--locked と等価だと説明されています。
uv lock --check
uv run --locked pytestClaude Codeがテストに失敗すれば、出力を読んで修正し、同じコマンドをもう一度回します。この反復の終了条件が「2行とも成功」になるので、pyproject.toml だけ書き換えてロックを更新し忘れた状態は完了にならない構成です。
テストの書き方そのものは、pytestの規約をCLAUDE.mdに教える方法で扱っています。ここでは実行の入口を uv run に固定する話に絞ります。
--locked、--frozen、--no-syncの使い分け
3つのオプションは似て見えますが、検査する範囲が違います。
| オプション | 動き | 向く場面 |
|---|---|---|
--locked | 動きロックが古ければエラー。自動更新しない | 向く場面検証コマンド(完了判定) |
--frozen | 動きロックが最新か確認せず、そのまま使う | 向く場面ロックの鮮度を問わず再現したいとき |
--no-sync | 動き環境が最新かの確認を省く | 向く場面環境を触りたくない軽い実行 |
完了判定には --locked が合います。--frozen はロックの更新漏れを見逃すため、「検証に通った」という信号が弱くなります。
pip直叩きをpermissionsで止める
CLAUDE.md の記述は指示であって、強制ではありません。確実に止めたいコマンドは、.claude/settings.json の permissions に書きます。
{
"permissions": {
"allow": [
"Bash(uv run *)",
"Bash(uv sync *)",
"Bash(uv lock *)"
],
"ask": [
"Bash(uv add *)",
"Bash(uv remove *)"
],
"deny": [
"Bash(pip install *)",
"Bash(pip3 install *)",
"Bash(python -m pip *)",
"Bash(python3 -m pip *)",
"Bash(uv pip *)",
"Bash(uv run pip *)",
"Bash(uv run python -m pip *)"
]
}
}設計の意図は3点です。
- 実行と検証は確認なしで通す(
allow) - 依存の増減は確認を挟む(
ask)。パッケージを足す操作は、人間が名前を見る価値があります - pip系は塞ぐ(
deny)
deny に Bash(pip install *) を置くと、pip install requests のような呼び出しが止まります。末尾の * の前にスペースを入れる書き方は、pip install で始まるコマンドに一致させる形です。
uv runの中に隠れたpipに注意
一見すると deny の最初の4行で足りそうですが、uv run 経由で抜ける道が残ります。
Claude Codeは、ルールを照合する前に timeout、time、nice、nohup、stdbuf などの決まったラッパーを剥がします。この一覧は組み込みで、変更できません。direnv exec や npx のような環境ランナーは一覧に入っておらず、uv run も同じ扱いです。
つまり pip install の deny ルールは、uv run pip install requests には一致しません。しかも allow に Bash(uv run *) を置いているので、そのままだと許可側に一致します。
対策が、deny の末尾の3行です。
Bash(uv pip *)— uvが提供するpipインターフェースです。仮想環境を直接操作する口なので、uv addの経路から外れますBash(uv run pip *)—uv runを介したpipBash(uv run python -m pip *)—uv runを介したpython -m pip
deny は allow より優先されます。広い deny ルールがあれば、より狭い allow ルールでは例外を作れません。allow で uv run * を開けつつ、deny で uv run pip * を塞ぐ組み合わせが成立するのはこのためです。
同じ考え方は、Denoのdeno taskと--allowの二重の権限設計や、OpenTofuでvalidateとplanだけ許可する設計でも使えます。ラッパーに包まれた呼び出しを個別に塞ぐ、という点が共通です。
落とし穴
uv run --with は allow の範囲に入る
Bash(uv run *) を許可すると、uv run --with <パッケージ> ... も通ります。--with は、その実行に限ってパッケージを追加で要求するオプションです。プロジェクトの要件とは無関係に、指定したバージョンが使われます。依存を増やす経路になり得るので、気になる場合は Bash(uv run --with *) を ask に入れる選択肢があります。
依存を足す作業を回す手順
新しい依存が要る作業をClaude Codeに頼むとき、流れは次の形に揃えます。
依存追加の流れ
- 1
追加を実行する
uv add <パッケージ>を実行します。pyproject.tomlの追記、ロックファイルの更新、環境への反映が一度に行われます。 - 2
開発用かを判断する
テストやリンターなど開発時だけ使うものは
uv add --devです。devグループとして[dependency-groups]に記録され、既定で同期されます。 - 3
ロックの整合を確認する
uv lock --checkで、ロックがpyproject.tomlと一致していることを確かめます。 - 4
検証コマンドを回す
uv run --locked pytestを実行し、通ることを確認します。
uv add httpx
uv add --dev pytest
uv lock --check
uv run --locked pytestrequirements.txt から移行するプロジェクトでは、uv add -r requirements.txt で一括取り込みができます。取り込み後は requirements.txt を残さず、依存の記録先を pyproject.toml に一本化すると、Claude Codeが2か所を見比べずに済みます。
特定パッケージだけを新しくしたいときは、uv lock --upgrade-package <パッケージ> です。他のロック済みバージョンはそのまま保たれます。新しいバージョンが出ただけでは、uvはロックを古いとは見なしません。更新は明示的な操作です。
pip直叩きが残すもの — テストだけ通ってしまう罠
pip直叩きを止める理由は、見た目のきれいさだけではありません。実害があるのは、次の流れです。
- Claude Codeが
pip install fooを実行し、環境にfooが入る fooをimportするコードを書き、uv run pytestが通るpyproject.tomlにfooは無く、uv.lockにも載らない- 別のマシンやCIでは
fooが入らず、ImportErrorで落ちる
手順2が通ってしまうのは、uv run が既定で「余分なパッケージを削除しない」同期をするためです。必要なパッケージが入っていることは保証しますが、ロックにないパッケージが残っていても気にしません。
一方 uv sync は既定で「厳密な」同期で、ロックにないパッケージを削除します。uv run でも --exact を付ければ同じ挙動になります。
完了判定の厳しさを変える
既定のまま
uv run --locked pytest。ロックの整合は見るが、環境に紛れ込んだ余分なパッケージは放置される。
厳密同期にする
uv run --locked --exact pytest。環境がロックと一致しない状態では、余分な依存に頼ったテストが通らなくなる。
CLAUDE.md の完了条件を uv run --locked --exact pytest にしておけば、deny をすり抜けた場合の2段目の網になります。
uvプロジェクトでよくあるつまずき
素のpythonで実行してModuleNotFoundErrorが出る
.venv は現在のシェルから分離されています。プロジェクトを import する python -c "import example" のような呼び出しは、素のままでは失敗します。uv run python -c "import example" と書けば動きます。
Claude Codeがこの失敗を見て pip install で解決しようとすることがあります。CLAUDE.md に「実行は uv run 経由」と書いたうえで、deny で塞いでおくと、そこで止まって正しい経路に戻ります。
仮想環境をactivateして作業させる
source .venv/bin/activate で環境を有効化すれば、uv run なしでもコマンドは動きます。ただし有効化の方法はシェルとOSで異なります。uv run なら環境を毎回コマンド側で指定できるので、シェルの状態に左右されません。
実行のたびにロックが書き換わる
--locked を付けない uv run は、ロックが古ければ更新してから実行します。意図しない差分が uv.lock に出る原因になります。検証コマンドには --locked を入れておくと、更新が必要なときはエラーで知らせてくれます。
フレームワークごとの検証は別の記事に
Webフレームワークの検証コマンドは、この記事の uv run に差し替えて使えます。たとえばFlaskなら、test_clientのpytestで完了を判定する手順の pytest を uv run --locked pytest に置き換える形です。CeleryのタスクをuvプロジェクトでテストするならClaude Codeにモックで検証させる方法が参考になります。
まとめ
uvプロジェクトで守るのは、依存の入口を uv add、実行の入口を uv run に固定することです。CLAUDE.md に uv lock --check と uv run --locked pytest を完了条件として書き、permissions でpip系を塞ぎます。
見落としやすいのは、uv run を allow に入れるだけでは uv run pip install が通ってしまう点です。deny に uv run pip * を足しておけば、許可と禁止が噛み合います。最後の確認に --exact を足すと、宣言漏れの依存が残った環境でのテスト成功も防げます。