はじめに
弊社のシステム開発グループでは Claude Code を利用して開発を行っています。 AI を利用した開発は便利である一方、注意すべきことも多くあると思います。
今回は、私が実際に開発時に経験した「シェルのコマンド置換による意図しないコマンド実行」についてお話しします。 ※ 動作確認は macOS(zsh)で行っています。
実際に起きたこと
私は作業完了後、Claude Code の AI エージェント経由で Obsidian(ドキュメントツール)に作業ログを Markdown 形式で記録しています。 Obsidian には Obsidian CLI というツールがあり、コマンドライン経由でドキュメントを更新できます。
その Obsidian CLI を使って作業ログを作成した際、ログ内に記述されていた過去の実行コマンド git stash pop が、実際にシェル上で実行されてしまっていたことに気づきました。
git stash pop は退避していた変更を作業ツリーに戻すコマンドなので、意図しないタイミングで実行されると作業中のファイルの状態が変わってしまいます。
原因
AI エージェントが Obsidian CLI を呼び出す際、Markdown のインラインコード表現であるバッククォートを含めた文字列を、ダブルクォートで囲んでコマンド引数として渡していたことが原因でした。
obsidian append content="10:11 `git stash pop` を実行しました。"
シェルでは、ダブルクォートで囲んだ文字列の中でもバッククォート(`...`)や $(...) が「コマンド置換」として展開され、中のコマンドが実行されてその出力に置き換わります。
今回は Obsidian CLI が実行したのではなく、Obsidian CLI に引数が渡される前の段階で、シェルが git stash pop を「文字」ではなく「実行する命令」として扱っていました。
※ シングルクォートで囲んだ場合は展開されません。
単語をインラインコードで表示する場合に通常の Markdown の書き方としてバッククォートを使用します。 AI エージェントは作業ログを書く際に、コマンド名をバッククォートで囲んでいました。
他の事例(GitHub CLI や git コマンドなど)
この問題は、CLI(コマンドラインツール)経由でドキュメントやテキストを更新する場面で発生する可能性があります。
例えば、gh コマンド(GitHub CLI)を使って PR や Issue の説明文を作成するときや、git commit のメッセージを指定するときなどです。
実際に、コミットを作らずに安全な方法で確認してみました。
git commit --dry-run --allow-empty -m "fix: `echo X > /tmp/proof` の修正"
※ --dry-run はコミットを実際には作成しないオプション
このコマンドを実行すると、コミットは作られないものの、シェルのコマンド置換が行われ、実際に /tmp/proof というファイルが生成されました。
GitHub CLI などのツールでも、AI が生成したコマンドの引数をきっかけに、意図しないコマンドが実行されるリスクが身近にあるということが分かります。
対策
この問題を防ぐため、以下の対策を実施しました。
- シェルを経由しないツール(Write / Edit)に切り替える
- Obsidian へのログ記録は Obsidian CLI の利用をやめ、Claude Code のファイル直接編集ツール(Write / Edit)に切り替えました。Write / Edit はシェルを通さないため、コマンド置換は起こりません。
- 文字列を CLI の引数として直接渡さない
- テキストを一時ファイルに書き出し、CLI にはそのファイルを読み込ませる形にしました。
「気をつけて」とあらかじめ教えておかないと、AI 自身は危なさに気づかないので、これらはルールとして CLAUDE.md などに書き残しておくことも大事だと思います。
また、CLI 経由ではなく MCP(Model Context Protocol)を活用するのも対策になります。
※ MCP サーバーが内部でシェルを使っている場合は、同様の問題が起こる可能性があります。
おわりに
今回は Obsidian CLI 導入時にたまたま気づくことができましたが、知らぬ間に影響が大きいコマンドが実行されていたらと思うとゾッとします。
AI エージェントを利用していてなにか怪しい挙動に気づいた時点で調査し、仕組み・ルールで防ぐという考え方が大事だと思いました。