Claude Media
Claude CodeでSolidityとFoundryを回す — 検証ループをCLAUDE.mdに固定

Claude CodeでSolidityとFoundryを回す — 検証ループをCLAUDE.mdに固定

Claude CodeにSolidityを書かせるとき、forge testとfuzzの検証ループをCLAUDE.mdに固定し、秘密鍵やデプロイ系コマンドをpermissionsで止める設定例をまとめます。

Claude CodeにSolidityを書かせるなら、Foundryのforge testを毎回の合格条件にします。この記事では、検証コマンドの順番をCLAUDE.mdに固定する方法と、秘密鍵・デプロイ系のコマンドをpermissionsで止める方法を設定例つきで示します。

スマートコントラクトは、デプロイ後に直せないことがあります。だからこそ、Claudeが「書いて、テストして、直す」を自分で回せる状態を先に作り、チェーンに触れる操作だけを人の手元に残す構成が効きます。

Foundryのどのコマンドを検証ループにするか

Foundryは、Forge(ビルド・テスト・デプロイ)、Cast(チェーン操作・署名・送信)、Anvil(ローカルノード)、Chisel(Solidity REPL)の4ツールで構成されています。Foundryのドキュメントには、AIエージェント向けの案内ページがあります。そこでは、forge build、forge test、forge config、cast call、cast rpcを読み取り中心のコマンドとして先に使うよう勧めています。cast send、forge create、forge script --broadcastは状態を変える操作として扱い、実行前にチェーン・署名者・金額・ユーザーの許可を確かめる、という整理です。

この線引きが、そのままClaude Code側の設計図になります。

線引き

Claudeに任せるものと止めるもの

  • 自走させる

    forge fmt --check、forge lint、forge build、forge test、forge snapshot --check。ローカルで完結し、チェーンの状態を変えません。

  • 承認制にする

    forge script(--broadcastなし)。シミュレーションですが、--rpc-urlを付けるとライブチェーンの状態に対して実行され、環境変数も読みます。

  • 拒否する

    forge create、cast send、cast wallet、forge script --broadcast、--private-keyを含むコマンド。署名と送信は人が行います。

テストを書く前に決めておくこと

Foundryのテストは、Solidityで書きます。ファイル名は.t.solで終え、テスト関数はtestで始め、forge-std/Test.solを継承します。setUp()は各テストの前に走ります。Claudeがこの慣習から外れたファイルを作ると、forge testがテストを拾わないことがあるので、CLAUDE.mdに明記しておきます。

Foundryのベストプラクティスにある命名規則も、そのまま規約にできます。

種類命名の型例
通常のテスト命名の型test_Description例test_TransferUpdatesBalances
fuzzテスト命名の型testFuzz_Description例testFuzz_TransferAnyAmount
revertのテスト命名の型test_RevertWhen_Condition例test_RevertWhen_InsufficientBalance

fuzzテストは、引数を取るテスト関数を書くだけで自動的にfuzzされます。実行回数の既定は256回です。入力の絞り込みにはvm.assume()とbound()がありますが、ベストプラクティスではbound()を勧めています。vm.assume()は条件に合わない入力を捨てるため、fuzzが遅くなるからです。この一点はCLAUDE.mdに書いておく価値があります。

CLAUDE.mdに検証ループを固定する

リポジトリのルートに置くCLAUDE.mdの例です。コマンドの順番と、失敗時の読み方を決めています。Foundryのドキュメントが示すプロンプト雛形(forge-stdを優先する、revertのテストを必ず書く、fuzzの入力はvm.assumeかboundで絞る)も、制約として取り込みました。

# Solidity / Foundry 開発ルール
 
## 検証ループ(変更のたびにこの順で実行)
1. `forge fmt --check`
2. `forge lint`
3. `forge build`
4. `forge test -vvv`
5. 失敗したら出力のスタックトレースを読み、原因を1行で述べてから直す
6. 全て通ったら、実行したコマンドと結果を報告する
 
## テストの書き方
- テストは `test/` に `*.t.sol` で置き、`forge-std/Test.sol` を継承する
- 命名は `test_` / `testFuzz_` / `test_RevertWhen_` の3種類
- 失敗経路ごとに revert のテストを書く(`vm.expectRevert`)
- fuzz の入力制約は `bound()` を使う。`vm.assume()` は最小限
- テストを通すためにアサーションを弱めたり削除したりしない
 
## 触らないもの
- `foundry.toml` の `ffi` を有効にしない
- `.env` と keystore を読まない
- `forge script --broadcast` / `forge create` / `cast send` を実行しない。
  デプロイが必要なら、コマンド案を提示して止まる

「アサーションを弱めない」の一行は、このループで最も効きます。テストが赤いとき、AIにとって最短の「解決」はテスト側を書き換えることです。検証コマンドを固定しても、合格条件を動かされては意味がありません。同じ考え方は、pytestの規約をCLAUDE.mdに書く場合や、ruffとmypyの品質ゲートでも使えます。

fuzzを深く回すプロファイルを分ける

fuzzの実行回数はfoundry.tomlで設定します。Foundryのドキュメントには、CI用のプロファイルで回数を増やす例があります。

[profile.default.fuzz]
runs = 256
 
[profile.ci]
verbosity = 3
 
[profile.ci.fuzz]
runs = 10000
 
[profile.ci.invariant]
runs = 1000
depth = 1000

普段の修正ループは既定の256回で速く回し、仕上げの確認だけciプロファイルに切り替えます。切り替えは環境変数FOUNDRY_PROFILEで行います。

FOUNDRY_PROFILE=ci forge test -vvv

CLAUDE.mdには「実装が一段落したら、最後にFOUNDRY_PROFILE=ci forge testを1回通す」と1行足します。毎回10,000回を回すと、修正のたびの待ち時間が増えるためです。

invariantテストは、ランダムな呼び出しの並びに対して「常に成り立つべき性質」を検証します。invariant_で始まる関数を書き、setUp()でtargetContract()を指定します。ハンドラーで入力を絞り、ゴースト変数で入出金の合計を追う型がFoundryのガイドにあります。残高と記帳の整合のように、状態が積み上がるコントラクトで効きます。

permissionsで鍵とデプロイを止める

CLAUDE.mdは「お願い」です。強制するのはpermissionsです。プロジェクトの.claude/settings.jsonに、次のような設定を置きます。

{
  "permissions": {
    "allow": [
      "Bash(forge fmt *)",
      "Bash(forge lint *)",
      "Bash(forge build *)",
      "Bash(forge test *)",
      "Bash(forge snapshot *)",
      "Bash(cast call *)"
    ],
    "ask": [
      "Bash(forge script *)",
      "Bash(anvil *)",
      "Edit(foundry.toml)"
    ],
    "deny": [
      "Bash(forge script *--broadcast*)",
      "Bash(forge create *)",
      "Bash(cast send *)",
      "Bash(cast publish *)",
      "Bash(cast mktx *)",
      "Bash(cast wallet *)",
      "Bash(*--private-key*)",
      "Read(./.env)",
      "Read(./.env.*)",
      "Read(~/.foundry/keystores/**)"
    ]
  }
}

評価の順番は、deny、ask、allowです。最初に当たったルールで結果が決まり、denyに当たった呼び出しは、allowに同じ呼び出しが含まれていても通りません。この例では、forge scriptは承認を求め、--broadcastを含む呼び出しはdenyに当たって拒否されます。

設計の意図は次のとおりです。

  • cast callを許可: 読み取り専用です。ローカルのAnvilに向ければ、デプロイ後の状態をClaudeが自分で確かめられます
  • forge scriptをask: --broadcastなしでも--rpc-urlがあればライブチェーンの状態を使い、vm.envUint("PRIVATE_KEY")のような環境変数の読み取りを含むことがあります。何を読むスクリプトかを人が見てから通します
  • foundry.tomlの編集をask: テスト設定のffiを有効にすると、テストが任意のプログラムを実行できるようになります。Foundryのドキュメントも、信頼できる環境でのみ有効にするよう警告しています
  • .envとkeystoreのRead拒否: ファイルツールからの読み取りを止めます。ベストプラクティスでも、本番の鍵は暗号化したkeystoreか、ハードウェアウォレットに置き、.envはバージョン管理から外すよう勧めています

Bashのdenyは境界ではない

Claude Codeのドキュメントは、Bashのdenyルールを「Claudeが通常書くコマンドの形を止めるもの」と位置づけています。Bash(rm *)はrm -rf build/を止めますが、/bin/rmやbash -c 'rm ...'は止めません。同じことがcast sendにも当てはまります。/usr/local/bin/cast send ...のような別の書き方は、上のdenyに当たらない可能性があります。

完全に塞ぐには、2つの手があります。1つはサンドボックスで、ファイルシステムとネットワークの制限をコマンドの文字列に依存せずに掛けます。もう1つはPreToolUseフックで、実行直前のコマンド全文を自前のロジックで検査します。ドキュメントのフック例を、Foundry向けに書き換えた形がこちらです。

{
  "hooks": {
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-chain-writes.sh",
            "args": []
          }
        ]
      }
    ]
  }
}
#!/bin/bash
# .claude/hooks/block-chain-writes.sh(chmod +x が必要・jq が必要)
COMMAND=$(jq -r '.tool_input.command')
 
if echo "$COMMAND" | grep -Eq 'cast +(send|publish|mktx)|forge +create|--broadcast|--private-key'; then
  jq -n '{
    hookSpecificOutput: {
      hookEventName: "PreToolUse",
      permissionDecision: "deny",
      permissionDecisionReason: "チェーンへ書き込む操作は人が実行します"
    }
  }'
else
  exit 0
fi

パスや引用符で書き方を変えられても、コマンド全文にcastとsendが含まれていれば拾えます。正規表現が広すぎて誤検知する場合は、まずログで拾われたコマンドを見て調整してください。

Anvilでローカルの動作を確かめる

デプロイの挙動を確かめたいときは、ローカルノードのAnvilが使えます。Anvilは起動すると、10個の開発用アカウントにそれぞれ10,000 ETHを与えます。この秘密鍵は公開されているため、ローカル専用です。ドキュメントは、メインネットをforkするときやパブリックなRPCに触れるときは、カスタムのニーモニックを使うよう注意しています。

Anvilは常駐するプロセスなので、Claudeに起動させるよりも、別のターミナルで起動しておく運用が扱いやすくなります。Claudeにはローカルの8545番ポートへのcast callだけを許可すれば、状態の確認は任せられます。

# 別ターミナルで起動しておく
anvil
 
# Claude に許可する読み取り(コントラクトのアドレスは例)
cast call $CONTRACT "number()(uint256)" --rpc-url http://localhost:8545

fork先を使うテストでは、RPCのURLが秘密情報になる場合があります。Foundryのドキュメントは、CIではETH_RPC_URLをリポジトリのシークレットに置く構成を示しています。ローカルでも同様に、URLを.envに置いてClaudeには読ませず、シェルの環境変数として渡す形が安全です。fork先のブロック番号を固定すれば、テストの結果も再現できます。

つまずきやすい点

  • forge test --watchを検証ループに入れない: ファイル変更のたびに再実行する監視モードです。Claudeが「テストを1回走らせて結果を読む」流れに合わないため、CLAUDE.mdには書かず、人が手元で使うコマンドとして残します
  • テストを通すための書き換え: 前述のとおり、アサーションの弱体化をCLAUDE.mdで禁じます。差分でテストファイルの変更を見る習慣も併せて持ちます
  • ツールの名前を推測させない: Foundryはnightlyの更新が速く、ドキュメントと挙動がずれることがあります。バージョン依存の判断はforge --versionとforge test --helpの出力で確かめる、とCLAUDE.mdに書いておくと、古い記憶で書かれたフラグを減らせます
  • デプロイスクリプトのレビュー: .s.solはClaudeに書かせても、実行は人が行います。シミュレーションの出力を読み、チェーンID・送信者・コントラクトの引数を確かめてから、自分の端末で--broadcastを付けます

他の言語やツールでの同じ型

テスト・静的検査・副作用のある操作の3層に分け、前の2つを自走、最後の1つを承認制にする型は、言語を変えても同じです。Terraformではplanの差分を人が確認し、AWS Lambda(SAM)ではlocal invokeで検証してdeployを承認制にします。Solidityでは、副作用の中身が「秘密鍵による署名」になる点が違います。署名が絡む分、denyの対象をcastとforge createまで広げているのが、この記事の設定の特徴です。

まとめ

Solidityでは、デプロイ後に直せないことがあるため、「どこまでをClaudeに任せ、どこから人が署名するか」の線を先に引く価値が大きくなります。forge fmt・lint・build・testはCLAUDE.mdに順番を固定して自走させ、cast send・forge create・--broadcastはdenyとフックの二重で止める。この組み合わせで、Claudeは検証ループを回し続けながら、チェーンには触れません。

テストが通ったあとに残る判断は、2つです。1つは、そのテストが仕様を十分に縛っているかの読み取りです。もう1つは、本番へ出すスクリプトの中身を自分の目で確かめることです。どちらもコマンドでは自動化せず、人の側に残しておく設計です。

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