いきさつ
あるデータの改修作業で、変更したい箇所だけが変わっていることを diff で確認していたところ、まったく手を触れていない箇所に「全角スペース → 半角スペース」の差分が出ました。どこに問題があったか原因を調べたところ、GitHub のコピーボタンに行き着きました。
何が起きたか
全角スペースを含む CSV を GitHub の issue のコードブロックに書いておき、別の場所へ移すときにコピーボタンでコピーして貼り付けました。(実際に扱ったのは別の形式のデータですが、説明のため CSV に置き換えています)
id,name
1,サンプル 商品
ところが、貼り付けた先では「サンプル 商品」(半角スペース)になっていました。同じデータに含まれていた全角の括弧「()」やローマ数字「Ⅱ」はそのままで、変わっていたのはスペースだけでした。
起きたことを三行で
- GitHub の Markdown のコードブロック右上にあるコピーボタンでコピーすると、全角スペース(
U+3000)が半角スペースに変わる - 同じコードブロックを範囲選択してコピーすると、全角スペースのまま
- 原因は、コピーボタンの部品 github/clipboard-copy-element に 2026年6月に入った、空白文字を正規化する処理
切り分け
| # | 確認したこと | 結果 |
|---|---|---|
| 1 | issue 本文の元データ(GitHub API で取得) | 全角 |
| 2 | issue の編集履歴(全版) | 全角 |
| 3 | GitHub が返す HTML(コードブロックの表示部分と、コピーボタンの value 属性) |
全角 |
| 4 | 常駐させているクリップボード管理ツールを止めてコピー | 半角(変わらず再現) |
| 5 | コードブロックを範囲選択してコピー | 全角 |
| 6 | コピーボタンでコピー | 半角 |
ブラウザの開発者ツールで見ると、コピーボタンは次のような要素で(一部の属性は省略)、value 属性にはコードブロックの元のテキストが全角スペース入りで入っていました。
<clipboard-copy aria-label="Copy code to clipboard" class="ClipboardButton btn js-clipboard-copy m-2 p-0" value="id,name
1,サンプル 商品">
value は全角なのに、ボタンを押した後のクリップボードは半角です。つまり、ボタンの処理の中で変換されています。
原因
<clipboard-copy> は GitHub が公開している github/clipboard-copy-element です。PR #88 がマージされた時点(npm では v1.3.2 として公開)の src/clipboard.ts に次の処理があります。
// Normalize characters that render as a space (or as nothing) so that copied
// text matches what the user sees.
function normalizeText(text: string): string {
return text
.replace(/[\u00A0\u1680\u2000-\u200A\u202F\u205F\u3000]/g, ' ')
.replace(/[\u200B-\u200D\u2060\uFEFF\u180E]/g, '')
}
export function copyText(text: string): Promise<void> {
const normalized = normalizeText(text)
if ('clipboard' in navigator) {
return navigator.clipboard.writeText(normalized)
}
// 省略
}
ボタンを押すと value 属性の値が copyText に渡され、normalizeText で次の文字が置き換えられてからクリップボードに書き込まれます。
| 処理 | 対象の文字 |
|---|---|
| 半角スペースに置き換え | U+00A0(ノーブレークスペース)、U+1680、U+2000〜U+200A、U+202F、U+205F、U+3000(全角スペース) |
| 削除 | U+200B〜U+200D(ゼロ幅スペースなど)、U+2060、U+FEFF(BOM)、U+180E |
github.com で配信されているスクリプトにも同じ正規表現が入っていることを確認しました(2026年10月13日時点)。
範囲選択でのコピーはブラウザ自身がテキストを取り出すので、この処理を通りません。コピーの方法で結果が変わったのはこのためです。
なぜこの処理があるのか
発端はセキュリティ対策で、その後「見た目との一致」を目的に対象が広げられました。
| 日付(UTC) | 変更 |
|---|---|
| 2026年6月15日 | PR #87(v1.3.1)で、ノーブレークスペース(U+00A0)だけを半角スペースに置き換える処理を追加(コミット)。コードのコメントでは、見た目は普通のスペースと同じ文字を使ってシェルコマンドなどに悪意のある内容を隠せてしまうことへの対策と説明され、HackerOne の報告が参照されている |
| 2026年6月16日 | PR #88(v1.3.2)で、スペースに見える文字と見えない文字全般に対象を拡大(コミット)。目的は、コピーした文字を画面の見た目と一致させること |
U+00A0 のように普通のスペースと見分けがつかない文字への対策としては、妥当な処理だと思います。
ただ、全角スペースは事情が違います。全角スペースは、特に等幅フォントのコードブロックでは半角スペースより明らかに広く表示されます。画面では全角に見えているのに、コピーすると半角になるので、この処理が掲げる「コピー結果を見た目と一致させる」という目的に対して、全角スペースについてはかえって見た目と一致しなくなっています。
しかも画面上は全角のままなので、コピーした時点で変わっていることには普通気づけません。今回の私のように、データの照合やテストの期待値が知らないうちに変わる、といった形で後から表面化します。
この点について、upstream に issue を立てました:github/clipboard-copy-element#94
全角スペースがコピーできているかの確かめ方
貼り付け先のアプリの影響を受けずに確認するには、クリップボードを直接調べるのが確実です。
Windows(PowerShell)
(Get-Clipboard -Raw).Contains([char]0x3000)
# True: 全角スペースあり / False: なし
OS を問わない方法(ブラウザの開発者ツールのコンソール)
navigator.clipboard.readText().then(t => console.log(t.includes('\u3000')))
// true: 全角スペースあり / false: なし
対策
- 全角スペースやゼロ幅文字を含むテキストは、コピーボタンを使わず範囲選択でコピーする
- 貼り付ける前に、「確かめ方」の方法でクリップボードに全角スペースが残っているか確認する
- 貼り付け先がエスケープを解釈できる形式なら、全角スペースをエスケープで書いておく。エスケープなら、コピーの方法にかかわらず全角スペースになる
| 形式 | 全角スペースの書き方 | 例 |
|---|---|---|
| JSON | \u3000 |
{"name": "サンプル\u3000商品"} |
| JavaScript / Python の文字列 | \u3000 |
"サンプル\u3000商品" |
| HTML |   |
サンプル 商品 |
CSV のようにエスケープの仕組みがない形式では、範囲選択でコピーし、貼り付ける前にクリップボードを確認するしかありません。
おわりに
煽りタイトルかなとは思ったものの、注意喚起はしたい内容です。犯人がまさかのGithubで本当にびっくりしました。
似たような記事が見つからなかったです。ただ、気づかれていないだけか全角スペースを扱うことなんて基本ないからなのか。少なからずあるとは思うのですが。
日本人からするとしっくり来ない挙動になっているので、いつかこの記事が古い情報ですと指摘や修正できることを期待しています。
同じことで困るなって方は、issue に 👍 やコメントをいただけると修正につながりやすくなると思います。
