はじめに:AIは頼んでいないのに絵文字を入れてくる
Claude Code(Anthropic製のCLI統合AIコーディングアシスタント)を使ってウェブ媒体の記事やコンポーネントを書かせると、頼んでいないのに絵文字が混入することがある。
例えば手順を書かせると、文末にチェックマークの絵文字が付く。見出しに警告マークの絵文字、比較表の行末に星マークの絵文字、といった具合だ。こちらが「絵文字を使わないでください」と言っても、セッションをまたぐと忘れて再発する。
なお、この記事自体も「絵文字を使わない」という方針で書いている。そのため、引用元のコメントや文字列に含まれる絵文字グリフは、本文では「(絵文字)」や文章表現に置き換えて引用している点をあらかじめ断っておく。
これが実際の運用で困る理由は複数ある。
トーンが崩れる。 テキスト主体の媒体で突然絵文字が現れると、編集方針の一貫性が損なわれる。特にお金や健康など「YMYL(Your Money or Your Life)」領域の記事では、ビジュアル的な煽り表現は読者の信頼感を下げる。
広告ネットワークの媒体審査に影響する。 アフィリエイトプログラムの中には「誇張表現の禁止」を媒体規約に明記しているものがある。絵文字そのものが直接引っかかるかどうかはケースバイケースだが、審査担当者の印象管理として避けた方が無難だ。
人手レビューは確実でない。 「絵文字を見かけたら指摘する」という運用は、疲れているときや量が多いときに必ずすり抜ける。そもそも生成物を人間が全文目視するコストは、AIを使う意義と矛盾する。
解決策として「毎回プロンプトに書く」「セッション冒頭で禁止を宣言する」という手もある。しかし実際のところ、AIは長いコンテキストの中でルールを忘れる。根本的な対策は、AIが絵文字を書いたとしても、それがリポジトリやサーバーに到達する前に機械的に止める仕組みを作ることだ。
解決方針:3層のゲートで機械的にブロックする
人手に頼らない多層防御の考え方は、情報セキュリティの基本だ。これを絵文字対策に応用する。
コミット時 → pre-commit フック(.githooks/pre-commit)
レビュー時 → npm run review / npm run preflight の先頭で実行
デプロイ時 → deploy.ps1 の公開ゲートで実行
各層の役割と「なぜそこに置くか」はあとのセクションで詳しく説明する。
チェックロジックは1本のNode.jsスクリプト(scripts/check-emoji.mjs)に集約し、3層すべてから呼び出す。ロジックが分散していると、片方を直したときにもう片方が古いままになるからだ。
検出スクリプト(scripts/check-emoji.mjs)の中身
走査対象と方針
スクリプトの冒頭コメントに方針が書いてある(抜粋)。
/**
* 装飾絵文字の混入を検出して必ずブロックするゲート(媒体トーン統一・「二度と使わない」担保)。
*
* npm run check:emoji
*
* 方針:
* - チェックマークや炎などの装飾絵文字・ピクトグラムを、記事本文・コンポーネント・ページの
* 「画面に表示される箇所」で全面禁止する。draft の状態に関係なく1つでもあれば終了コード1。
* - 比較表で使う ○ △ ◎ × や矢印 → は Unicode 上この範囲外なので検出されない(誤検知しない)。
* - .astro / .ts / .mjs はコード扱いなので、コメント(// , 行頭 *, JSDoc)に書かれた絵文字は
* 画面に出ないため除外する。記事(.mdx)は全文を対象にする。
*/
走査対象ディレクトリは src 配下の表示物に絞っている。
const TARGET_DIRS = ['src/content/articles', 'src/components', 'src/layouts', 'src/pages'];
src/content/articles が記事MDXファイルの置き場、残り3つがAstroコンポーネントとページのディレクトリだ。node_modules や scripts 自体は対象外なので、このスクリプトのコメント内にある絵文字例が自己検出で引っかかることはない。
絵文字の判定範囲
const EMOJI_RE =
/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{1F000}-\u{1F2FF}]\u{FE0F}?/gu;
Unicodeの絵文字ブロックを4つのレンジで指定している。
| レンジ | 代表的な内容 |
|---|---|
| U+1F300 - U+1FAFF | 天気・動物・食べ物・旗・記号系の絵文字 |
| U+2600 - U+27BF | 各種記号(傘・星・チェックマーク等) |
| U+2B00 - U+2BFF | 矢印・幾何学的記号 |
| U+1F000 - U+1F2FF | 麻雀・トランプ・囲碁等 |
末尾の \u{FE0F}? は絵文字バリエーションセレクター(VS16)のオプション指定だ。絵文字の後ろにこの制御文字が続く場合も1件として数える。
重要な点は、比較表でよく使う ○ △ ◎ × や矢印 → はこのレンジに含まれないこと。これらは記号として検出されない。絵文字を禁止しつつ、記号による表組みを壊さないための設計だ。
コメント除去ロジック
.astro・.ts・.mjs などのコードファイルは、コメント行に書かれた絵文字を画面には出力しない。そのため、コードファイルではコメントを除去してから走査する。記事(.mdx)はコメントを除去しない(MDXコメント {/* ... */} 内の絵文字も念のため検出する)。
function stripComments(text) {
return text
.replace(/\/\*[\s\S]*?\*\//g, '') // ブロックコメント /* ... */
.replace(/(^|[^:])\/\/[^\n]*/g, '$1'); // 行コメント // ...(http:// を誤除去しないよう直前が : 以外)
}
http:// などのURLが行コメントとして誤除去されないよう、直前が : でない場合のみ // をコメントとみなす工夫がある。
ファイルごとに .mdx かどうかを判定して適用する。
const scanned = extname(file) === '.mdx' ? raw : stripComments(raw);
検出結果の出力と終了コード
絵文字が1件でも見つかれば、file:line 形式で全件を出力してから process.exit(1) で終了する。
if (hits.length === 0) {
console.log('[PASS] 装飾絵文字は見つかりませんでした。');
process.exit(0);
}
console.log(`\n[STOP] 装飾絵文字が ${hits.length} 件見つかりました(チェックマーク等の絵文字は使わない)。`);
console.log(' → ○ △ × や 【NG】【OK】などの記号・文字に置き換えてください。\n');
for (const h of hits) {
console.log(` ${h.file}:${h.line} [${h.emojis}] ${h.text}`);
}
process.exit(1);
出力例(絵文字グリフは本記事の方針により文章表現に置き換えている):
[STOP] 装飾絵文字が 2 件見つかりました(チェックマーク等の絵文字は使わない)。
→ ○ △ × や 【NG】【OK】などの記号・文字に置き換えてください。
src/content/articles/some-article.mdx:42 [(絵文字)] 手順が完了したら次へ進みます (チェックマークの絵文字)
src/components/SomeCard.astro:18 [(絵文字)] 人気の機能 (炎の絵文字)
開発者はこの出力を見て、該当行を ○ や 【OK】 などの文字表現に修正すればよい。
3層の組み込み
第1層:pre-commitフック
.githooks/pre-commit の全内容は以下のとおり。
#!/bin/sh
# 装飾絵文字(チェックマークや炎などの絵文字)の混入をコミット前にブロックする。
# 有効化: git config core.hooksPath .githooks (クローン直後に一度だけ実行)
# CLAUDE.md「表記・トーンのルール」を機械的に担保するゲート。
node scripts/check-emoji.mjs
if [ $? -ne 0 ]; then
echo ""
echo "コミットを中止しました。上記の装飾絵文字を ○ △ × や 【NG】【OK】 などに置き換えてください。"
echo "(どうしても通す必要がある場合のみ: git commit --no-verify)"
exit 1
fi
なぜここに置くか。 コミット前がコードの流れの最も早い段階だ。ここでブロックすれば、絵文字入りのコードがリポジトリ履歴に載らない。「後で直せばいい」の先送りがそもそも起きない。
有効化は1回だけ。 Gitはデフォルトで .git/hooks/ を見るため、リポジトリに .githooks/ を置いただけでは動かない。クローン後に1回だけ次のコマンドを実行する必要がある。
git config core.hooksPath .githooks
このコマンドはグローバルではなくリポジトリローカルな設定なので、他のリポジトリには影響しない。
緊急回避。 どうしても絵文字を含めたままコミットしたい特殊事情がある場合(例:絵文字の扱いを議論するIssueの関連コミット等)は git commit --no-verify で回避できる。フックのコメントにも記載してある。ただし多用しないこと。
第2層:npm run review / npm run preflight
package.json の scripts セクション(抜粋)。
{
"review": "node scripts/check-emoji.mjs && node scripts/check-articles.mjs",
"preflight": "node scripts/check-emoji.mjs && node scripts/check-articles.mjs && node scripts/check-freshness.mjs"
}
&& でチェーンしているため、絵文字チェックが失敗した時点で後続のチェックは走らない。「絵文字が残っているうちはレビューが完了しない」状態になる。
なぜここに置くか。 記事をドラフトから公開状態(draft: false)に切り替える前に必ず npm run review を実行するワークフローになっている。この段階に絵文字チェックを組み込むことで、pre-commitをすり抜けたケース(例:他ツールから直接ファイルが生成された、フックの有効化を忘れていた)をカバーできる。
preflight はさらに鮮度チェック(情報の古さ検証)も含む、より厳格なコマンドだ。デプロイ直前の最終確認に使う。
第3層:デプロイスクリプト(deploy.ps1)
scripts/deploy.ps1 の公開ゲート部分(抜粋)。
# --- 1.5. 公開ゲート: コンプライアンスチェック ---
if (-not $SkipChecks) {
# 装飾絵文字ゲート(装飾絵文字の混入を本番反映前に必ずブロック)
Write-Log "公開ゲート: node scripts/check-emoji.mjs"
if (-not $DryRun) {
& node (Join-Path $ScriptDir 'check-emoji.mjs')
if ($LASTEXITCODE -ne 0) {
throw "装飾絵文字を検出しました。○△×や【NG】【OK】などに置き換えてください(緊急回避は -SkipChecks)。"
}
}
}
なぜここに置くか。 第1層・第2層をすり抜けた場合の最終防波堤だ。どんな理由であれ、絵文字入りのコンテンツが本番サーバーに公開されることを物理的に防ぐ。throw で例外を投げるため、デプロイスクリプト全体が中断される。
-SkipChecks フラグを指定した場合のみ回避できる。緊急デプロイ時のエスケープハッチとして存在するが、使用する際はチームへの説明責任が生じる設計になっている。
3層が必要な理由
「1層でいいのでは?」という疑問が出るかもしれない。実際にはそうならない。
pre-commitフックは core.hooksPath を設定しないと動かない。新メンバーがセットアップ手順を飛ばすと無効になる。また、CIやスクリプトからファイルを直接生成してコミットする自動化フローでは --no-verify が使われることもある。
npm run review は開発者が意識的に実行しなければ走らない。疲れているとき、急いでいるときにスキップされる可能性がある。
デプロイゲートは強力だが、「本番デプロイ時にはじめてエラーになる」状況はフィードバックループが長すぎる。コミット直後にわかる方がはるかに修正コストが低い。
3層を重ねることで、「どこか1層が抜けても別の層が止める」という冗長性が生まれる。これはセキュリティの多層防御(Defense in Depth)と同じ考え方だ。
読者の環境への導入手順
既存のNode.jsプロジェクトに最小構成で入れる手順を示す。
1. 検出スクリプトを配置する
scripts/check-emoji.mjs を作成し、以下の内容を書く(実際の実装は冒頭で説明したとおり)。
import { readdir, readFile } from 'node:fs/promises';
import { fileURLToPath } from 'node:url';
import { dirname, join, relative, extname } from 'node:path';
const __dirname = dirname(fileURLToPath(import.meta.url));
const ROOT = join(__dirname, '..');
// 走査対象ディレクトリ(環境に合わせて変更する)
const TARGET_DIRS = ['src'];
const EMOJI_RE =
/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}\u{2B00}-\u{2BFF}\u{1F000}-\u{1F2FF}]\u{FE0F}?/gu;
function stripComments(text) {
return text
.replace(/\/\*[\s\S]*?\*\//g, '')
.replace(/(^|[^:])\/\/[^\n]*/g, '$1');
}
async function walk(dir) {
const out = [];
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return out;
}
for (const e of entries) {
const p = join(dir, e.name);
if (e.isDirectory()) out.push(...(await walk(p)));
else if (/\.(mdx|astro|ts|tsx|js|mjs)$/.test(e.name)) out.push(p);
}
return out;
}
const hits = [];
for (const d of TARGET_DIRS) {
for (const file of await walk(join(ROOT, d))) {
const raw = await readFile(file, 'utf8');
const scanned = extname(file) === '.mdx' ? raw : stripComments(raw);
scanned.split('\n').forEach((line, i) => {
const m = line.match(EMOJI_RE);
if (m) {
hits.push({
file: relative(ROOT, file).replace(/\\/g, '/'),
line: i + 1,
emojis: [...new Set(m)].join(' '),
text: line.trim().slice(0, 70),
});
}
});
}
}
if (hits.length === 0) {
console.log('[PASS] 装飾絵文字は見つかりませんでした。');
process.exit(0);
}
console.log(`\n[STOP] 装飾絵文字が ${hits.length} 件見つかりました。`);
console.log(' → ○ △ × や 【NG】【OK】などに置き換えてください。\n');
for (const h of hits) {
console.log(` ${h.file}:${h.line} [${h.emojis}] ${h.text}`);
}
process.exit(1);
走査対象の TARGET_DIRS は自分の環境に合わせて変える。Astro以外のフレームワークなら src/app、pages、components など。
2. pre-commitフックを配置する
.githooks/pre-commit を作成する(Windowsでも .githooks というディレクトリ名で動く)。
#!/bin/sh
node scripts/check-emoji.mjs
if [ $? -ne 0 ]; then
echo "コミットを中止しました。絵文字を記号・文字表現に置き換えてください。"
exit 1
fi
作成後、フックに実行権限を付与する(macOS / Linux)。
chmod +x .githooks/pre-commit
Windowsのgit-bash環境では chmod 不要のことが多いが、WSL経由の場合は必要になる。
3. フックを有効化する(1回だけ)
git config core.hooksPath .githooks
チームで使う場合は、README.md や CONTRIBUTING.md のセットアップ手順にこのコマンドを記載しておく。
4. package.json にスクリプトを登録する
{
"scripts": {
"check:emoji": "node scripts/check-emoji.mjs"
}
}
npm run check:emoji で単体実行できるようにしておく。レビューコマンドがある場合はその先頭に組み込む。
{
"scripts": {
"check:emoji": "node scripts/check-emoji.mjs",
"review": "node scripts/check-emoji.mjs && node scripts/your-other-check.mjs"
}
}
5. 動作確認
テスト用に任意のファイルに絵文字を1文字書いてから npm run check:emoji を実行し、[STOP] が出て終了コード1になることを確認する。確認後に絵文字を削除して [PASS] になることを確認する。
まとめ
AIが書くコードや文章への「ルール遵守の強制」は、プロンプトではなく機械的なゲートで担保する方が確実だ。
今回実装した3層ゲートの設計原則は次のとおり。
- チェックロジックは1本のスクリプトに集約し、複数の層から呼び出す
- 各層は「なぜそこでブロックするか」の文脈が異なる(コミット履歴を汚さない / ドラフト公開前に止める / 本番到達前の最終防波堤)
- どこか1層が無効化されても別の層が止める冗長性を持つ
- エスケープハッチ(
--no-verify、-SkipChecks)は存在させるが、意図的な操作が必要な形にする
AI生成コンテンツを運用に組み込む場合、「AIが守ってくれる」ことに依存せず「守れなかったときに止まる仕組み」を先に作ることが重要だ。絵文字の例は小さく見えるかもしれないが、同じ仕組みでスパム的なキーワードの混入検出、外部リンクのドメイン制限、特定表現の禁止など幅広い用途に応用できる。
参考
- Node.js 公式ドキュメント —
node:fs/promises - Git 公式ドキュメント — githooks
- Unicode Emoji Chart — https://unicode.org/emoji/charts/full-emoji-list.html
関連記事
同じく「AIに任せた作業の品質を機械で担保する」テーマで書いた記事です。