Claude Codeでborrow checkerエラーを直す — cloneに逃げない修正方針
Rustの借用エラーE0382・E0499・E0502を、cargo checkのJSON診断をClaude Codeに渡して直す手順。エラーコード別の直し方と、clone連発をレビューで弾く規約をまとめます。
Rustのborrow checker(借用検査器)エラーをClaude Codeで直すコツは、エラーコードごとに「直し方の優先順位」を先に決め、.clone()を最後の手段にしておくことです。borrow checkerは、参照が生きている間に値が動かされたり書き換えられたりしないかをコンパイル時に調べます。エラーは「一度に持てる可変参照は1つ」「参照は有効でなければならない」という2つの規則のどちらかに触れたときに出ます。
指示が曖昧だと、Claudeはエラーを消すいちばん近い手段として.clone()を足しがちです。コンパイルは通っても、無駄な複製が増え、E0499のように意図が変わる直しも混じります。ここではcargo checkのJSON診断を渡す手順、E0382・E0499・E0502の直し方、clone連発をレビューで弾く規約を順に扱います。cargoコマンドの許可設定とclippyの回し方はClaude CodeでAxumのRust APIを開発する手順にまとめてあります。
E0382・E0499・E0502は何が違うのか
三つのコードは、規則のどこに触れたかで分かれます。E0382は所有権の移動(move)の後で元の変数を使ったとき、E0499は可変借用が同時に2つ以上あるとき、E0502は可変借用と不変借用が重なったときに出ます。
同じ系統に、借用中の値を別の場所へ移動したときのE0505もあります。直し方の候補を並べると、次のとおりです。コンパイルが通った後のドキュメント側の警告はrustdocの警告とintra-docリンク切れを直す手順で扱っています。
| コード | 起きていること | 最初に試す直し | cloneが妥当な場面 |
|---|---|---|---|
| E0382 | 起きていることmoveした後に元の変数を使った | 最初に試す直し&で参照渡しにする | cloneが妥当な場面呼び出し後も両方が所有する必要がある |
| E0499 | 起きていること可変借用が同時に2つ | 最初に試す直し先の借用を使い終えてから次を取る | cloneが妥当な場面原則なし |
| E0502 | 起きていること可変借用と不変借用が重なる | 最初に試す直し読み取りと書き込みの順序を入れ替える | cloneが妥当な場面読んだ結果を値として持ち続けたい |
| E0505 | 起きていること借用中の値をmoveした | 最初に試す直し参照で渡すか、借用が終わってからmoveする | cloneが妥当な場面借用側が別の値を持てばよい |
E0382の解説ページは、直し方として参照(&)で借りる方法、.clone()で複製する方法、Copyを実装する方法、RcとRefCellで共有する方法を挙げています。複製は選択肢の一つにすぎず、並びも参照が先です。E0505のページも「moveを避ける」「move前に借用を解放する」「Copyを実装する」の順で、cloneは載っていません。
借用の長さは、参照を作った行からその参照を最後に使った行までです。この仕様を知っていると、E0499・E0502の多くは「使い終わるのを待つ」「順序を替える」だけで消えます。cloneが要るかどうかは、その後で判断します。
cargo checkのJSON診断をClaude Codeに渡す
エラーを渡すときは、人間向けの整形出力でも通じますが、--message-format=jsonの出力を使うと、エラーコード・位置・メッセージを機械的に抜き出せます。cargoは1行に1つのJSONオブジェクトをstdoutへ出し、reasonフィールドで種類を区別します。コンパイラのメッセージはreasonがcompiler-messageの行にあります。
rustcの診断JSONでは、エラーコードがcode.codeに入ります。levelはerrorやwarning、場所はspansの配列で、主たる箇所にはis_primaryが付きます。人間向けに整形された全文もrenderedに入っています。これらを使うと、エラーだけを1行ずつに絞れます。
cargo check --message-format=json 2>/dev/null \
| jq -c 'select(.reason=="compiler-message")
| .message
| select(.level=="error" and .code != null)
| {code: .code.code,
at: (.spans[] | select(.is_primary)
| "\(.file_name):\(.line_start)"),
msg: .message}'ここで示したjqの抜き出しは、上記のフィールド定義から組んだ例です。出力の例として、次のような行が並ぶ形になります。
{"code":"E0382","at":"src/main.rs:8","msg":"borrow of moved value: `name`"}
{"code":"E0502","at":"src/main.rs:14","msg":"cannot borrow `v` as mutable because it is also borrowed as immutable"}注意点が二つあります。まず、--message-format=jsonが制御するのはcargoとrustcの出力だけです。cargo runのようにプログラム自身の出力が混ざるコマンドでは、{で始まる行だけをJSONとして読む回避策が案内されています。次に、2>/dev/nullでstderrを捨てているので、cargo自身のエラー(依存の解決失敗など)は見えなくなります。診断が0行なのに失敗する場合は、リダイレクトを外して実行してください。
渡し方は「直す前に方針を聞く」の二段にする
Claude Codeの一般的なワークフローには、バグ修正を三段で進める型があります。エラーを共有し、直し方の案を出させ、案を指定して適用させる流れです。借用エラーではこの二段目が特に効きます。案の段階で止めると、clone連発を実行前に見つけられるからです。
上の cargo check の結果(E0382・E0499・E0502)について、
まだコードは変更しないで、エラーごとに直し方の案を2つずつ出して。
案ごとに「参照渡し / 順序変更 / 型の変更 / clone」のどれかを明記し、
clone を含む案は、複製しないと成立しない理由も1文で書いて。案を読み、採用するものを選んでから適用を指示します。
E0382 は案1、E0502 は案2で進めて。直したら cargo check を再実行して、
エラーが残っていれば同じ手順で報告して。Claude Codeのベストプラクティス集は、ファイル名・症状・「直った」の条件を先に伝えると修正の往復が減ると説明しています。ここでは「cloneを使うなら理由を書く」が、直った条件の一部です。
エラーコード別の直し方
E0382: moveした値を使い続けている
let s2 = s1;や関数への値渡しで所有権が動くと、元の変数は使えません。E0382のページは、Copyでない型(Stringや独自の構造体)は代入でmoveされると説明しています。直す順は次のとおりです。
E0382を直す優先順
- 1
参照で渡せないか考える
関数が値を読むだけなら、引数を
&Stringや&strにして&s1を渡します。呼び出し側の所有権は動きません。 - 2
小さい型はCopyにできないか考える
数値だけを持つ構造体のように、全メンバーが
Copyなら#[derive(Clone, Copy)]で代入してもmoveされなくなります。 - 3
両方が所有する必要があるときだけcloneする
呼び出し後に元の値も使い、関数側も所有したい場合です。この場合は複製が設計上の要件なので、理由をコメントに残します。
- 4
共有が仕様ならRcとRefCellを検討する
可変な共有所有が本当に必要なときの手段です。借用の検査がコンパイル時から実行時に移るため、設計を見直せないかを先に確認します。
Claudeに任せるときの落とし穴は、手順1を飛ばして3へ行くことです。引数の型をStringから&strに変えるだけで済むのに、呼び出し側に.clone()を足す直しは典型例です。型を変える直しは呼び出し元に波及するため、規約で「まず参照渡しを試す」と書いておくと、波及を引き受けた直しが出やすくなります。
E0499: 可変借用が同時に2つある
Rustの書籍は、可変参照を持つ間は同じ値への他の参照を持てないと説明しています。二つの&mut sを作って両方をあとで使うコードは、E0499になります。
// エラーになる形
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
println!("{r1}, {r2}");直し方は二つあります。波括弧でr1のスコープを閉じてからr2を作るか、r1を使い終える行をr2の前に移します。この書籍の記述では、参照の有効範囲は作った場所から最後に使った場所までです。
ここでcloneを足すと、「s.clone()を可変借用して書き換える」形になりがちです。複製を書き換えても元の値には反映されないため、エラーは消えても処理の意味が変わります。E0382の解説も、複製の変更は元の変数に影響しないと書いています。E0499の表でcloneが「原則なし」なのは、この理由です。
E0502: 読んでいる最中に書こうとしている
不変参照が使われている間に可変参照を作ると、E0502が出ます。E0502のページの直し方は、他の参照が残らない状態にしてから借用の種類を変えることです。配列の先頭を読みながら末尾へ要素を足す、よくある形で比べます。
読み取り結果を値として取り出す
参照を持ったままpushする(E0502)
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{first}");値を取り出してからpushする
let mut v = vec![1, 2, 3];
let first = v[0];
v.push(4);
println!("{first}");要素がi32のようにCopyな型なら、参照でなく値を取り出せば借用が残りません。Stringのような型なら、参照を使い終えてからpushする順序の入れ替えが第一候補です。取り出した後も値が必要で、順序を替えられないときだけ.clone()を検討します。
cloneで逃げないための規約とレビュー
方針を毎回の指示に書くのは手間なので、CLAUDE.mdに規約として置きます。CLAUDE.mdは強制ではなく文脈なので、検証できる形で書くほうが守られます。1ファイルは200行未満が目安とされています。
## Rustの借用エラー
- 変更後は必ず `cargo check` を実行し、エラーが残る間は直し続ける
- 借用エラーは、参照渡し→使用順序の変更→型の変更の順に試す
- `.clone()` は最後の手段。足すときは直後のコメントに複製が必要な理由を書く
- エラーを消すために lifetime 注釈の追加、`unsafe`、`Rc<RefCell<_>>` の導入をしない。
必要なら先に提案して承認を取る
- 直したら `cargo clippy -- -W clippy::redundant_clone` も通す最後の行のlintは、要らない複製を見つけるためのものです。redundant_cloneは、以後使われずにdropされる値のcloneを検出します。このlintはclippyのnurseryグループに属するため、-Wで明示して有効にします。引数がCopyな型への.clone()を指摘するclone_on_copyは、complexityグループです。
追加されたcloneを差分から拾う
lintで拾えない複製は、差分から機械的に拾います。変更行のうち.clone()を含むものだけを出し、Claudeに一つずつ理由を説明させます。
git diff -U0 main -- '*.rs' | grep -n '^+.*\.clone()'上の差分で追加された .clone() を1件ずつ見て、
参照渡しや順序変更で消せるものは消して。
消せないものは、複製が必要な理由を1文で説明して。この確認を毎回の定型にすると、レビューの負担は「clone行を読む」だけになります。説明が「借用エラーを避けるため」だけの複製は、直しの案をやり直す合図です。
cargoコマンドの許可
反復ではcargo checkを何度も実行します。許可ルールはBash(cargo check *)のように書き、末尾の空白付きの*は引数なしのcargo checkにも一致します。Bash(cargo *)と書くとcargo publishまで通るため、動詞ごとに分けて許可してください。
{
"permissions": {
"allow": [
"Bash(cargo check *)",
"Bash(cargo clippy *)",
"Bash(git diff *)"
]
}
}つまずきやすい点
一つ直すと別のエラーが出る
moveの修正で参照渡しにしたら、呼び出し先で今度はE0502が出る、という連鎖はよくあります。直した箇所の周りで、別の規則に触れる書き方になることがあるためです。一回の修正で全部消えなくても、エラーの数と種類が減っているかを毎回報告させると、直しが進んでいるか判断できます。
落とし穴
cloneで通ったコードは、あとで性能と意図のずれになる
.clone()を足してコンパイルが通ると、エラーは消えますが、借用の設計は直っていません。大きなVecやStringをループ内で複製すると、無駄な確保が増えます。E0499の例のように複製した側を書き換える直しは、意図が変わります。
RcとRefCellは解決でなく検査の先送り
RcとRefCellでコンパイルエラーは消せますが、借用の検査が実行時に移ります。E0382の解説ページも、RefCellは実行時に借用検査を行い、書き込みは同時に1つ、読み取りは複数、という規則を守らせる仕組みだと説明しています。違反したときは実行時に失敗します。その読み方はRustのpanicをbacktraceからClaude Codeで辿る手順で扱っています。
設計が原因のときは型の変更に進む
同じ構造体の二つのフィールドを別々に可変借用したいとき、メソッド越しに借りるとE0499になる場合があります。この種の衝突は、構造体を分けるか、更新処理を一つのメソッドにまとめるといった型の変更で解消します。ゲームのように状態が大きいプロジェクトでは、BevyのRustゲーム開発のようにシステムごとに借用の範囲が決まる設計が前提になります。CLIならば、clap製CLIをnextestで回す手順のテスト反復と組み合わせて、直した後の挙動も確かめられます。
まとめ
借用エラーは、cargo checkのJSON診断からエラーコード別に整理し、参照渡し、順序の入れ替え、型の変更の順で直します。cloneは最後の手段にして、足した行に理由を書かせます。差分から.clone()を拾って一つずつ説明させれば、レビューで弾く基準が明確になります。
最初に決めておく価値があるのは、「この直しで所有権の設計は変わったか、エラーが消えただけか」を毎回問う習慣です。エラーが消えただけの直しは、同じ箇所で次の変更のたびに再発します。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。
- code.claude.com/docs/en/common-workflows
- code.claude.com/docs/en/best-practices
- code.claude.com/docs/en/permissions
- doc.rust-lang.org/book/ch04-02-references-and-borrowing.html
- doc.rust-lang.org/error_codes/E0382.html
- doc.rust-lang.org/error_codes/E0499.html
- doc.rust-lang.org/error_codes/E0502.html
- doc.rust-lang.org/error_codes/E0505.html
- doc.rust-lang.org/cargo/reference/external-tools.html
- doc.rust-lang.org/rustc/json.html