Claude CodeでRの解析を進める — renvで依存を固定しRscriptで確かめる
Rの解析プロジェクトでClaude Codeにパッケージを入れさせるとき、renv.lockとRscriptの検証スクリプトをCLAUDE.mdとpermissionsで固定する手順です。
Rの解析プロジェクトでClaude Codeにコードを書かせると、必ず出てくる場面があります。必要なパッケージをinstall.packages()で入れ、動いたところで作業が終わる場面です。次に開いたときや別のマシンでは、パッケージのバージョンが変わって結果がずれるかもしれません。この記事では、renvで依存をrenv.lockに固定し、Rscriptで再現を確かめる流れを、Claude Codeに守らせる形で組みます。CLAUDE.mdに書く内容と、permissionsの設定例まで扱います。
renvとは何か、Claudeに任せるときに何が変わるか
renvは、Rプロジェクトごとに専用のパッケージライブラリを持たせ、使っているパッケージの正確なバージョンを記録するパッケージです。効果は3つに分けて説明されています。
- 隔離: あるプロジェクトでパッケージを更新しても、他のプロジェクトは壊れない
- 持ち運び: 別のマシンや別のOSへ、依存パッケージを入れ直しやすい
- 再現: 記録した正確なバージョンが、どこでも入る
人間が1人で使うなら、ここで話は終わります。Claude Codeが加わると、もう1つ論点が増えます。Claudeは「動くまで試す」ので、途中で何度もパッケージを入れ、更新します。どのタイミングでrenv.lockに記録するかを決めておかないと、動かないバージョンが記録されたり、逆に何も記録されなかったりします。
だから決めるのは次の3点です。
- 初期化の結果として何をコミットするか
- パッケージを足したとき、どの確認を通ってから記録するか
- 「再現できた」と言ってよい条件は何か
以降の節は、この3点を順に埋めていきます。
最初の一回: renv::init()とコミット対象
新規でも既存でも、プロジェクトのルートでrenv::init()を実行します。この関数が行うのは次のとおりです。
- 現在使っているパッケージを探して、プロジェクト専用ライブラリに入れる
- ライブラリの状態を
renv.lockに記録する .Rprofileを置いて、プロジェクトを開くたびにそのライブラリが使われるようにする
Rscript -e 'install.packages("renv")'
Rscript -e 'renv::init()'この初期化はClaudeに任せず、人間が一度だけ実行しておくほうが事故が少なくなります。パッケージの探索と導入を伴うため、時間がかかり、途中で再起動を求められる場合があるからです。renv::init()のrestart引数の既定はinteractive()で、RStudioの中で動かすと再起動します。
コミットするのは次の4つです。共同作業の前提として挙げられているファイルです。
| ファイル | 役割 |
|---|---|
renv.lock | 役割パッケージとRのバージョンの記録 |
.Rprofile | 役割起動時にプロジェクトライブラリを有効にする |
renv/settings.json | 役割renvのプロジェクト設定 |
renv/activate.R | 役割renvを自動で読み込む仕組み |
gitを使っていれば、renvが.gitignoreも作ってくれます。提案されたファイルをそのままコミットすれば足ります。共同作業者がプロジェクトを開くと、renvが自分で必要なバージョンを導入し、パッケージのrenv::restore()を実行するか尋ねます。
renv.lockはJSONで、RとPackagesの2つの部分からなります。Rには使ったRのバージョンとリポジトリ、Packagesにはパッケージごとに再インストールに必要な情報が入ります。公式の例では、CRANのパッケージはSource: Repository、GitHubのものはRemoteSha付きの記録になります。通常は手で編集しないファイルですが、gitの差分では目にします。
パッケージを足す手順をClaudeに固定する
日常の作業は、renvの標準の流れがそのまま使えます。パッケージを入れ、コードが動くことを確認し、そのあとでrenv::snapshot()を実行して記録します。順番が肝心で、確認の前に記録してはいけません。
パッケージ追加から記録までの流れ
- 1
入れる
renv::install("パッケージ名")かinstall.packages()で入れます。renvは既存のやり方を壊さない方針で、どちらも使えます。 - 2
動かす
解析スクリプトとテストを実行し、期待どおり動くかを見ます。
- 3
記録する
通ったときだけ
renv::snapshot()を実行し、renv.lockを更新します。 - 4
戻す
動かなければ
renv::restore()で、renv.lockに記録された最後の状態へ戻します。
更新も同じ型です。公式は、独立したライブラリは「バグ修正の恩恵を受けない」というリスクがあるため、活発に開発されているパッケージは少なくとも年に1回、renv::update()で最新にするよう勧めています。更新後はコードを動かして確認し、問題がなければrenv::snapshot()、行き詰まったらrenv::restore()で巻き戻します。さらに古い状態へ戻したいときはrenv::history()とrenv::revert()が使えます。
非対話では確認が出ない
ここにClaude特有の落とし穴があります。snapshot()もrestore()も、prompt引数の既定値はinteractive()です。人間がRのコンソールで実行するときは「この内容で記録しますか」と確認されますが、ClaudeがRscriptから呼ぶと対話セッションではないため、確認は出ません。実行すればそのままrenv.lockが書き換わります。
もう1つ、snapshot()の既定の種類はimplicitです。renv::dependencies()が見つけた、プロジェクトのコードが実際に使っているパッケージだけがロックファイルに入ります。Claudeが試しに書いたlibrary(xxx)の行がファイルに残っていれば、そのパッケージも記録の対象になります。試行用のコードを消し忘れると、不要な依存がrenv.lockに混ざる、ということです。
この2点から、CLAUDE.mdには「renv::snapshot()は検証が通ったあとに限る」「試行用のコードは残さない」と明記します。
再現確認を1つのスクリプトにまとめる
「再現できた」の定義を、毎回会話で伝えるのは確実ではありません。判定をスクリプトにして、Claudeには「これを通す」とだけ伝えます。見るのは2つです。ライブラリとrenv.lockが一致しているか、そしてテストが通るか。
renv::status()は、ロックファイル・ライブラリ・コードが使うパッケージの食い違いを報告します。戻り値は見えない形のリストで、synchronizedにライブラリとロックファイルが同期しているかが入ります。公式は、status()が問題を報告しない状態を目指すよう説明しています。テストはtestthat::test_dir()で実行できます。stop_on_failureの既定はTRUEで、失敗があるとエラーになります。
例えば次のようなスクリプトにします。
# scripts/check.R
# 依存の同期とテストをまとめて確かめる
status <- renv::status()
if (!isTRUE(status$synchronized)) {
stop("ライブラリとrenv.lockが一致していません")
}
testthat::test_dir("tests/testthat", stop_on_failure = TRUE)
cat("OK: 依存は同期済み、テストは成功\n")実行は1行です。
Rscript scripts/check.R成功すれば最後のOKの行が出ます。失敗したときは、stop()のメッセージかテストの失敗内容が出力に現れるので、Claudeはそれを読んで直せます。パッケージの追加後は、まずこのスクリプトを通し、その後にsnapshot()を実行するのが基本の順序です。
status()の診断は修復の指針も示します。使われていて記録されているが未インストールならrestore()、使われているが未記録ならinstall()、使われていないのに記録されていればsnapshot()で外す、という分類です。複数の問題が重なったときは、restore()、install()、snapshot()の順に実行するよう勧められています。この分類はそのままCLAUDE.mdに写せます。
scripts/check.Rはtestthat::を呼ぶので、testthatはプロジェクトが使うパッケージとしてrenv.lockに入るはずです。初回のsnapshot()のあとに、grep testthat renv.lockで入ったかを確かめると安心です。
テストの置き場所と内容は、プロジェクトごとに変わります。解析プロジェクトなら、集計関数の出力列名、欠損の扱い、主要な統計量の値を固定する小さなテストが効きます。Pythonでの同じ発想はClaude CodeにpytestのCLAUDE.md規約を教えるにあります。
CLAUDE.mdに書く内容
CLAUDE.mdは、システムプロンプトではなくその後のユーザーメッセージとして渡されます。Claudeは従おうとしますが、厳密な遵守は保証されません。公式は、検証できる粒度で書くよう勧めています。「テストを確認する」でなく、実行するコマンドを書くということです。長さは200行以内が目安です。
Rプロジェクト向けの例を示します。
## Rプロジェクトの規約
### 環境
- パッケージはrenvで管理する。`renv.lock`は手で編集しない
- パッケージ追加は`renv::install("名前")`を使う
- Rのバージョンはrenvが管理しない。`renv.lock`のR欄を参照する
### 検証(コードを変えたら必ず実行)
- `Rscript scripts/check.R` が成功するまで完了としない
- 失敗したら出力を読み、原因を直してから再実行する
### renv.lockの更新
- `renv::snapshot()`は`scripts/check.R`が成功した後にだけ実行する
- 試しに書いた`library()`や一時ファイルは残さない
- `renv::update()`を実行する前に、ユーザーへ確認する
- 更新後に検証が通らなければ`renv::restore()`で戻す
### 状態の食い違い(renv::status()の指示)
- 使われて記録済みで未導入: `renv::restore()`
- 使われて未記録: `renv::install()`
- 未使用なのに記録あり: `renv::snapshot()`ポイントは3つです。第一に、完了条件をRscript scripts/check.Rという1つのコマンドにしています。第二に、snapshot()の前提を「検証の成功後」に絞っています。第三に、update()だけは人間の判断に回しています。更新は結果が変わる可能性があるためです。
Pythonで同じ型を組んだ例は、Claude Codeでruffとmypyの検証ループを組むにあります。言語が違っても「検証コマンドを固定し、完了条件をそこに寄せる」構造は同じです。
permissionsで実行の線引きを決める
CLAUDE.mdだけだと、確認プロンプトは毎回出ます。逆にRscript全体を許可すると、任意のRコードが確認なしで動きます。そこで.claude/settings.jsonで、検証スクリプトだけを許可します。
{
"permissions": {
"allow": [
"Bash(Rscript scripts/check.R)"
],
"ask": [
"Bash(Rscript -e *)"
],
"deny": [
"Edit(/renv.lock)"
]
}
}ルールの評価順はdeny、ask、allowです。完全一致のBash(Rscript scripts/check.R)は確認なしで通り、Rscript -eで渡す任意のコードは確認に回ります。-eの中身はRの任意のコードなので、ルールで安全な部分だけを選ぶことはできません。renv::snapshot()もrenv::update()もここに含まれ、毎回人間の目に触れます。
Edit(/renv.lock)のdenyは、ファイル編集ツールでrenv.lockを書き換えるのを止めます。スラッシュ始まりのパスは、プロジェクト設定ファイルの位置を基準に解釈されます。ただし、これが止めるのはEditツールだけです。Rscript経由のsnapshot()による書き込みは別の経路で、止まりません。そのため、snapshot()は上のaskで確認に回しています。
ルールの確認や追加は/permissionsでできます。操作はClaude Codeの/permissionsコマンドで権限ルールとauto mode拒否を管理するにまとめています。
解析スクリプト本体(Rscript analysis/run.Rのような実行)を確認なしで通したいなら、そのパスの完全一致ルールをallowへ足します。広いRscript *は避けるのが無難です。
renvが守らない範囲
renvが固定するのはRパッケージです。再現性にはほかの要素もあり、別途の対処が必要だと注意書きにあります。
renvだけでは再現しないもの
Rのバージョン
renvは記録するだけで、切り替えは助けません。
rigのようなツールが紹介されています。Pandoc
rmarkdownはPandocに依存しますが同梱されません。
renv.lockを復元しても、同じレンダリングになる保証はありません。OSとシステムライブラリ
OS、システムライブラリ、コンパイラのバージョンは範囲外です。Dockerとの併用が案内されています。
もう1つ見落としやすいのが、バイナリの扱いです。もとのパッケージがバイナリで入っていて、そのバイナリが入手できなくなると、renvはソースからのインストールを試みます。システムの前提が足りないと、これは失敗しがちです。別のマシンでrestore()が失敗したときは、最初にここを疑います。
これらはClaudeに任せる範囲の外です。CLAUDE.mdに「Rのバージョンはrenv.lockのR欄を見る」と書き、実際のRのバージョンが違うときはClaudeに警告させておくと、原因の切り分けが早くなります。
従われないときと、強制したいとき
CLAUDE.mdの指示が守られないときは、順に確認します。
/contextでMemory filesの一覧に、そのCLAUDE.mdが出ているか- 指示が「テストする」のように曖昧でないか
- 別のCLAUDE.mdや
.claude/rules/と矛盾していないか
存在しないコマンドや矛盾は、/doctor prompt-auditでも洗い出せます。
「コードを変えるたびに必ず検証する」という保証が欲しいなら、CLAUDE.mdではなくhookの領分です。公式も、特定のタイミングで必ず実行したい処理はhookにするよう案内しています。Rscript scripts/check.Rを編集後や完了前に走らせる構成は、PostToolUse hookのLint自動修正の型が参考になります。
まとめ
Rのプロジェクトでは、依存の固定をrenv.lockに、再現の判定をscripts/check.Rに、運用の約束をCLAUDE.mdとpermissionsに分けて持たせると、Claudeに任せる範囲がはっきりします。snapshot()は非対話では確認なしで書き込まれるため、記録は検証が通ったあとだけ、更新は人間の確認つき、が最小限のルールです。Pythonのデータ処理で同じ考え方を使う場合は、Claude Codeでpandasのデータ処理を書く実践手順が参考になります。