ハングル学習アプリ「推し韓マル」と Web「iffthen hangul」のコードは、ほとんどを AI(Claude)と一緒に作りました。この連載でこれまで書いてきた判定エンジンや配信の仕組みも、そうして作ったものです。進め方は次のとおりです。
- AI はリポジトリに直接触れない。調査や修正のコマンドを AI が書き、人が自分の Mac で実行し、結果を AI に返す
- だから、「AI が何を見るか」と「何を実行させるか」を、決まりで管理する必要があった
1. 調査の出力は、必ずマスキングのフィルターを通す
きっかけは、実際の事故でした。別のプロジェクトで、調査のコマンドの出力(ログ)に外部の API キーがそのまま出て、会話に上がってしまったことがあります。「指示にマスキングしろと書く」だけでは効き目がありませんでした。そこで、機械的に強制する層を作りました。
すべての調査は probe.sh を通します。調査のコマンドは heredoc で渡し、出力はすべてマスキングのフィルターを通ります。
#!/bin/bash
# すべての調査の作業指示は、このスクリプトを経由する。直接の実行の指示は禁止。
set -uo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
MASKER="$SCRIPT_DIR/mask_secrets.py"
[ -f "$MASKER" ] || { echo "[probe] マスキングのフィルターがない = 調査を中止" >&2; exit 1; }
cd "$(dirname "$SCRIPT_DIR")" || exit 1
if [ "${1:-}" = "-c" ]; then BODY="${2:-}"; else BODY="$(cat)"; fi
# 本文を実行 → stdout と stderr をまとめる → マスキングのフィルターを通す
# 本文が失敗しても、出力はマスキングしてから見せる(エラーメッセージの中のキーの露出を防ぐ)
bash -c "$BODY" 2>&1 | python3 "$MASKER"
使う側は、次のように書きます。結果が長ければファイルに保存します。ファイルへの保存もフィルターの後なので安全です。
bash scripts/probe.sh > ~/Desktop/result.txt 2>&1 << 'EOF'
git status --short
cat src/some/file.ts
EOF
マスキングは「値で」が主、「形で」が補助
正規表現で API_KEY=... のような形を探す方法は、キーの名前・順番・URL エンコードが変わると見逃します。そこで、逆から攻めます。
.env・Gradle の設定・Firebase の設定ファイルなど、実際の秘密の値のファイルから値を読んでおく- 出力の文字列の中にその値が出てきたら、必ず隠す(URL エンコードされた形も含む)
- 形での判定は補助にだけ使う。例:URL の中の
user:password@・大文字のKEYの定数に入れた文字列
# 秘密ではないキーの名前(値でのマスキングから外す・名前で判定)
_NON_SECRET_KEY_RE = re.compile(r'(?i)(^|_)(MODEL|PATH|DIR|URL|ENDPOINT|HOST|PORT|LOCALE|LANG|TIMEZONE|TZ|ENV|NODE_ENV)$')
# 設定ファイル(properties)では、秘密らしい名前だけを読む(JVM のオプションの誤検知を避ける)
_SECRET_KEY_RE = re.compile(r'(?i)(PASSWORD|PASSWD|PWD|SECRET|TOKEN|API[_-]?KEY|PRIVATE|CREDENTIAL)')
# URL の中の認証情報(グループ 1 = パスワード)
_URL_CRED_RE = re.compile(r'[A-Za-z][A-Za-z0-9+.\-]*://[^:/\s@]+:([^@\s/]+)@')
# 8 文字未満の値はマスキングしない(普通の単語の誤検知を避ける)
_MIN_SECRET_LEN = 8
補強した例:DATABASE_URL は、名前が _URL で終わるので「秘密ではない」に分類されていました。ところが、値の中にパスワードが入っていました。そこで、URL の認証情報の形を、値での判定と形での判定の両方に足しました。
副作用:マスキングがコードを隠す
const RECENT_KEY = 'hangul.recent' のような普通の定数も、「大文字の KEY の定数」の形に引っかかって '***MASKED***' と見えます。そこで、決まりをもうひとつ置きました。
調査の出力で、ファイルを丸ごと書き直さない。修正はいつも「基準の文字列を見つけて置き換える」部分的なパッチで。
2. 修正は「検証済みのパッチスクリプト」だけで
人がエディタに手で貼り付ける方法は、間違いが多くなります。行が抜けたり、フォーマッターが引用符を変えたり、違う場所に貼ったりします。修正の大きさによって、方法を固定しました。
| 大きさ | 方法 |
|---|---|
| 1 ファイルの 1〜2 か所 | before/after を見せて、人が直す |
| 1 ファイルの何か所も | ファイルを丸ごと入れ替える(cat) |
| 何ファイルも・大きな修正 | パッチスクリプト(tmp_env_test/ に作って実行し、あとで消す) |
パッチスクリプトには、次のものを必ず入れます。
- 事前の点検:置き換える基準の文字列が、ファイルごとにちょうど 1 回出てくるかを確かめる。ひとつでも合わなければ、何も変えずに止まる。すでに当たっている状態なら、2 回目の実行も止まる
- 当てた後の検証:文法のチェック(
node --check・bash -n・py_compile)、変わった数の確認 - 自動での巻き戻し:検証に失敗したら、元の内容に戻す
def rep(p, old, new, label):
n = out[p].count(old)
if n != 1:
bad.append(f'{label}: 基準 {n} 回 (1 回でなければならない)')
return
out[p] = out[p].replace(old, new)
# ... すべての rep の呼び出し ...
if bad:
print('[中止] 事前の点検に失敗 (何も変えていない):')
sys.exit(1)
# 書く → 検証 → 失敗したら元の内容に戻す
そして、いちばん大事な決まりがあります。AI は、そのスクリプトを自分のサンドボックスで先に動かしてから出す。人からもらった実際のファイルの写しに当て、ビルド・テスト・ブラウザでの確認(Playwright)までしてから渡します。この方法で、「動画が見本とずれるバグ」を画面の大きさ 3 種類で再現し、直したあとに位置が一致することを数字で確かめてから配信しました。
スポンサーリンク
スポンサーリンク
3. コマンドはひとまとめにし、結果はファイルで
人が実行する回数が、そのままコストです。調査 → パッチ → ビルド → 検査 → コミット → 配信を、1 回の貼り付けにまとめます。
setopt NO_BANG_HIST PIPE_FAIL
cd <リポジトリ> && { [ -z "$(git status --porcelain -- <対象のフォルダ>)" ] \
&& cat > tmp_env_test/patch.py << 'PYEOF' && python3 tmp_env_test/patch.py && rm tmp_env_test/patch.py \
&& npm run build && npm run check && git add <対象> && git commit -q -m '...' \
&& bash deploy.sh --auto || echo '[中止または失敗] 上の出力を確認'; } 2>&1 | tee ~/Desktop/result.txt
<パッチスクリプトの本文>
PYEOF
コツは次のとおりです。
- zsh の履歴展開を切る(
setopt NO_BANG_HIST)。貼り付けたコードの!(!==など)が前のコマンドに置き換わる事故を防ぎます。heredoc の区切りは、いつも引用符で囲みます(<< 'EOF'・変数の展開を止める) - heredoc の区切りは、本文に出てこない名前にする。本文の中に
EOFの行があると、そこで切れます。パッチはPYEOFのように別の名前を付けます PIPE_FAIL。| teeを使うと、前のコマンドの失敗が隠れます。このオプションで、失敗をそのまま伝えます- 最初の条件 = 作業ツリーの確認。コミットしていない別の変更が混ざらないようにします。範囲は「今回変えるフォルダ」に絞ります。ほかの人の作業が残っていても止まらないようにするためです
- 出力は
teeでファイルに。1 画面を超える結果はファイルで受け取り、AI に添付します
4. 人に判断を求めるときは、表ひとつで
AI が作ったコンテンツ(例:新しい流行語 15 個)は、人が検収します。最初に「検収の結果を教えてください」とだけ書いたところ、「何を検収しろというのか」という反応が返ってきました。今は形を決めています。
| # | 表現 | 韓国語の意味の説明 | 例文 | 画面に出す値 | 判定 |
|---|---|---|---|---|---|
| 28 | 갬성 | 「감성」をわざとひねった言葉・雰囲気のある感じ | 이 카페 완전 갬성 있다. | エモい雰囲気 | OK/修正/除外 |
同じメッセージに、次のことを書きます。
- 何を見るか
- どんな基準で見るか(韓国で実際にその意味で使われているか・例文が自然か)
- どう答えればよいか(「全部 OK」・「26 除外」)
- 確信の低い項目には、勧める案をひとつ
5. つまずいたこと(決まりになった失敗)
- ゲートが自分自身に引っかかった。「i18n のファイルを読み込むほかのファイルがないか」を grep で検査したところ、i18n のファイル自身の頭のコメントにあるファイル名に引っかかって止まりました。ゲートの grep は、対象のファイル自身を外します
- 点検のスクリプトが何を読むかを確かめる。例文の点検はビルドした DB を読むので、元ファイルだけを直して回すと、古い結果が出ました
- エディタのフォーマッターが、作ったファイルを変える。完全一致のパッチは、ファイルを保存したときに書式が変わると失敗します。作ったファイルや設定ファイルは、エディタで保存しません
- コードブロックの中のバッククォート 3 つ。会話の画面のコードブロックがそこで切れ、貼り付けが壊れます。スクリプトの中では避けます
- 「100 %」を疑う。正解データをほぼそのまま写したテストのデータで測った合格率 100 % は、実機ですぐに崩れました(連載の最初の回)
おわりに
- AI が見るのは、マスキングのフィルターを通った後の出力だけ
- AI が出す修正は、サンドボックスで検証した、止まることを知っているパッチスクリプト
- 人がすることは、1 回貼り付け、結果のファイルを返し、判断が必要なものだけを表で判定すること
スポンサーリンク
スポンサーリンク
この記事について
自社のハングル学習アプリ「推し韓マル」と Web「iffthen hangul」を、AI と一緒に開発・運用したときの進め方の記録です。コードは実際のスクリプトから抜き出し、コメントを読みやすく縮めています。秘密の値やローカルのパス、別のプロジェクトの名前は載せていません。
- この方法で作った Web(無料):iffthen hangul
- アプリ:推し韓マル(App Store・Google Play)


コメント