Claude CodeでPoetryプロジェクトを動かす — poetry run pytestとlockの戻し方
Poetryのプロジェクトでpoetry run pytestを完了条件にし、依存の編集をClaude Codeに任せるときのCLAUDE.md・権限・Stop hookと、poetry.lockがずれたときの戻し方を示します。
PoetryのプロジェクトでClaude Codeに作業させるなら、検証は poetry run pytest に固定し、依存の追加は poetry add で行わせます。pyproject.toml を手で書き換えさせると poetry.lock とずれやすく、ずれたときの戻し方もあらかじめ決めておく必要があります。
この記事では、CLAUDE.mdへの書き方、許可ルール、Stop hookによる強制、そしてlockの不整合から戻す手順を順に組みます。
なぜpoetry runを完了条件にするのか
Poetryは既定で、プロジェクト専用の仮想環境を キャッシュディレクトリ配下の virtualenvs に作ります。virtualenvs.in-project を有効にすれば、仮想環境をプロジェクト直下に置くこともできます。どちらの場合も、環境は自動で有効化されません。
Claude Codeが実行するシェルコマンドは、仮想環境が有効化されていない素の環境で動くのが普通です。pytest と直接打つと、仮想環境の外にあるpytestを呼ぶか、そもそも見つからずに失敗します。poetry run pytest なら、Poetryが仮想環境の中でコマンドを実行します。Poetryのドキュメントでも、pytestやblackのようなコマンドラインツールは poetry run pytest で動かすと説明されています。
pytestの呼び方を2通りで比べる
pytestを直接実行
PATHにあるpytestが使われます。プロジェクトの依存が入っていない環境だと、ImportErrorで落ちます。失敗の原因がコードなのか環境なのか、Claudeには区別がつきません。
poetry run pytestを実行
Poetryの仮想環境で動くので、lockに載った依存で検証されます。終了コードで成否が返り、Claudeが出力を読んで直す反復に乗ります。
なお、仮想環境を自分で有効化してから使う運用の場合は、poetry run が無くても同じpythonが使われます。Poetryは、外部で有効化された仮想環境を検出するとそれをそのまま使います。Claude Codeを起動したシェルの環境は引き継がれますが、セッション中に有効化しても次のコマンドには持ち越されません。コマンド側で poetry run を付ける形が確実です。
プロジェクトの前提を確認する
テスト用の依存はdevグループなどに分けておくと、本番の依存と混ざりません。Poetry 2系では、poetry add --group dev pytest のようにグループを指定して追加できます。--dev(-D)は -G dev の別名です。
poetry add --group dev pytest
poetry install
poetry run pytestpoetry install は、poetry.lock があればその中の正確なバージョンを使い、無ければ解決してlockを作ります。クローン直後に実行するのはこのコマンドです。lockに無い余分なパッケージも取り除きたいなら、poetry sync が使えます。
Poetryには、パッケージとして配布するpackageモードと、依存管理だけに使う非packageモードがあります。アプリケーションなら [tool.poetry] に package-mode = false を置く形があります。この場合、poetry install はプロジェクト自身をインストールせず、依存だけを入れます。Claudeに「importできない」と言われたら、モードとプロジェクト自身のインストール状態を疑う切り分けになります。
CLAUDE.mdに検証とlockの規約を書く
CLAUDE.mdには、検証コマンドと依存操作の約束を短く書きます。次のような形です。
## Python環境(Poetry)
- 検証は `poetry run pytest -q`。直接 `pytest` や `python` を呼ばない
- 依存の追加・削除は `poetry add` / `poetry remove` を使う。
pyproject.toml の dependencies を手で書き換えない
- pyproject.toml を編集したら、完了前に `poetry check --lock` を実行する
- 不整合の警告が出たら `poetry lock` を実行し、差分を報告する
- `poetry update` はユーザーが頼んだときだけ実行する
- 仮想環境の作成や削除はしない「手で書き換えない」の理由は、lockとの同期にあります。poetry add は pyproject.toml に書き込み、パッケージをインストールし、lockも更新します。手編集だとlockが取り残されます。
poetry update を限定しているのは、挙動が異なるからです。Poetryのドキュメントによれば、update は pyproject.toml の制約の範囲で最新バージョンを解決し、poetry.lock を更新します。依存が一斉に動くので、テストの失敗が自分のコード変更によるのか依存更新によるのか切り分けにくくなります。
pytest向けの規約の全般はCLAUDE.mdにpytestの規約を教えるにまとめています。ここではPoetry固有の分だけを足します。
許可ルールで実行できるコマンドを絞る
検証のたびに確認を求められると反復が止まります。settings.jsonの permissions.allow に、検証コマンドを登録します。ルールの書き方は、Claude Codeの設定ドキュメントにある Bash(npm run test *) と同じ形式です。
{
"permissions": {
"allow": [
"Bash(poetry run pytest *)",
"Bash(poetry check *)",
"Bash(poetry lock)"
],
"deny": [
"Bash(poetry update *)"
]
}
}poetry run pytest だけを引数なしで許可したい場合は、* の無い形も並べます。poetry lock は依存の解決結果を書き換えます。poetry check は検証だけなので、許可に入れても状態は変わりません。lock を許可に入れるかどうかは、レビュー体制次第です。入れないなら確認を挟み、入れるなら下のとおりlockの差分を必ずgit diffで見ます。
複合コマンドの照合やワイルドカードの癖はBash権限と複合コマンドで扱っています。
Stop hookでpytestの合格を強制する
CLAUDE.mdは、Claudeが読んで従う指示です。守られなかった場合に止めるには、Stop hookを使います。hooksのドキュメントによれば、Stop はClaudeの応答が終わるときに発火します。終了コード2で終わると、Claudeは止まらず会話が続き、標準エラーの内容が理由として渡されます。
.claude/settings.json に追加します。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/poetry-gate.sh",
"timeout": 300
}
]
}
]
}
}スクリプトは次のとおりです。stop_hook_active を見ているのは、すでにhookの結果で続行している最中に、解消しない条件でブロックし続けないためです。ドキュメントでもこのフィールドで確認するよう書かれています。
#!/bin/bash
input=$(cat)
if [ "$(echo "$input" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! out=$(poetry check --lock 2>&1 && poetry run pytest -q 2>&1); then
echo "$out" | tail -40 >&2
exit 2
fipoetry check --lock を先に置いたのは、lockの不整合やlockの欠落を、テスト失敗より前に分かりやすく伝えるためです。jq が必要になる点に注意してください。hookの設計全般はClaude Code hooksでテストを自動実行するも参考になります。
依存編集でlockがずれたときの戻し方
ここがPoetryに固有の落とし穴です。Poetryのドキュメントには、poetry.lock と pyproject.toml が同期していないときに、installコマンドが警告を表示すると書かれています。Claudeが pyproject.toml を手で編集したり、別ブランチのlockを取り込んだりすると起こります。
lockのずれを切り分ける
- Qpoetry check --lock は通るか?答えが通らない答えがはい
poetry lockを実行します。すでにlockにあるパッケージは更新されません。lockに無い依存や、制約を変えた依存が解決の対象になります。 - Qlockの差分は意図した依存だけか?答えが関係ない依存が大量に動いている答えがはい
git checkout -- poetry.lockでlockを戻し、pyproject.tomlの編集をpoetry addでやり直させます。
poetry run pytest が通る
差分をレビューしてコミットします。
手順を順に見ます。
lockの不整合から戻す
- 1
状態を調べる
poetry check --lockを実行します。poetry checkはpyproject.tomlの内容とpoetry.lockとの整合を検証し、--lockを付けるとpoetry.lockが無い場合もエラーにします。 - 2
lockだけを作り直す
poetry lockを実行します。ドキュメントによれば、すでにlockにあるパッケージは更新されません。 - 3
差分を確認する
git diff poetry.lockで、意図した依存だけが動いたかを見ます。 - 4
最終手段で再生成する
全依存を最新の互換バージョンへ動かしたいときだけ、
poetry lock --regenerateを使います。既存のlockを無視して一から作り直すオプションです。
poetry add に --lock を付けると、インストールせずlockだけ更新します。環境をまだ触らせたくない場面で使えます。
落とし穴
poetry updateはpyprojectの制約を書き換えない
Poetryのドキュメントでは、update は pyproject.toml を変更せず、既存の制約内でlockを更新するコマンドです。^2.3 のような制約を 2.4 へ上げたいときは、poetry add で制約を指定し直します。Claudeに「最新にして」と頼んだ結果、updateが何も変えないように見えるのはこのためです。
Claudeに頼む指示の例
依存を足す作業なら、検証の流れまで含めて頼みます。
requestsを依存に追加して、APIクライアントを実装してください。
依存は poetry add で追加すること。終わる前に poetry check --lock と
poetry run pytest -q を実行し、poetry.lock の差分に無関係な依存の
更新が含まれていないか報告してください。lockの差分を報告させると、Claudeが何を動かしたかを自分で確かめる手間が減ります。
lockファイルをコミットするかどうか
Claude Codeが poetry.lock を更新した場合、その差分をコミットに含めるかが次の判断になります。Poetryのドキュメントは、アプリケーション開発者にはlockのコミットを勧めています。CIや本番、チームの全員が同じバージョンの依存で動くため、特定の環境だけで起きるバグを減らせるからです。
ライブラリ開発者は事情が違います。ライブラリを使うアプリケーション側は、ライブラリのlockを無視します。lockが古いままだと、新しい依存バージョンで壊れることに気づくのが遅れます。lockを置かない選択肢もありますが、再現性と速度を一部手放すことになります。lockを置くなら、定期的に更新するよう勧められています。
この違いは、Claudeへの指示にも効きます。ライブラリなら「lockを更新したら必ず報告する」、アプリケーションなら「lockの差分は依存追加の理由と一対一で対応させる」といった形で、レビューの観点を変えられます。
まとめ
Poetryでは、環境の固定は poetry run、lockの同期は poetry add と poetry check --lock で担保します。Claude Codeに任せる範囲は、CLAUDE.mdで文章として、許可ルールで権限として、Stop hookで強制として三重に書いておくと崩れにくくなります。それでもlockがずれたら、まず poetry lock で再解決し、差分が想定外ならgitでlockごと戻して poetry add からやり直します。
Claude Codeそのものの全体像はClaude Code完全ガイドにまとまっています。