はじめに
Claude CodeにRustのコードを生成させると、コンパイルは通っているのにclone()とunwrap()が大量に出てくることがある。エラーを沈黙させる最短路として使われるためだ。RustらしいコードをClaude Codeに書かせるには、CLAUDE.mdに「何をしてはいけないか」を書く。
Claude CodeがRustで間違えやすいパターン
clone()の乱用──借用で解決できるのに複製する
// Claude Codeが出しやすい(Vecごとclone)
fn process_names(names: Vec) -> Vec {
names.clone().iter().map(|n| n.clone().to_uppercase()).collect()
}
// 正しいパターン(スライスとイテレータ)
fn process_names(names: &[String]) -> Vec {
names.iter().map(|n| n.to_uppercase()).collect()
} borrow checkerのエラーをclone()で黙らせる癖が出やすい。&[T]や&strで渡せる場合、clone()は不要だ。
unwrap()の多用──エラー処理の強みを捨てる
// Claude Codeが出しやすい
fn read_config(path: &str) -> String {
let content = fs::read_to_string(path).unwrap();
serde_json::from_str::(&content).unwrap()["key"]
.as_str().unwrap().to_string()
}
// 正しいパターン(anyhow + ?演算子)
use anyhow::{Context, Result};
fn read_config(path: &str) -> Result {
let content = fs::read_to_string(path)
.with_context(|| format!("設定ファイルの読み込みに失敗: {path}"))?;
serde_json::from_str::(&content)
.context("JSONパースに失敗")?["key"]
.as_str().context("keyが文字列ではない")
.map(|s| s.to_string())
} tokio非同期の落とし穴
async fn内でstd::fsなどの同期I/Oを呼ぶと、tokioのスレッドをストールさせる。tokio::fsの非同期版を使うか、tokio::task::spawn_blocking()でブロッキング処理を包む。またtokio::sync::Mutexが必要なのはawaitをまたぐ場合のみで、それ以外はstd::sync::Mutexで十分だ。
Rust向けCLAUDE.md完全テンプレート
# Rustプロジェクト
技術スタック
Rustエディション: 2021 / 非同期ランタイム: tokio / エラー: lib→thiserror / app→anyhow
ビルド・テストコマンド
cargo check / cargo test / cargo fmt / cargo clippy -- -D warnings
所有権・借用
clone()は原則禁止。&T/&mut Tで解決できないか先に検討する
関数引数は &[T] / &str を優先する(Stringを渡さない)
構造体が参照するだけのフィールドにはライフタイム付き借用を使う
'staticをライフタイムエラーの回避に使わない
エラー処理
unwrap()はテストコードのみ許可
expect()を使う場合はパニック理由を文字列で明記する
ライブラリ(lib.rs): thiserrorで型付きエラーを定義する
アプリ(main.rs, bin/): anyhowと?演算子で伝播する
エラーにはwith_context(|| format!(...))でコンテキストを付与する
非同期(tokio)
async fn内でstd::fs等のブロッキングI/Oを使わない(tokio::fs or spawn_blocking)
Mutexはawaitをまたぐときのみtokio::sync::Mutexを使う
block_on()をtokioランタイム内から呼ばない(デッドロック原因)
禁止パターン
本番コードでのunwrap()
理由のないclone()
'staticをライフタイムエラーの回避手段として使うこと
async fn内でのブロッキングI/O PostToolUseフック──cargo fmt + clippy自動実行
.rsファイルを書き込むたびに自動でフォーマット+Lintが走る設定だ。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Write|Edit|MultiEdit",
"hooks": [{ "type": "command", "command": "bash -c 'if [ -f Cargo.toml ]; then cargo fmt && cargo clippy -- -D warnings 2>&1; fi'" }]
}
]
}
}clippyが警告を検出すると、Claude Codeがエラー出力を読んでコードを修正する。このフィードバックループにより、人間が毎回チェックしなくても品質が保たれる。
CIにも同じゲートを設定する。
# .github/workflows/quality.yml(主要ステップのみ)
run: cargo fmt --check
run: cargo clippy -- -D warnings
run: cargo test EM視点:RustチームへのClaude Code導入
導入初期は最低限の禁止事項2行から始める。「unwrap()はテストのみ」「clone()は理由をコメントで明記」だけで、Claude Codeの出力が変わる。1〜2ヶ月後、チームレビューで気になったパターンを追記する。thiserror/anyhowの使い分けや、プロジェクト固有の非同期パターンなどだ。安定期に入ったらclippy.tomlでプロジェクト固有のlintを追加し、-D clippy::unwrap_usedのように特定パターンをCIで強制する。
Rustのborrow checkerとclippyは即座にエラーを返す。他の言語と違い、Claude Codeが変なコードを書いてもコンパイラが自動で検出する。人間のレビューは設計判断とドメインロジックに集中できる。
まとめ
clone()とunwrap()が出てくる根本原因は、borrow checkerとエラー処理のルールをCLAUDE.mdで伝えていないことだ。まず「unwrap()はテストのみ」「clone()は理由をコメントで明記」の2行を追記して、PostToolUseフックでclippyを自動実行する設定を入れてほしい。Rustのコンパイラがそれ以降の品質を担保してくれる。

コメント