# 12 Files In, 4 Out: The Secret-Scanning Gate Between Claude Code's Audit Memos and My Obsidian Wiki

> Source: <https://dev.to/bokuwalily/12-files-in-4-out-the-secret-scanning-gate-between-claude-codes-audit-memos-and-my-obsidian-wiki-19dd>
> Published: 2026-09-12 00:00:05+00:00

Last time I wrote about [handing a monitoring script exactly one root command](https://zenn.dev/bokuwalily/articles/dasd-guard-scoped-sudo). 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.

`~/.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`.

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 `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.

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.

``` bash
$ 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.

`log.md` (2,316 lines / ~228 KB) as-is would wreck the wiki's per-note granularity`*-plan-*.md` pattern doesn't match `xxx-plan.md` (no suffix)`[dry] would write timestamped` means "same-named file present or not," not "there is a diff"
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](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*
