Claude Media
Claude Codeのskillで制御文字が混ざると「No shell command」で全部止まる

Claude Codeのskillで制御文字が混ざると「No shell command」で全部止まる

skillやカスタムコマンドの!コマンドに制御文字が1つ混ざると、そのファイルのコマンドは1つも実行されません。エラー文の読み方と、文字の探し方・取り除き方をまとめます。

skillやカスタムコマンドを呼んだとき、No shell command in <name> was run, because the Bash tool does not accept one of them と出て何も実行されない場合、原因はコマンド行に紛れた制御文字です。1つでもあると、そのファイルにある !`コマンド` は全部実行されません。直し方は、メッセージ末尾に引用されたコマンドの � が指す文字を取り除くことです。

このエラーが出る条件は「タブと改行以外の制御文字」

skillの本文には、Claudeへ渡す前にシェルコマンドを実行して出力を差し込む記法があります。!`gh pr diff` のような行や、```! で開くフェンスがそれです。この記法を動的コンテキストの注入と呼びます。

エラーが出るのは、ファイル中のいずれかの ! コマンドに、生の制御文字が含まれているときです。ただしタブと改行は対象外です。対象は、タブと改行を除く制御文字すべてです。

読み取れる点は次のとおりです。

項目内容
対象内容skillとカスタムコマンドのファイル
条件内容! コマンドに、タブ・改行以外の生の制御文字がある
結果内容呼び出し全体を拒否。ファイル内のコマンドは1つも走らない
実行ツール内容メッセージにBashかPowerShellの名前が出る
導入内容v2.1.290 の changelog に「raw control characters を含む ! コマンドを拒否する」変更として記載

「全部止まる」点が見落としやすいところです。怪しいコマンドが3行目の1つだけでも、1行目と2行目の無害なコマンドまで実行されません。

エラー文のどこを読めばよいか

エラーリファレンスにある例は次のとおりです。

No shell command in /my-skill was run, because the Bash tool does not accept one of them: command contains control characters that would be hidden in the approval dialog. Unprintable characters are shown as � below: remove them from the skill or command file, or from the arguments or options substituted into it.
build.sh --target�dist

読みどころは3つあります。

  • 先頭の /my-skill: どのskillまたはコマンドが拒否されたかを示します
  • Bash tool: コマンドを実行するはずだったツールです。PowerShellのskillならその名前になります
  • 最終行のコマンド: 拒否の原因になったコマンドです。表示できない文字が � に置き換わっています

理由の文にある「承認ダイアログで隠れてしまう」が、拒否の根拠です。画面に見えない文字を含むコマンドは、人が承認するとき中身を確かめられません。メッセージの理由文が示す方針は、人が承認で中身を確かめられないコマンドは実行前に止める、というものです。

� は文字そのものではなく、位置の目印です。最終行を見て「--target と dist の間に何かある」と分かれば足ります。何の文字かは後述の方法で確かめます。

似た3つのエラーとの見分け方

注入コマンドまわりには、よく似た中断が3つあります。出る段階が違うので、文言で切り分けられます。

メッセージ止まる理由実行前 / 実行中
No shell command in <name> was run止まる理由! コマンドに制御文字がある実行前 / 実行中実行前。1つも走らない
Shell command failed for pattern "..."止まる理由コマンドが失敗した(終了コードが非0など)実行前 / 実行中実行中。失敗した時点で呼び出し全体が中断
Shell command permission check failed for pattern "..."止まる理由権限チェックが許可を返さなかった実行前 / 実行中実行前の権限判定

2行目と3行目は、skillsページが「コマンドの失敗は呼び出し全体を中断する」「権限チェックが許可以外なら中断する」と説明している挙動です。どれも「skillの本文がClaudeに届かない」点は同じです。

制御文字のエラーだけは、コマンドの中身に問題があり、コマンド自体の成否は関係しません。echo ok のような無害なコマンドでも、制御文字が混ざれば拒否されます。

制御文字はどこから混ざるか

エラーリファレンスの対処は「skillまたはコマンドのファイルから、あるいはファイルに差し込む引数やオプションから、その文字を取り除く」です。混入経路が2つあることになります。

  1. ファイルそのもの: エディタで書いた時点で入っている
  2. 差し込まれた値: $ARGUMENTS や $1、名前付き引数に入った値が、展開後のコマンドに制御文字を持ち込む

2つ目は見つけにくい経路です。ファイルを cat -v で調べてもきれいなのに、特定の引数で呼んだときだけ拒否されます。ファイル側を疑って空振りしたら、直前に打った引数を確認します。

ページに書かれてはいませんが、一般にこの種の文字が入りやすい場面は次のとおりです。

  • 色付きのターミナル出力をそのまま貼り付けた(ESC、つまり 0x1b が残る)
  • ウェブページやチャットから貼り付けた文章に、見えない制御コードが混ざっていた
  • 他のツールが生成したファイルを取り込んだ

CRLFの改行コード(行末の \r)が該当するかは、公式のページに記載がありません。\r もタブ・改行以外の制御文字なので、気になる場合は次の節の検出コマンドで実際に拾えるかを確かめられます。

制御文字を探して取り除く

メッセージの � の位置が分かっていても、目では文字を特定できません。ファイルを直接調べるほうが確実です。

まず cat -v で、見えない文字を ^[ や ^_ の形に可視化します。

cat -v .claude/skills/my-skill/SKILL.md | grep -n '\^'

^ を含む行が候補です。ただしこの方法は、本文中に正当な ^(正規表現など)があるとそれも拾います。誤検出が多いときは、制御文字の範囲を直接指定します。

LC_ALL=C grep -n $'[\001-\010\013-\037\177]' \
  .claude/skills/my-skill/SKILL.md

この文字クラスは、タブ(\011)と改行(\012)を除く、0x00〜0x1f と 0x7f の制御文字を指します。条件と同じ範囲を狙った書き方です。ヒットした行番号が分かれば、エディタで該当行を開いて消せます。

たとえば --target と dist の間に 0x1f がある行は、端末では --targetdist と詰まって見えることがあり、人の目には何も入っていないように映ります。この grep なら、そうした行も行番号付きで拾えます。承認ダイアログで隠れるというメッセージは、この見えにくさを指しています。

まとめて除去したいときは tr が使えます。

LC_ALL=C tr -d '\000-\010\013-\037\177' \
  < SKILL.md > SKILL.clean.md

出力を目で確かめてから差し替えます。tr はCRも消すので、CRLFで保存していたファイルはLFになります。

PowerShellを使うWindows環境では、Select-String に正規表現 [\x00-\x08\x0B-\x1F\x7F] を渡す方法があります。

引数側が原因のとき

$ARGUMENTS などに入った値が原因なら、ファイルを直しても解決しません。呼び出しの入力を確かめます。

  1. 同じskillを、引数なしで呼んで通るか確かめる
  2. 通るなら、引数を手で打ち直す(貼り付けた文字列を使わない)
  3. 日常的に貼り付けで渡すなら、コマンドに差し込む前の整形をskillの仕様として決める

3つ目の整形を、skill側でどう書くかは記載がありません。引数を !`コマンド` へ直接差し込む設計を避け、Claudeに引数の内容を読ませてから実行させる形も選べます。

どのシェルがコマンドを受けるか

エラー文の Bash tool の部分は、frontmatterの shell キーと実行環境で決まります。skillsページの整理は次のとおりです。

  • shell: powershell でPowerShellツールが有効なら、PowerShellツールが実行する
  • shell: bash を指定してbashが使えない環境(Git BashのないWindows)では、コマンドが走る前に「Skill <name> requires bash」で呼び出し自体が失敗する
  • それ以外は、bashが使えればBashツール、使えなければPowerShellツールが実行する

この選択は、制御文字の検査とは独立しています。PowerShellのskillでも、エラー文のツール名が変わるだけで、取り除く対象は同じ制御文字です。

実行時の性質も知っておくと、切り分けが速くなります。既定のbashでは標準エラー出力が標準出力に混ざって差し込まれ、各コマンドには2分のタイムアウトがかかります。作業ディレクトリはセッションのシェルの現在地で、Claudeが cd すると動きます。パスを固定したいコマンドには、skillのディレクトリを指す ${CLAUDE_SKILL_DIR} や、プロジェクトのルートを指す ${CLAUDE_PROJECT_DIR} を使います。

検査の対象になる ! コマンドの範囲

拒否されるかどうかは、ファイルのどこに制御文字があるかでなく、! コマンドの中にあるかで決まります。skillsページから、コマンドとして扱われる書き方は次の2つです。

書き方例備考
インライン例!`git status --short`備考! が行頭か空白の直後にあるときだけ認識される
フェンス付きブロック例```! で開いて閉じる備考複数行のコマンド向け

KEY=!cmd`` のように ! が別の文字の直後にあると、そのプレースホルダーはコマンドとして実行されず、文字のまま残ります。説明はここまでです。この形に制御文字があった場合の扱いは記載がありません。

コマンドの出力は一度だけ差し込まれ、出力の中の ! プレースホルダーは再度展開されません。つまり、コマンドの出力に制御文字が含まれていても、この検査には関係しません。検査されるのは実行前のコマンド文字列です。出力は実行後に差し込まれるからです。

名前に制御文字があるときは別の症状になる

制御文字の問題は、コマンド行だけでなくskillの名前にも起きます。ただし症状は別物です。

skillsページは、フォルダ名やfrontmatterの name にゼロ幅スペースや制御文字があると、Claude Codeがその名前を拒否すると説明しています。拒否された場合のskillは、次のように振る舞います。

  • / メニューに出ない、またはメニューに name の値が出ない
  • フォルダ名やファイル名が原因のときは、skillやカスタムコマンドが読み込まれない
  • プラグインのskillフォルダ名が原因のときは、該当文字がハイフンに変わり、ハイフン入りの名前で呼べる

名前として拒否される文字は、制御文字とゼロ幅スペースだけではありません。skillsページは次のような文字を挙げています。

  • アイコンフォントのグリフのような、私用領域や未割り当ての文字
  • 前後の文字の表示方向を変える文字
  • 単語間の半角スペース1つ以外の空白(no-breakスペース、連続する2つのスペース、先頭や末尾のスペース)
  • < と >

どの名前が拒否されたかは、claude --debug で起動してデバッグログの [names] を検索すると分かります。対話セッションなら、起動時の通知にも「N skill or command names refused」という警告が出ます。名前を直したあとはセッションを再起動します。変更を監視しているディレクトリにあるファイルなら、再起動は要りません。

デバッグログの該当行には、見えない文字が \u{200B}(ゼロ幅スペース)のようなコードで書き出されます。目で探さなくても、どこに入っているかが分かります。行の書式は症状で2通りあります。

ログの行意味
<name> is not offered意味skillの名前が拒否され、読み込まれていない。/ メニューにも出ない
<name value> is not shown for <skill>意味frontmatterの name だけが拒否された。skillは読み込まれるが、name の値では呼べない

名前の長さにも上限があり、256字を超える名前も同じ通知の対象です。

エラー文に No shell command が出るかどうかが切り分けの鍵です。

切り分け

skillが動かないときの切り分け

  1. Q/ メニューにskillが出ているか?
    答えが出ていない

    名前に制御文字や見えない空白が入っていないか確かめます。claude --debug で起動し、デバッグログの [names] を検索すると、拒否された名前が分かります。

    答えがはい
  2. Q呼び出すと `No shell command in ... was run` と出るか?
    答えが出ない

    別の中断です。Shell command failed なら終了コードを、permission check failed なら権限設定を確かめます。

    答えがはい

コマンド行か引数に制御文字がある

最終行の � を手がかりに、ファイルと引数を調べて取り除きます。

「承認ダイアログで隠れる」とは何が問題なのか

注入コマンドは、skillの描画中に承認を求めません。Claude Codeが各コマンドを権限ルールに照らして判定し、許可されなければ呼び出しごと中断します。承認ダイアログが出るのは、Claudeが自分で実行するコマンドの場合です。

拒否理由の「承認ダイアログで隠れる」は、そうした承認画面で人が確かめるコマンド文字列の話と読めます。端末の制御文字には、表示を消したり書き換えたりするものがあります。承認する人の目に映るコマンドと実際に実行されるコマンドがずれる余地を、入口で断つ設計です。

調整の余地について、ページから言えるのは次の点です。

  • 拒否は、コマンドが安全かどうかの判定とは別の、文字種の検査として書かれている
  • 取り除く以外の回避策は、skillsページとエラーリファレンスに載っていない
  • allowed-tools による事前承認との前後関係は、どちらのページにも記載がない

事前承認で通るかを知りたい場合は、無害なコマンドに制御文字を入れた最小のskillで試します。

権限チェックで止まるときの対処

制御文字のエラーとは別に、権限まわりで同じように呼び出し全体が止まるケースがあります。Shell command permission check failed for pattern "..." が出たときの話です。

各コマンドは先に権限ルールに照らされ、結果で扱いが分かれます(権限モードの違いは権限モードの記事にまとめています)。

状況結果
denyルールに一致する結果呼び出し全体を中断
通常のモードで、判定がallow以外(askルールを含む)結果同じエラーで中断
auto modeで、承認が要るコマンド結果中断せず、Claudeに「先にコマンドを実行せよ」と指示する形でskillが読み込まれる
context: fork で agent を指定したforked skill結果auto modeでも中断

どのルールにも当たらないコマンドで止まるなら、frontmatterの allowed-tools で事前承認する方法があります。ただし、denyとaskのルールは allowed-tools より優先されます。denyで止まっているコマンドは、事前承認しても通りません。

コマンドが失敗して止まる場合の注意点も、ページに書かれています。

  • 既定の bash では、終了コードが0以外なら失敗として扱われる
  • grep や diff のような検索・比較コマンドが終了コード1を返すのは、正常な結果として出力が差し込まれる。2以上は失敗になる
  • 終了コード1を返すと分かっているほかのコマンドには、末尾に || true を付ける
  • エラー文には、コマンドの出力が [stderr] の下に含まれる

制御文字のエラーとの違いは、コマンドが実際に走ったかどうかです。権限エラーと失敗エラーは「コマンドを評価した結果」で止まり、制御文字のエラーは「コマンド文字列そのもの」で止まります。そのため、echo ok のような無害なコマンドは前の2つでは通りますが、制御文字が入っていれば3つ目では通りません。

最小の再現ファイルで切り分ける

原因が分からないときは、問題のskillを触らずに、再現用のファイルを別に作ります。

mkdir -p .claude/skills/ctrl-test
printf -- '---\nname: ctrl-test\ndescription: control character test\n---\nout: !`echo a\037b`\n' \
  > .claude/skills/ctrl-test/SKILL.md

\037 は0x1fの制御文字を printf で書き込む指定です。このファイルを呼び出して No shell command in ctrl-test was run が出れば、環境で検査が働いていると分かります。\037 を取り除いたファイルで通ることも合わせて確かめると、切り分けが完了します。確認が済んだら .claude/skills/ctrl-test は削除します。

ファイルを直した後は再読み込みを確認する

直したのに同じエラーが出るなら、古い内容を読んでいる可能性があります。ディスク上のskillの変更を反映するコマンドは、/reload-skillsの記事にまとめています。どのskillが読み込まれているかは、/skillsコマンドの使い方で確かめられます。

skillの変更は、通常は再起動なしで反映されます。Claude Codeがskillのディレクトリを監視していて、ホームの ~/.claude/skills/、プロジェクトの .claude/skills/、--add-dir で渡したディレクトリ内の .claude/skills/ で、追加・編集・削除を同じセッション内で拾うからです。

例外は2つあります。セッションの開始時に存在しなかったトップレベルのskillディレクトリを新しく作った場合は、/reload-skills を実行するまで拾われません。以後の変更のたびに、同じコマンドが要ります。また、bareモードでは監視自体が働きません。再現用の ctrl-test を作り直しても反映されないときは、まずこの2点を疑います。

disableSkillShellExecutionとの関係

disableSkillShellExecution を true にすると、! コマンドは実行されず [shell command execution disabled by policy] に置き換わります。この設定は別の記事で詳しく扱っています。

この設定が有効な環境で、制御文字の検査が先に走るのかは、skillsページにもエラーリファレンスにも記載がありません。設定を入れている環境では、このエラーが出ないほうが自然に見えます。両ページとも、併用時の順序には触れていません。どちらが先に働くかを知りたいときは、最小の再現ファイルで切り分ける節のctrl-testに設定を入れて試します。

もう1つ、claude.aiアカウントから同期されたskillの ! コマンドは、そもそもこのマシンで実行されません。この制限はv2.1.228以降の挙動です。こちらは「実行されない」ので、このエラーの対象とは別の話として切り分けます。

予防: コマンド行を短く、見える文字だけにする

今回のエラーは、長いコマンド行に見えない文字が紛れる設計で起きやすくなります。次の形に寄せると、再発しにくくなります。

  • コマンドは短く保ち、複雑な処理はスクリプトファイルに出して !`${CLAUDE_SKILL_DIR}/scripts/context.sh` のように呼ぶ
  • 貼り付けは、いったんプレーンテキストのエディタを経由する
  • コミット前に、制御文字を探す節のgrepを、skillのディレクトリへ実行する

スクリプトに処理を逃がすと、制御文字の検査対象が短い呼び出し行だけになります。スクリプトの中身は、エディタの拡張機能やlinterで別途見張れます。ただし、この「スクリプト側は検査対象外」という点がページに明記されているわけではありません。検査はあくまで ! コマンドの文字列に対するものと読めます。

まとめ

エラー文の末尾に引用されたコマンドの � が、犯人の位置です。ファイルだけでなく、差し込まれた引数も疑います。1つ混ざるだけでファイル全体のコマンドが止まるので、「一部だけ動かない」状態にはなりません。

直す手順は、cat -v か LC_ALL=C grep で文字を特定し、消して、必要なら再読み込みして呼び直す、の順です。

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