【Claude Code】ブログ画像の機密情報を自動マスクするスキルを作った

概要

今回はブログ記事のスクリーンショットに写り込む API キーや個人情報を、Claude Code で自動マスクするスキルを作ったので紹介していきます。

ブログでスクショを貼るとき、「このアカウント名まずいかも」「API キー写ってない?」って毎回チェックしますよね(- -;
でも記事が増えるとだんだん雑になって、うっかり公開してしまう事故が怖くなってきます。

そこで、Claude Code のスキルとして呼び出せる自動マスクツールを作りました。

スクショ追加 → 「マスクして」と依頼 → Claude が画像を見て機密領域を特定 → Node ツールがグレー塗り+白フチで描画、という流れです。

それではやっていきましょう!

目次

作ったもの

before / after はこんな感じです。
Python スクリプトに API キーを直書きしたスクショを入力すると、キー文字列だけがグレー塗り+白フチでマスクされる挙動になります。

マスク前:SERPAPI_KEY がそのまま露出している状態
マスク前のPythonコード SERPAPI_KEYが露出
マスク後:キー文字列だけを正確に覆い、前後のコード・コメントは残す
マスク後のPythonコード APIキーがグレー塗りでマスクされた状態

SERPAPI_KEY = "..." の文字列部分だけが矩形で覆われて、前後の変数名や末尾のコメントはそのまま読める状態になっていますね。

記事の説明に必要な文脈は壊さず、機密文字列だけを隠すのがポイントです(^^b

なぜ作ったか

手動マスクの限界

スクショ貼る系の記事を書いてると、こんな悩みが出てきます。

<手動マスクの悩み>
  • 毎回ペイントや Figma で矩形を描くのが地味に面倒
  • 記事が増えると抜け漏れチェックが雑になる
  • 過去記事の見直しで「これ残ってた…」となりがち
  • マスクの色・線の太さが記事ごとにバラつく

人間のチェックに頼る運用は、どこかで必ず事故るというのが個人的な感覚でした。

要件整理

なので最初にざっくり要件を決めました。

<要件>
  • 検出は自動(目視だと結局抜ける)
  • 出力は統一感のある見た目(グレー塗り+白フチ)
  • 元画像は上書きで置換(バックアップは残さず、原本の機密情報が git に漏れないようにする)
  • 記事生成フローに組み込んで「記事を書いたら自動で動く」

設計:Claude Vision と Node で役割分担

悩んだのが「何で機密情報を検出するか」のところでした。

選択肢

<検出方式の候補>
  • ・OCR(tesseract.js)+ 正規表現で検出
  • ・Claude Vision に画像を読ませて領域を返してもらう

最初はOCRで考えてたんですが、「タイトルバーに写ってる個人名」「ブラウザ右上のアカウント表示」みたいに正規表現では判別できないケースが地味に多いんですよね。

GCP 管理画面の右上に出るアカウント表示名とか、テストユーザー画面の自分のメールアドレスとか、文脈で判断するしかない領域があります。

ということで Claude Vision 方式を採用しました。
Claude なら「ここ個人情報っぽい」をセマンティックに判定できるので、OCR + 正規表現より漏れが少ないと考えたためです。

デメリット

Claude Vision の座標精度はピクセル完璧ではありません。
なので多少の位置ズレは許容しなければなりません。。。

回避策としてはツール側で領域を自動的に5%外側に広げる処理を入れて、矩形のズレを吸収するようにしました。

構成

役割を Claude とツールで分担しました。

<役割分担>
  • Claude:画像を視覚的に解析して「ここをマスクする」を JSON で出す頭脳担当
  • Node ツール:JSON を受け取ってひたすら矩形を描く手足担当

こうすると、ロジックが単純な Node ツールは壊れにくく、賢さが必要な検出部分は Claude の判断に任せられるので、全体として保守しやすい構造になります(^^

まぁclaude君が必須なのでお金はかかりますが。。。

実装

ファイル構成

<新規追加ファイル>
  • dev_Hexo/tool/mask_image/mask_image.js
    • 本体の Node スクリプト
  • dev_Hexo/tool/mask_image/README.md
    • 使い方メモ
  • .claude/skills/mask-images/SKILL.md
    • Claude Code 用のスキル定義

Hexo プロジェクト側の dev_Hexo/node_modulessharpcanvas が既に入っていたので、個別の package.json は作らず親を参照する方式にしました。

ツール本体

sharp でメタデータ取得と保存、canvas で描画するシンプル構成にしています。
設定は JSON で受け取る形式です。

mask_image.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import sharp from "sharp";
import { createCanvas, loadImage } from "canvas";

const STYLES = {
gray: { fill: "#9CA3AF", stroke: "#FFFFFF" },
white: { fill: "#FFFFFF", stroke: "#9CA3AF" },
};

async function maskOne(imageEntry, style) {
const absPath = imageEntry.path;
const { width, height } = await sharp(absPath).metadata();

// canvas に元画像を描画
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
ctx.drawImage(await loadImage(absPath), 0, 0, width, height);

// 領域ごとに矩形を描画(中心cx,cy + 幅w,h → ピクセル変換、5%パディング)
const { fill, stroke } = STYLES[style];
const border = Math.max(2, Math.round(Math.min(width, height) * 0.003));
for (const r of imageEntry.regions) {
const padX = r.w * width * 0.025;
const padY = r.h * height * 0.025;
const pw = Math.min(width, r.w * width + padX * 2);
const ph = Math.min(height, r.h * height + padY * 2);
// cx, cy は矩形の中心なので、左上は pw/2, ph/2 だけ差し引く
const px = Math.max(0, Math.min(width - pw, r.cx * width - pw / 2));
const py = Math.max(0, Math.min(height - ph, r.cy * height - ph / 2));

ctx.fillStyle = fill;
ctx.fillRect(px, py, pw, ph);
ctx.strokeStyle = stroke;
ctx.lineWidth = border;
ctx.strokeRect(px, py, pw, ph);
}

// 元パスへ上書き保存(バックアップは作らない = 原本の機密情報を漏らさない)
await sharp(canvas.toBuffer("image/png")).toFile(absPath);
}
本ツールは元画像を直接上書きし、バックアップは一切作成しません
もし使用する際は必ず別の場所にバックアップを取ってからやりましょう!!
片道設計にした理由は下に書いてます。
canvas の drawImage は元画像のピクセル比を保ったまま貼り付けるので、リサイズによる画質劣化は発生しません。上書き保存時も sharp 経由で PNG/JPEG を形式ごとに扱っています。

入力 JSON の形式

座標は画像サイズに対する 0〜1 の正規化値で、かつ矩形の「中心」を cx, cy で指定する形式にしています。
こうしておくと、解像度が違う画像でも同じ config が使えるのと、Claude Vision が「このテキストの真ん中はどこ」を素直に書けば、マスクの中心が機密文字列の中心にそのまま乗るメリットがあります。

config.json

1
2
3
4
5
6
7
8
9
10
11
{
"style": "gray",
"images": [
{
"path": "C:/.../posts/20260418_engineering_claude_image_mask/after.png",
"regions": [
{ "cx": 0.460, "cy": 0.220, "w": 0.590, "h": 0.040, "note": "SERPAPI_KEY" }
]
}
]
}
最初は左上起点の (x, y, w, h) で書いていたんですが、Claude Vision は「領域の中心」をそのまま出力するのが自然なんですよね。左上を計算させるとそこで地味にズレが出るので、最初から中心指定にした方がマスクがターゲットにピタッと合うという実地の学びでした(^^
path は絶対パスで指定するのが安全です。Windows 環境でも区切りは `/` を使う方がノーマライズ不要でシンプル。note フィールドは描画には影響しない、デバッグ用のメモ欄です。

Claude スキル化

ここが今回の肝です。
Claude Code のスキル機構を使って、「画像マスクして」と言うだけで自動で動くようにしました。

SKILL.md の役割

.claude/skills/mask-images/SKILL.md には以下を記述しています。

<SKILL.md の内容>
  • いつ発動するか(トリガー)
  • 処理手順(Read で画像閲覧 → 領域特定 → config 書き出し → ツール実行 → 再 Read で検証)
  • 検出対象(メアド、APIキー、OAuth ID、プロジェクトID、個人名、IP、等)
  • マスクしないもの(公開ドメイン、UI ラベル、一般ドキュメント URL)

SKILL.md(冒頭フロントマター)

1
2
3
4
---
name: mask-images
description: ブログ記事に追加したスクリーンショット画像を Claude Vision で解析し、個人情報・APIキー・OAuth ID 等をグレー塗り+白フチで自動マスクする。
---

description にトリガーワードを入れておくと、Claude Code が関連する指示を検知してスキルを自動発動してくれます

「画像マスクして」「機密情報隠して」といった曖昧な依頼でも拾えるように書きました。

記事生成フローへの組み込み

このブログでは dev_Hexo/.github/prompts/article-format.prompt.md に記事作成のルールをまとめているので、ここから mask-images スキルを参照する一文を追加しました。

article-format.prompt.md(追記部分)

1
2
3
4
5
6
7
# スクリーンショット画像のマスキング

記事に画像を追加・更新した際は、個人情報・API キー等の漏洩防止のため
mask-images スキルを発動してください。
- 対象: site/themes/light/source/images/posts/<slug>/ 配下
- スキル定義: .claude/skills/mask-images/SKILL.md
- 実体ツール: dev_Hexo/tool/mask_image/mask_image.js

これで、記事を書く → 画像追加 → 自動的にスキル発動 → マスク済みの状態でコミット、という流れが回るようになりました(^^b

実行フロー

実際の動きはこんな感じです。

<処理ステップ>
  1. ユーザが「記事書いて」や「この画像マスクして」と依頼
  2. Claude が対象フォルダの画像を Glob で列挙
  3. 各画像を Read ツールで閲覧(Vision)
  4. 機密領域を特定し、正規化座標で JSON 化
  5. node mask_image.js config.json を実行
  6. 元画像は上書き(バックアップは作らない/後述)
  7. Claude が結果画像を再 Read して、網羅できているか検証

片道設計にした理由

実は最初は originals/ サブフォルダに原本を退避する実装にしていたんですが、途中で気がつきました。

originals/ に原本が残ると、うっかりそのまま git add . してコミットすると、機密情報つきの原本がそのままリポジトリに乗ります。そもそも機密情報を隠したくて作ったツールなのに、横に原本を置いておいたら意味がない、というジレンマです(- -;

なので元画像を直接上書きして復元不可能にする片道設計に切り替えました。
リカバリの代わりに、「実行前に座標を再確認」「実行後に Read で検証」の 2 ステップをスキル定義に明記する運用で安全性を担保しています。

マスクがズレても、上書き後の画像に対して region を広めに指定して再実行すれば、既存マスクの上から重ね塗りされるので大抵は挽回できます。
ただし過剰マスクで UI ラベルまで隠してしまった場合はスクショを撮り直すしかないので、バックアップは必須ですね。。。

ハマったところ

Node 22 の ESM 判定

最初に import sharp from "sharp" のような ESM 構文で書いたら、こんな警告が出ました。

警告メッセージ

1
2
3
4
MODULE_TYPELESS_PACKAGE_JSON Warning:
Module type of file://.../mask_image.js is not specified
and it doesn't parse as CommonJS.
Reparsing as ES module because module syntax was detected.

Node 22 以降は import 構文から自動で ESM 判定してくれるので動作自体は問題ないんですが、警告が気になる場合は package.json"type": "module" を追加すれば消えます。

今回は既存ツール(imgae_text)と挙動を揃えるために package.json を作らず、警告は許容する判断にしました。

座標のズレ吸収

Claude Vision で返ってくる座標は、ピクセル単位で完璧ではないです。
特に密なスクショだと数ピクセル外すことが普通にあるので、ツール側で自動的に領域を外側 5% 拡張する処理を入れました。

paddingロジック

1
2
3
4
const padX = r.w * width * 0.025;
const padY = r.h * height * 0.025;
const pw = r.w * width + padX * 2;
const px = r.cx * width - pw / 2;

この 5% は何度か試して決めた値で、端の 1〜2 文字がうっかり露出するのを防げるサイズ感でした。

コメント

最初はパディングを入れず「Claude が返した座標をそのまま信じる」実装にしてたんですが、本記事の before/after 画像を作るときにキー先頭の「ek」が 3px はみ出す事故がありました(- -;

自動化でやる以上、こういう「人間なら見逃さないけどモデルはズレるポイント」は仕組みで吸収するしかないと実感。

締め

今回はブログ画像の機密情報を Claude Code のスキルで自動マスクする仕組みを紹介しました。

ポイントをおさらいすると以下のとおりです。

<まとめ>
  • Claude Vision で検出、Node ツールで描画と役割分担
  • 正規化座標 + 5% パディングで解像度・ズレを吸収
  • バックアップなし片道設計で、原本の git 漏洩リスクを根本的に排除
  • SKILL.md + article-format.prompt.md の二段構えで記事生成と連動

「セキュリティは仕組みで担保する」と実感できるツールになって、書いてる側のメンタルもかなり楽になりました。

似たようにスクショ多めのブログを運用してる方は、ぜひ参考にしてみてください!

以上!
機密情報は大事にしましょう!
お疲れさまでした。