Claude Media
Claude CodeでRの解析を進める — renvで依存を固定しRscriptで確かめる

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点です。

  1. 初期化の結果として何をコミットするか
  2. パッケージを足したとき、どの確認を通ってから記録するか
  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. 1

    入れる

    renv::install("パッケージ名")かinstall.packages()で入れます。renvは既存のやり方を壊さない方針で、どちらも使えます。

  2. 2

    動かす

    解析スクリプトとテストを実行し、期待どおり動くかを見ます。

  3. 3

    記録する

    通ったときだけrenv::snapshot()を実行し、renv.lockを更新します。

  4. 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の指示が守られないときは、順に確認します。

  1. /contextでMemory filesの一覧に、そのCLAUDE.mdが出ているか
  2. 指示が「テストする」のように曖昧でないか
  3. 別の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のデータ処理を書く実践手順が参考になります。

この記事を共有:XはてブLinkedIn