◉CLAUDE LABEN
●2.1.296 — サブエージェントごとに autoCompactWindow を指定でき、ワークフロー用のモデルも環境変数で固定できます●AGENTS — Managed Agents の動的ワークフローがベータに。大量の文書を扱う実行を agent が自分で組めます(10/09)●SONNET 4.5 — Claude API での提供終了は 11/30、残り49日。移行先は Sonnet 5.5●Q&A — バグ報告だけ渡すとテストの中だけ直る、という検証が共有されています。頼み方と範囲の決め方が論点です●HAIKU 5.5 — Haiku 4.5 向けコードは budget_tokens が 400 エラーになる点に注意●NEW — つまづき集を毎回全文読ませるのをやめ、番号で一節だけ読む●2.1.296 — サブエージェントごとに autoCompactWindow を指定でき、ワークフロー用のモデルも環境変数で固定できます●AGENTS — Managed Agents の動的ワークフローがベータに。大量の文書を扱う実行を agent が自分で組めます(10/09)●SONNET 4.5 — Claude API での提供終了は 11/30、残り49日。移行先は Sonnet 5.5●Q&A — バグ報告だけ渡すとテストの中だけ直る、という検証が共有されています。頼み方と範囲の決め方が論点です●HAIKU 5.5 — Haiku 4.5 向けコードは budget_tokens が 400 エラーになる点に注意●NEW — つまづき集を毎回全文読ませるのをやめ、番号で一節だけ読む
記事一覧/Claude Code
⟐ Claude Code/2026-08-28初級

名前に混ざった見えない文字を、プラグインを入れる前に洗い出す小さなスクリプト

プラグインやスキルの名前に不可視文字や制御文字が混ざっていても、ターミナルの一覧では見分けがつきません。実際に6つの紛らわしい名前を並べて ls の出力を比べ、導入前に走らせる点検スクリプトと、その点検が拾えない範囲までを整理しました。

claude-code132plugin3security10unicodemarketplace

同じ名前に見えるディレクトリを6つ作って ls に流したとき、私は自分の目をあまり信用しなくなりました。並んだ6行のうち、本当に claude-helper だったのは1行だけです。残りの5行は、幅も文字数もほぼ同じに見えるのに、中身のバイト列が違っていました。

きっかけは 8月26日にリリースされた Claude Code v2.1.247 です。プラグインマーケットプレイスに関する変更として、制御文字や不可視文字を含む名前を拒否するようになったと Claude Code の changelog に書かれていました。読んだときは地味な項目だと思ったのですが、では自分の環境ではどうやって確かめるのか、という問いには答えられませんでした。手を動かして確かめたので、その過程を残しておきます。

個人開発で使うプラグインやスキルは、たいてい自分ひとりで入れて自分ひとりで使います。レビューしてくれる同僚がいない環境ほど、目視以外の確認手段を1つ持っておく価値があると考えています。

一覧では6つの名前がほとんど区別できませんでした

まず、紛らわしい名前を意図的に6つ作りました。すべて私が検証のために自分で作ったもので、どこかから拾ってきた実物ではありません。

種類混入している文字文字数UTF-8 バイト数
素の名前なし1313
ゼロ幅スペースU+200B1315
ゼロ幅接合子U+200D1416
右横書きオーバーライドU+202E / U+202C1822
キリル文字の aU+04301314
ソフトハイフンU+00AD1314

注目していただきたいのは、ゼロ幅スペース入りの名前が素の名前と同じ13文字である点です。文字数を数えても差が出ません。差が出るのはバイト数だけで、13 バイトに対して 15 バイトあります。

ls の出力はこうなりました。行の順序は環境によって変わりますが、見た目の情報量はこれで全部です。

claude-helper
claude-hel[U+200D]per
claude-[U+202E]resu-daer[U+202C]
claude[U+00AD]helper
claude[U+200B]helper
clаude-helper

角括弧で囲んだ部分は、この記事のために私が入れた印です。実際のターミナルには何も表示されません。つまり本物の画面では、6行が縦にほとんど同じ形で並ぶだけです。記事に貼るときですら印を足さないと伝わらない、という事実そのものが、この問題の性質をよく表していると思います。

ここから元の名前を復元するのは、画面を眺めているだけでは無理です。私も無理でした。ところが同じ一覧を cat -v に通すと、状況が一変します。

$ ls -A d | cat -v
claude-helper
claude-helM-bM-^@M-^Mper
claude-M-bM-^@M-.resu-daerM-bM-^@M-,
claudeM-BM--helper
claudeM-bM-^@M-^Khelper
clM-PM-0ude-helper

M-bM-^@M-^K は U+200B の UTF-8 表現(E2 80 8B)です。cat -v は非表示文字をこの記法に置き換えるので、目で見えない差分がそのまま行に現れます。ターミナルで怪しい名前に出くわしたとき、最初に通すコマンドとして覚えておくと便利です。

v2.1.247 が塞いだ範囲と、手元に残る仕事

changelog に書かれている内容を、私なりに整理するとこうなります。

  • マーケットプレイス側で、制御文字や不可視文字を含む名前が拒否されるようになりました
  • /plugin と claude plugin の出力に含まれるマーケットプレイス由来のテキストがエスケープされるようになりました
  • レンダリングされた Markdown のハイパーリンクのうち、ネットワークパスやオートマウンタパスを指すもの、制御文字を含むもの、不可視文字で始まるものはプレーンテキストとして表示されます

つまり、名前そのものと、その名前が画面に表示される経路の両方が塞がれた形です。よくできた対処だと思います。

ただ、これは Claude Code が読み込む段階での防御です。手元のディスクに置かれたディレクトリ名や、自分で書いた設定ファイル、GitHub から git clone してきたリポジトリの中身までを見てくれるわけではありません。私が確かめたかったのはそちらでした。

導入前に走らせる、2つの点検関数

用意したのは短いシェル関数です。.bashrc や .zshrc に貼っておいて、新しいプラグインやスキルを入れた直後に走らせています。

# 制御文字・不可視文字を含む名前を検出する
scan_names() {
  local dir="${1:?usage: scan_names <dir>}"
  local names hits
  names=$(cd "$dir" && ls -A) || return 2
  hits=$(printf '%s\n' "$names" \
    | grep -nP '[\x00-\x1F\x{00AD}\x{200B}-\x{200F}\x{202A}-\x{202E}\x{2060}-\x{206F}\x{FEFF}]')
  if [ -n "$hits" ]; then
    printf '%s\n' "$hits" | cat -v
    echo "NG: 不可視文字または制御文字を含む名前が $(printf '%s\n' "$hits" | wc -l) 件あります"
    return 1
  fi
  echo "OK: 不可視文字・制御文字は見つかりませんでした"
}

対象は、制御文字(U+0000〜U+001F)、ソフトハイフン(U+00AD)、ゼロ幅文字と方向制御文字(U+200B〜U+200F、U+202A〜U+202E)、一般句読点の書式文字(U+2060〜U+206F)、そして BOM(U+FEFF)です。grep -P の \x{...} 記法は PCRE のもので、GNU grep が --perl-regexp 付きでビルドされている必要があります。macOS 標準の grep では使えないので、brew install grep で入れた ggrep に置き換えるか、後述の Python 版を使ってください。

先ほどの6件に当てた結果です。

$ scan_names d
2:claude-helM-bM-^@M-^Mper
3:claude-M-bM-^@M-.resu-daerM-bM-^@M-,
4:claudeM-BM--helper
5:claudeM-bM-^@M-^Khelper
NG: 不可視文字または制御文字を含む名前が 4 件あります
$ echo $?
1

6件のうち4件を拾いました。素の名前が通るのは正しい挙動です。問題は、もう1件が素通りしていることです。

キリル文字の a は、この点検では拾えません

拾えなかったのは clаude-helper です。3文字目の а がラテン文字の a(U+0061)ではなくキリル文字の а(U+0430)になっています。これは不可視文字でも制御文字でもなく、独立した1つの表示可能な文字なので、先ほどのパターンには一致しません。

$ python3 -c "
a='claude-helper'; b='clаude-helper'
print('文字列として一致:', a == b)
import unicodedata
print('NFKC 正規化後に一致:', unicodedata.normalize('NFKC',a) == unicodedata.normalize('NFKC',b))
"
文字列として一致: False
NFKC 正規化後に一致: False

Unicode 正規化をかけても同一にはなりません。NFKC は互換文字を畳み込む正規化ですが、キリル文字とラテン文字は別の文字として設計されているので、当然といえば当然です。混同されやすい文字を網羅的に潰そうとすると、Unicode の混同可能文字表を持ち込む話になり、個人の点検スクリプトとしては重くなります。

そこで私はこの点検の線を、もっと手前に引き直すことにしました。プラグインやスキルの名前は印字可能な ASCII だけで書かれているはず、という前提を置いて、そこから外れたものを一律で報告します。

# 印字可能 ASCII 以外を含む名前を検出する(ホモグリフを含めて拾える)
scan_ascii_only() {
  local dir="${1:?usage: scan_ascii_only <dir>}"
  local hits
  hits=$(cd "$dir" && ls -A | LC_ALL=C grep -n '[^ -~]')
  if [ -n "$hits" ]; then
    printf '%s\n' "$hits" | cat -v
    echo "NG: ASCII 以外の文字を含む名前が $(printf '%s\n' "$hits" | wc -l) 件あります"
    return 1
  fi
  echo "OK: すべて印字可能な ASCII の範囲でした"
}

[^ -~] は、半角スペース(0x20)からチルダ(0x7E)までの範囲に入らない文字、という意味です。LC_ALL=C を付けているのは、ロケールによって文字クラスの解釈が変わるのを避けるためです。

$ scan_ascii_only d
2:claude-helM-bM-^@M-^Mper
3:claude-M-bM-^@M-.resu-daerM-bM-^@M-,
4:claudeM-BM--helper
5:claudeM-bM-^@M-^Khelper
6:clM-PM-0ude-helper
NG: ASCII 以外の文字を含む名前が 5 件あります

今度は5件すべてを拾いました。日本語のディレクトリ名を混ぜている環境では誤検知が増えますが、プラグインやスキルの置き場に限れば、この単純さのほうが実用的だと感じています。判断を細かくするより、線を1本引いて例外を目で確認するほうが、続けられる点検になります。

手元の 906 件に当てて、0件だったこと

書いた以上は自分の環境に当てないと意味がないので、プラグインとスキルの置き場をまとめて走査しました。ディレクトリとファイルを合わせて 906 件、うち最長の名前は 43 文字でした。

結果は、不可視文字・制御文字が 0 件、ASCII 以外の文字を含む名前も 0 件です。

何も出ませんでした。記事にする材料としては拍子抜けする結果で、私自身、少し肩透かしを食らった気分になりました。それでもこの点検を残しているのは、0 件だと確認できている状態と、確認していない状態は別物だからです。前者は次に何かが増えたときに差分として現れますが、後者はいつまでも増えたことに気づけません。

もう一点、find を使う版を最初に書いたのですが、-printf は GNU findutils 固有のオプションで macOS では使えませんでした。上に載せた関数が ls -A を使っているのはそのためです。手元で動いたコードをそのまま配るときに、いちばん踏みやすい種類の段差だと思っています。

次に試すこと

まず、自分のプラグインとスキルの置き場に scan_ascii_only を1回だけ当ててみてください。0件なら、その事実を控えておけば十分です。1件でも出たら、cat -v の行を読んで、その文字が意図して入れたものかどうかを確かめます。それだけで、目視では埋まらない穴が1つ塞がります。

この点検を毎回手で走らせるのが面倒になってきたら、フックに常駐させる段階です。どのタイミングのフックに何を置くかは、Claude Code Hooks — 8種類のフックを使い分ける設計判断と落とし穴 に、8種類それぞれの向き不向きと私が踏んだ落とし穴をまとめてあります。

最後までお読みいただきありがとうございました。小さな確認が積み重なることで安心して手を動かせるようになる、という感覚を共有できていれば嬉しく思います。

シェア

お読みいただきありがとうございます

Claude Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥2,480 の永久アクセス
メンバーシップを見る →

もしこの記事がお役に立ちましたら、チップ(¥150)で応援いただけると大変励みになります。広告なしでの運営を続けるため、皆さまのご支援が大きな力になっています。

関連記事

⟐ Claude Code2026-08-26
キャッシュのTTLを1時間に延ばすかは、離席の長さではなく戻る場所で決めています
Claude Code v2.1.242 で promptCacheTtl と subagentPromptCacheTtl が追加されました。1時間キャッシュが効く条件は離席時間ではなく、同じ作業ディレクトリと同じ git 状態へ戻るかどうかです。判断の順番と、効いているかを確かめる手順を書き残します。
⟐ Claude Code2026-06-12
Android の戻るボタン広告ゲートを入れ子から並列独立構造へ — Claude Code と進めた終了フロー再設計の記録
戻るボタンの広告が「出ない」と「出すぎる」を同時に起こした原因は、入れ子分岐に埋め込まれた暗黙の優先度でした。壁紙アプリ v2.1.0 で広告ゲートを並列独立構造へ作り直した過程を、Kotlin実装・Claude Codeへの依頼内容・テストまで含めて記録します。
⟐ Claude Code2026-05-27
壁紙アプリ 2 本の Crashlytics を Claude Code で 11 日間掘り続けた実装ログ
壁紙アプリ 2 本(綺麗な壁紙 v2.0.0 / 浮世絵壁紙 v1.7.0)でリリース後に Crashlytics と Play Console から噴き出した 30 件超のバグを、Claude Code で 11 日間掘り続けて v2.1.1 / v1.8.1 まで仕上げた工程を、スタックトレース → 原因仮説 → 修正コミットの順で公開します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます