0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

AIの自己改善メモをWikiに出す前に止める ― secretスキャン付き同期ゲート

0
Posted at

前回、監視スクリプトにrootを1コマンドだけ渡す話を書きました。今回は同じ「無人自動化に何を渡すか」の話ですが、対象が変わります ―― Claude Code自身が書き溜める監査・改善プランを、人間用のObsidian Wikiに流し込む配管です。

困りごと:AIが書いた監査メモは、会話ログとは別の危険を持っている

~/.claude/improvements/ には、Claude Codeが自律運用の中で書いた監査レポートや改善プランが溜まっています。中身を見ると、こういう記述があります(audit-2026-05-29.md から抜粋)。

| project | 状態 | 推奨アクション |
|---|---|---|
| **seo-affiliate-site** 🔴 | 63 files 未コミット(Like / コメント / AdSense 等のマネタイズ実装が宙吊り) | 機能単位で分割コミット |
| closet-os 🟢 | clean / 直近活発 | `.env.production.local` 1.9KB は gitignore 済だがバックアップ運用注意 |

これは個人プロジェクト名・実装の進捗・.envファイルの存在まで具体的に書いてあります。会話ログを長期記憶に変えるパイプラインとは別に、AI自身が生成した文書そのものに秘密や個人情報が混入しうるというリスクがここにはあります。デバッグ中に貼ったAPIキーの断片が監査メモに引用されたまま残る、というのは十分ありうる事故です。

このメモをそのままWikiに流し込む前に、機械的に止める仕組みが要りました。それが wiki-sync-improvements.sh です。

設計:対象を絞り、secretで全停止し、既定はdry

スクリプトの冒頭のコメントに、設計の全体像が要約されています。

# wiki-sync-improvements.sh
# ~/.claude/improvements/ の個別 plan/audit/curation docs を
# ~/Documents/claude-obsidian/wiki/meta/improvements/ に同期する。
#
# 対象: audit-*.md / *-plan-*.md / *-curation-*.md
# 除外: log.md (巨大), README.md, next-session-todo.md など
# 秘密スキャン: sk-* / ghp_* が含まれていれば sync 中止
#
# 使い方:
#   wiki-sync-improvements.sh dry       # 変更プレビューのみ
#   wiki-sync-improvements.sh apply     # 実際に書き込む

log.md を除外している理由は数字で確認できます。手元で見ると2316行・約228KBあり、これは時系列ログであってWikiのノート単位には向きません。

対象ファイルの絞り込みはシェルの case パターンです。

for f in "$SRC"/*.md; do
  base="$(basename "$f")"
  case "$base" in
    audit-*.md|*-plan-*.md|*-curation-*.md)
      candidates+=("$f")
      ;;
  esac
done

secretスキャンは、候補全部を先に一括で見てから止めるかどうかを決めます。

SECRET_RE='(sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{20,})'
secret_hits=()
for f in "${candidates[@]}"; do
  if grep -E -q "$SECRET_RE" "$f"; then
    secret_hits+=("$f")
  fi
done
if [[ ${#secret_hits[@]} -gt 0 ]]; then
  echo "[abort] secrets detected — sync stopped" >&2
  for h in "${secret_hits[@]}"; do echo "  - $h" >&2; done
  exit 2
fi

ここは「秘密が見つかったファイルだけ弾く」のではなく、1件でも引っかかったら候補全部を止める設計です。部分同期を許すと「secretを含む1件だけスキップして残りは通した」判断をスクリプト側でしなければならず、その判定ミスが一番怖い。だから閾値は「全部止める」の一択にしています。

書き込みの既定はdryです。MODE="${1:-dry}" になっていて、引数なしで実行すると何も書かず、プレビューだけが出ます。apply時は、出力先に同名ファイルが既にあれば内容をハッシュ相当(cmp -s)で比較し、一致すればskip、差分があればタイムスタンプ付きの別ファイルとして書き出す ―― 上書きではなく併存です。

if cmp -s "$tmp" "$out"; then
  rm -f "$tmp"
  skipped=$((skipped+1))
  continue
fi
stamped="$DST/${base%.md}.$(ts_suffix).md"
mv "$tmp" "$stamped"

実際に動かしてみる

手元で dry を叩くと、こう出ました。

$ ~/.claude/scripts/wiki-sync-improvements.sh dry
[dry] would write timestamped: audit-2026-05-29.md -> meta/improvements/audit-2026-05-29.<ts>.md
[dry] would write timestamped: plugin-curation-2026-05-30.md -> meta/improvements/plugin-curation-2026-05-30.<ts>.md
[dry] would write timestamped: seo-affiliate-commit-plan-2026-05-30.md -> meta/improvements/seo-affiliate-commit-plan-2026-05-30.<ts>.md
[dry] would write timestamped: wiki-cleanup-plan-2026-05-29.md -> meta/improvements/wiki-cleanup-plan-2026-05-29.<ts>.md
---
[done] planned=4 total_candidates=4 (dry run; no writes)

~/.claude/improvements/ には実際には12個の.mdがあります。フィルタを通ったのはこの4件だけで、残り8件(log.md / README.md / next-session-todo.md / commands-consolidation-plan.md など)は対象外です。

ここで気づいたのが commands-consolidation-plan.md の扱いです。ファイル名を見ると「plan」を含むので対象に入りそうですが、実際は候補に出てきません。理由はパターンです。

*-plan-*.md

これは「-plan-後にもう1文字以上あってから .md」という意味なので、xxx-plan.md(plan の直後がドット)は一致しません。xxx-plan-2026-05-30.md のように日付サフィックスが付いていれば拾われますが、サフィックスを付け忘れた改善プランは静かに対象から漏れます。エラーも警告も出ません。

運用の生々しい現在地:宛先を検証しようとしたら詰まった

このスキルのfrontmatterには status: stale と書かれています。

status: stale

Curatorの基準(30日未使用でstaleに降格)に照らすと、しばらく実運用で回っていないということです。それなら宛先ディレクトリの中身を見て「apply が実際どこまで進んでいるか」を確認しようとしたのですが、ここでつまずきました。

$ ls ~/Documents/claude-obsidian/wiki/meta/improvements/
ls: .../meta/improvements/: Interrupted system call
total 0

lsfind もGlobツールも、同じ Interrupted system call(EINTR)を返し続けて中身を列挙できません。一段上の meta/ ディレクトリでも同じ症状が出ました。一方で、個別ファイルの存在チェックだけは通ります。

$ [ -e .../meta/improvements/audit-2026-05-29.md ] && echo exists
exists

ところがその同じファイルを cat すると、応答がないまま600秒でタイムアウトしました。iCloud同期下のディレクトリで、ファイルのメタデータだけ存在し実体が未ダウンロード(placeholder)のまま止まっている状態に近い挙動です。断定はできませんが、事実として「4件が存在する扱いにはなっているが、内容を読み出す経路がこのセッションからは機能していない」というのが今の現在地です。

つまり、secretスキャンと全停止ロジックそのものは読めば健全に見える一方で、「本当にWikiに正しく届いているか」を後から検証する経路が細く、壊れると気づきにくいというのが、動かしてみて分かった現実です。安全ゲートを書くことと、そのゲートの出力を継続的に検証できることは別問題でした。

踏んだ落とし穴

  • log.md(2316行/約228KB)はそのまま対象に入れるとWikiのノート単位を壊す → ファイル名パターンで明示除外
  • secretが1件でも見つかったら候補全部を止める → 部分同期の判断をスクリプトに持たせない設計
  • *-plan-*.md パターンは xxx-plan.md(サフィックスなし)を一致させない → 日付サフィックスを付け忘れた改善プランが警告なしで対象から漏れる
  • dry modeは「宛先に同名ファイルがあるか」しか見ず、中身の差分は見ない[dry] would write timestamped は「差分がある」ではなく「同名ファイルの有無」の判定
  • 宛先ディレクトリの実体検証がiCloud同期の影響でEINTRに詰まる → ゲート自体とは別に、出力の到達確認を独立した経路(ハッシュ台帳など)で持つ必要がある

まとめ

  • AIが自分で書く監査・改善メモには、会話ログとは別の経路で秘密や個人情報が混ざりうる
  • secretスキャンは「引っかかったファイルだけ弾く」より「1件でも出たら全停止」の方が判断ミスを減らせる
  • 対象ファイルのフィルタはシェルパターン頼みだと静かに漏れが出る。命名規則の揺れは事故の温床
  • 既定dry・apply時はハッシュ比較でskip、差分は上書きせずタイムスタンプ併存という安全側の設計は保てている
  • ただし「ゲートを通ったものが本当に届いたか」を検証する経路は別に要る。今回はそこがまだ壊れたままだった

次回は、この宛先到達をゲートの外側からハッシュ台帳で検証できるようにする話を予定しています。


Lily@bokuwalily)― 個人開発者。Claude Code で自動化基盤を組みながら、iOSアプリやWebサービスを量産しています

皆さんの ❤️ やシェアが励みになります!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?