Claude CodeでTauri v2のnot allowedエラーを直す — 最小権限の足し方
Tauri v2のフロントエンドから出るnot allowedエラーを、Claude Codeにcapabilitiesの最小権限として足させる手順です。エラー文の読み方とスコープの落とし穴も扱います。
Tauri v2のアプリでfs.write_text_file not allowed. Permissions associated with this command: ...のようなエラーが出たら、原因はほぼ決まっています。そのコマンドを、そのウィンドウに許可するcapabilityが無いのです。
直す場所はsrc-tauri/capabilities/のJSONです。Claude Codeに任せるなら、エラー文をそのまま渡し、足す権限を最小に絞らせます。ここでは、エラー文の読み方、権限の足し方、スコープの落とし穴、Claude Codeに広すぎる許可を書かせない設定を順に説明します。環境構築からRust連携まではClaude CodeでTauriアプリを開発するにあります。
not allowedエラーは何を言っているのか
Tauriの実行時権限機構(runtime authority)は、WebViewからコマンドが呼ばれるたびに、呼び出し元がそのコマンドを使えるかを検査します。許可が無ければリクエストを拒否し、コマンド本体は実行されません。つまりRustのコードが間違っているのではなく、入口で止められています。
Tauriのチュートリアルに載っているエラーは次の形です。
[Error] Unhandled Promise Rejection: fs.write_text_file not allowed. Permissions associated with this command: fs:allow-app-write, fs:allow-app-write-recursive, ...この1行から読み取れるものは3つあります。
エラー文から取り出す3つの情報
プラグイン名とコマンド名
fs.write_text_fileのうち、fsがプラグイン、write_text_fileがコマンドです。候補の権限識別子
Permissions associated with this command:以降が、このコマンドを解禁できる権限の一覧です。fs:allow-app-writeのようにプラグイン名:権限名の形で並びます。不足しているもの
エラーは「どれか1つでも許可すれば通る」という候補を並べているだけです。どれを選ぶかは、アプリが何を必要とするかで決まります。
権限識別子の形式は、プラグイン全体の既定が<name>:default、個別コマンドが<name>:<command-name>と決められています。write_text_fileを個別に許可したいなら、チュートリアルが使っているfs:allow-write-text-fileが対応する識別子です。
候補の一覧はエラー文では...で切れて表示されることがあります。全体が必要なときは、プラグインのドキュメントの権限表か、プロジェクトが生成するスキーマで探します。Tauriはビルド時に、アプリで使える権限をすべて含むJSONスキーマをgen/schemasに生成します。capabilityファイルの$schemaにそれを指定すると、エディターで権限名が補完されます。
Claude Codeにエラーを渡す
Claude Codeの公式ワークフローは、バグ修正でまずエラーメッセージを共有し、再現するコマンドと手順、エラーが毎回出るのか断続的なのかも伝えるよう勧めています。このエラーはビルド時ではなく、アプリを動かして操作した時点で、フロントエンドのPromiseの拒否として出ます。cargoのビルドが通っていても起きるので、再現操作もあわせて伝える必要があります。
そこで、次の3点を貼ります。
- エラー全文(
not allowedを含む行) - そのエラーが出る操作(ボタンを押す、ファイルを開くなど)
- 呼び出したフロントエンドのコードの場所
プロンプトは次のような形にすると、権限を広げる方向へ流れにくくなります。
pnpm tauri dev で「保存」ボタンを押すと、次のエラーが出ます。
fs.write_text_file not allowed. Permissions associated with this command: ...
src/save.ts の writeTextFile が原因のはずです。
src-tauri/capabilities/ に、この呼び出しに必要な最小の権限だけを足してください。
fs:default 全体や、$HOME 全体を許すスコープは使わないでください。
足した理由を1行ずつ説明してください。最後の「理由を説明する」は、Claude Codeの差分が妥当かを読む側が判断しやすくするための指示です。
手順: capabilityに権限を足す
エラーから修正までの流れ
- 1
コマンドと権限名を特定する
エラー文のプラグイン名とコマンド名から、
プラグイン名:allow-コマンド名の候補を作ります。write_text_fileならfs:allow-write-text-fileです。プラグインのドキュメントの権限表に同名の行があるかを確認します。 - 2
対象ウィンドウのcapabilityを開く
src-tauri/capabilities/default.jsonなど、そのウィンドウのwindowsに含まれるファイルを選びます。ウィンドウはタイトルではなくラベルで指定されます。 - 3
permissionsに足す
必要な権限だけを
permissions配列に追加します。スコープが要る権限は、次の節のオブジェクト形式で書きます。 - 4
有効化の対象に入っているか確かめる
tauri.conf.jsonのapp.security.capabilitiesに識別子を1つでも書いている場合、そこに並んだcapabilityだけがビルドに使われます。新しいファイルを作ったときは、この列挙も更新します。 - 5
開発サーバーを起動し直して同じ操作を試す
エラーが消えたか、別のコマンドでnot allowedが出ていないかを見ます。複数のコマンドを順に呼ぶ処理では、1つ直すと次の不足が見えることがあります。
公式チュートリアルの例では、$HOME直下のtest.txtにだけ書き込めるようにするとき、default.jsonは次のようになります。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"fs:default",
{
"identifier": "fs:allow-write-text-file",
"allow": [{ "path": "$HOME/test.txt" }]
}
]
}ここでの要点は、権限識別子だけの文字列ではなく、identifierとallowを持つオブジェクトにしている点です。チュートリアルの解答例にはこの前にcore:系の既定権限も並びますが、省略しています。
権限を足したのにforbidden pathになるとき
ファイルシステムプラグインの公式ページには、権限だけではスコープが与えられないという注意があります。fs:allow-existsのような権限を有効にしても、許可するパスが無ければ、実行時にforbidden pathのエラーで失敗します。
スコープが無いと許可は空振りする
権限だけを書いた場合
{
"permissions": ["fs:default", "fs:allow-exists"]
}権限はあるが許可パスが無く、forbidden pathで失敗します。
許可パスを添えた場合
{
"permissions": [
"fs:default",
{
"identifier": "fs:allow-exists",
"allow": [{ "path": "$HOME" }, { "path": "$HOME/**/*" }]
}
]
}公式ページが「動く」例として挙げる形です。$HOME全体を許す例なので、実際のアプリでは範囲を狭めます。
not allowedは権限の不足、forbidden pathはスコープの不足、と読み分けると、Claude Codeへの次の指示も変わります。前者には権限の追加、後者にはパスの許可範囲の設計を頼みます。
fs:defaultが許す範囲も確認しておきます。この既定セットは、アプリ固有のディレクトリ(AppConfig、AppData、AppLocalData、AppCache、AppLog)の読み取りと、そこへのフォルダー作成を許可します。アプリのデータ置き場に保存するだけなら、$APPDATA系のスコープで足りることが多く、$HOMEを開ける必要はありません。公式ページの例にも"allow": [{ "path": "$APPDATA/*" }]の形があります。
それでも直らないときの切り分け
権限を足してもエラーが残るときは、次の順で確かめます。Claude Codeに「この順に確認して、どこで止まったかを報告して」と頼むと、むやみに権限を足し続ける動きを防げます。
not allowedが消えないときの確認順
- Qエラー文のコマンドと、足した権限の名前は対応しているか?答えがいいえのとき答えがはい
プラグイン名とコマンド名を見直し、権限表にある識別子へ直します。
- Qそのcapabilityの`windows`に、呼び出し元ウィンドウのラベルが入っているか?答えがいいえのとき答えがはい
セキュリティ境界はウィンドウのラベルで決まり、タイトルではありません。
windowsのラベルを実際のウィンドウに合わせます。 - Q`tauri.conf.json`の`app.security.capabilities`を使っている場合、そのcapabilityが並んでいるか?答えがいいえのとき答えがはい
列挙に識別子を足します。ここを見落とすと、ファイルを足しても無視されます。
- Q`platforms`で、今動かしているOSが除外されていないか?答えがいいえのとき答えがはい
platformsを指定したcapabilityは、指定外のOSでは適用されません。デスクトップ用とモバイル用でファイルを分けているときに起こります。
それでも出るとき
権限は足りているのでスコープ側を疑います。エラー文がforbidden pathなら、許可パスの書き方を見直します。
自作のRustコマンドは、少し事情が違います。invoke_handlerに登録したコマンドは、既定ではすべてのウィンドウとWebViewから呼べます。許可を絞るのはsrc-tauri/build.rsのAppManifest::commandsで、コマンドを列挙した後は、アプリ側のsrc-tauri/permissions/に権限定義を書いて、capabilityから参照する形になります。アプリ側の権限ファイルはTOMLでしか書けず、capabilityはJSONかTOMLを選べます。
Claude Codeに広すぎる許可を書かせない
エラーを早く消したいとき、AIはfs:defaultを足す、$HOME/**を許す、といった広い解決に傾きがちです。Tauriの公式ページは、複数のcapabilityに入ったウィンドウは、それぞれの許可が合算されると説明しています。広い許可を1か所に足すと、他のcapabilityと合わさって効果が広がります。
ガードレールは2つあります。1つ目は、プロジェクトのCLAUDE.mdに方針を書くことです。
## Tauriの権限
- not allowed エラーは、該当コマンド1つ分の権限だけを capabilities に足す
- fs のスコープに $HOME や ** 全体を指定しない
- 権限を足したら、足した識別子と理由をコミットメッセージに書く2つ目は、capabilityの編集で毎回確認を出す設定です。Claude Codeの権限ルールは、askルールが、よりくわしいallowルールに優先して確認を出します。編集系のルールはEditで書き、パスはgitignore形式で指定します。
{
"permissions": {
"ask": [
"Edit(src-tauri/capabilities/**)",
"Edit(src-tauri/tauri.conf.json)"
]
}
}.claude/settings.jsonに置くと、チームで共有できます。これでcapabilityの変更が出るたびに差分を読む機会ができます。権限ルールの書き方は、ffmpegの権限設計でも別の題材で扱っています。
守備範囲を取り違えない
capabilityが守るのはフロントエンドからの不正な呼び出しです。公式ページは、悪意のあるRustコード、緩すぎるスコープ、コマンド実装内のスコープ検査の誤りは防げないと明記しています。つまり、権限を足した後は、Rust側のコマンドが受け取るパスを検証しているかも見る対象です。
もう一つ、リモートのURLからコマンドを呼ぶ設定(capabilityのremote.urls)は、既定では使えません。LinuxとAndroidでは、埋め込みの<iframe>とウィンドウ本体からのリクエストを区別できないため、慎重な利用が求められています。not allowedを消したい一心で、外部サイトに権限を開く方向へ進めないでください。
まとめ
not allowedはコマンド単位の権限不足、forbidden pathはスコープ不足です。エラー文を読み分ければ、Claude Codeへの依頼は「該当コマンドの権限を1つ足す」と「許可パスを狭く書く」に分けられます。修正のたびに差分を読めるよう、capabilityの編集にaskルールを置いておくと、AIが善意で広げた許可に気づけます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。