Last time I wrote about handing a monitoring script exactly one root command. This post is the same question ― what do you let unattended automation touch? ― aimed at a different target: the plumbing that carries audit reports and improvement plans written by Claude Code itself into my human-facing Obsidian wiki. The gate I ended up with takes 12 Markdown files as input, lets 4 through, and refuses to write anything at all if a single one of them contains something that looks like an API key. The problem: AI-written audit memos carry a different risk than chat logs ~/.claude/improvements/ accumulates audit reports and improvement plans that Claude Code writes during autonomous operation. Look inside and you find entries like this (excerpt from audit-2026-05-29.md): | project | 状態 | 推奨アクション | |---|---|---| | **seo-affiliate-site** 🔴 | 63 files 未コミット(Like / コメント / AdSense 等のマネタイズ実装が宙吊り) | 機能単位で分割コミット | | closet-os 🟢 | clean / 直近活発 | `.env.production.local` 1.9KB は gitignore 済だがバックアップ運用注意 | That's personal project names, implementation progress, and even the existence of .env files, all spelled out. Separate from the pipeline that turns conversation logs into long-term memory, there is a distinct risk here: documents the AI generates on its own can carry secrets and personal information. A fragment of an API key pasted during debugging ending up quoted in an audit memo is an entirely plausible accident. I needed something mechanical that would stop these memos before they got poured into the wiki. That's wiki-sync-improvements.sh. Design: narrow the targets, halt everything on a secret, default to dry The comment block at the top of the script summarizes the whole design. # 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 # 実際に書き込む The reason log.md is excluded is visible in the numbers. On my machine it's 2,316 lines and roughly 228 KB ― a chronological log, not something that fits the wiki's one-note-per-file granularity. Target filtering is a shell case pattern. for f in "$SRC"/*.md; do base="$(basename "$f")" case "$base" in audit-*.md|*-plan-*.md|*-curation-*.md) candidates+=("$f") ;; esac done The secret scan looks at every candidate first, and only then decides whether to stop. 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 Note: This is deliberately not "drop only the files that contain a secret." A single hit halts every candidate. Allowing partial syncs would force the script to make the call "skip the one file with the secret, pass the rest through" ― and a mistake in that call is the scariest failure mode here. So the threshold has exactly one setting: stop everything. Writing defaults to dry. MODE="${1:-dry}" means running with no argument writes nothing and only prints a preview. In apply mode, if a file with the same name already exists at the destination, the contents are compared (cmp -s, effectively a hash comparison); a match is skipped, and a difference is written out as a separate timestamped file ― coexistence, not overwrite. if cmp -s "$tmp" "$out"; then rm -f "$tmp" skipped=$((skipped+1)) continue fi stamped="$DST/${base%.md}.$(ts_suffix).md" mv "$tmp" "$stamped" Running it for real Running dry locally produced this: $ ~/.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/ actually holds 12 .md files. Only these 4 made it through the filter; the other 8 (log.md / README.md / next-session-todo.md / commands-consolidation-plan.md and so on) are out of scope. That's where I noticed how commands-consolidation-plan.md gets handled. The filename contains "plan," so you'd expect it to qualify, but it never shows up as a candidate. The reason is the pattern. *-plan-*.md This means "-plan- followed by at least one more character before .md," so xxx-plan.md (where a dot immediately follows "plan") does not match. A file like xxx-plan-2026-05-30.md with a date suffix gets picked up, but an improvement plan whose author forgot the suffix silently falls out of scope. No error, no warning. Where operations actually stand: I tried to verify the destination and got stuck This skill's frontmatter says status: stale. status: stale By the Curator's rule (demoted to stale after 30 days unused), that means it hasn't been running in real operation for a while. So I tried to look inside the destination directory to check how far apply had actually gotten ― and tripped right there. $ ls ~/Documents/claude-obsidian/wiki/meta/improvements/ ls: .../meta/improvements/: Interrupted system call total 0 ls, find, and the Glob tool all kept returning the same Interrupted system call (EINTR) and could not enumerate the contents. The parent meta/ directory showed the same symptom. Meanwhile, existence checks on individual files went through. $ [ -e .../meta/improvements/audit-2026-05-29.md ] && echo exists exists But cat on that very same file hung with no response and timed out at 600 seconds. It behaves a lot like a directory under iCloud sync where only the file metadata exists and the actual content is stuck as an undownloaded placeholder. I can't say for certain, but the factual state is: "4 files are treated as existing, yet the path to read their contents doesn't work from this session." In other words, the secret scan and the halt-everything logic look sound on reading ― but the path for verifying afterwards that things "actually landed correctly in the wiki" is thin, and when it breaks, it's hard to notice. That's the reality that only showed up when I ran it. Writing a safety gate and being able to continuously verify that gate's output turned out to be two separate problems. Pitfalls I hit Including log.md (2,316 lines / ~228 KB) as-is would wreck the wiki's per-note granularity → explicitly excluded via filename pattern A single secret hit halts every candidate → the script is never asked to make a partial-sync decision The *-plan-*.md pattern doesn't match xxx-plan.md (no suffix) → improvement plans missing a date suffix drop out of scope with no warning Dry mode only checks "does a same-named file exist at the destination," not whether the contents differ → [dry] would write timestamped means "same-named file present or not," not "there is a diff" Verifying the destination directory's real contents gets stuck on EINTR because of iCloud sync → separately from the gate itself, you need an independent path (a hash ledger, for instance) to confirm output arrival Summary Audit and improvement memos the AI writes on its own can pick up secrets and personal information through a route entirely separate from conversation logs For secret scanning, "halt everything on a single hit" reduces judgment errors compared to "drop only the offending file" Target-file filtering that relies on shell patterns leaks silently. Inconsistent naming conventions are a breeding ground for accidents The safe-side design holds: default dry, hash-comparison skip on apply, and timestamped coexistence instead of overwrite when there's a diff But you still need a separate path to verify that what passed the gate actually arrived. This time, that path was still broken Next time I plan to cover verifying destination arrival from outside the gate using a hash ledger. If you're piping AI-generated notes into a knowledge base of your own, how do you confirm they actually landed ― or do you trust the exit code and move on? Written by **Lily* — I ship iOS apps and automate my content stack with Claude Code. Follow along: Portfolio · X · GitHub*