{"slug": "12-files-in-4-out-the-secret-scanning-gate-between-claude-code-s-audit-memos-and", "title": "12 Files In, 4 Out: The Secret-Scanning Gate Between Claude Code's Audit Memos and My Obsidian Wiki", "summary": "A developer built wiki-sync-improvements.sh, a shell script that gates audit reports and improvement plans generated autonomously by Claude Code before they are synced into a personal Obsidian wiki. The script filters 12 Markdown files down to 4 eligible audit/plan/curation documents and aborts the entire sync if any candidate matches an API-key pattern such as sk-* or ghp_*, rather than dropping only the offending file. It defaults to a dry-run preview and writes conflicting files as timestamped copies instead of overwriting existing notes.", "body_md": "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.\n\n`~/.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`):\n\n```\n| project | 状態 | 推奨アクション |\n|---|---|---|\n| **seo-affiliate-site** 🔴 | 63 files 未コミット（Like / コメント / AdSense 等のマネタイズ実装が宙吊り） | 機能単位で分割コミット |\n| closet-os 🟢 | clean / 直近活発 | `.env.production.local` 1.9KB は gitignore 済だがバックアップ運用注意 |\n```\n\nThat'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.\n\nI needed something mechanical that would stop these memos before they got poured into the wiki. That's `wiki-sync-improvements.sh`.\n\nThe comment block at the top of the script summarizes the whole design.\n\n```\n# wiki-sync-improvements.sh\n# ~/.claude/improvements/ の個別 plan/audit/curation docs を\n# ~/Documents/claude-obsidian/wiki/meta/improvements/ に同期する。\n#\n# 対象: audit-*.md / *-plan-*.md / *-curation-*.md\n# 除外: log.md (巨大), README.md, next-session-todo.md など\n# 秘密スキャン: sk-* / ghp_* が含まれていれば sync 中止\n#\n# 使い方:\n#   wiki-sync-improvements.sh dry       # 変更プレビューのみ\n#   wiki-sync-improvements.sh apply     # 実際に書き込む\n```\n\nThe 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.\n\nTarget filtering is a shell `case` pattern.\n\n```\nfor f in \"$SRC\"/*.md; do\n  base=\"$(basename \"$f\")\"\n  case \"$base\" in\n    audit-*.md|*-plan-*.md|*-curation-*.md)\n      candidates+=(\"$f\")\n      ;;\n  esac\ndone\n```\n\nThe secret scan looks at every candidate first, and only then decides whether to stop.\n\n```\nSECRET_RE='(sk-[A-Za-z0-9_-]{16,}|ghp_[A-Za-z0-9]{20,})'\nsecret_hits=()\nfor f in \"${candidates[@]}\"; do\n  if grep -E -q \"$SECRET_RE\" \"$f\"; then\n    secret_hits+=(\"$f\")\n  fi\ndone\nif [[ ${#secret_hits[@]} -gt 0 ]]; then\n  echo \"[abort] secrets detected — sync stopped\" >&2\n  for h in \"${secret_hits[@]}\"; do echo \"  - $h\" >&2; done\n  exit 2\nfi\n```\n\n**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.\n\nWriting 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.\n\n```\nif cmp -s \"$tmp\" \"$out\"; then\n  rm -f \"$tmp\"\n  skipped=$((skipped+1))\n  continue\nfi\nstamped=\"$DST/${base%.md}.$(ts_suffix).md\"\nmv \"$tmp\" \"$stamped\"\n```\n\nRunning `dry` locally produced this:\n\n```\n$ ~/.claude/scripts/wiki-sync-improvements.sh dry\n[dry] would write timestamped: audit-2026-05-29.md -> meta/improvements/audit-2026-05-29.<ts>.md\n[dry] would write timestamped: plugin-curation-2026-05-30.md -> meta/improvements/plugin-curation-2026-05-30.<ts>.md\n[dry] would write timestamped: seo-affiliate-commit-plan-2026-05-30.md -> meta/improvements/seo-affiliate-commit-plan-2026-05-30.<ts>.md\n[dry] would write timestamped: wiki-cleanup-plan-2026-05-29.md -> meta/improvements/wiki-cleanup-plan-2026-05-29.<ts>.md\n---\n[done] planned=4 total_candidates=4 (dry run; no writes)\n```\n\n`~/.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.\n\nThat'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.\n\n```\n*-plan-*.md\n```\n\nThis 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.\n\nThis skill's frontmatter says `status: stale`.\n\n```\nstatus: stale\n```\n\nBy 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.\n\n``` bash\n$ ls ~/Documents/claude-obsidian/wiki/meta/improvements/\nls: .../meta/improvements/: Interrupted system call\ntotal 0\n```\n\n`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.\n\n```\n$ [ -e .../meta/improvements/audit-2026-05-29.md ] && echo exists\nexists\n```\n\nBut `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.\"\n\nIn 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.\n\n`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\"\nNext time I plan to cover verifying destination arrival from outside the gate using a hash ledger.\n\nIf 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?\n\n*Written by **Lily** — I ship iOS apps and automate my content stack with Claude Code.\n\nFollow along: [Portfolio](https://bokuwalily.com) · [X](https://x.com/bokuwalily) · [GitHub](https://github.com/bokuwalily)*", "url": "https://wpnews.pro/news/12-files-in-4-out-the-secret-scanning-gate-between-claude-code-s-audit-memos-and", "canonical_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_at": "2026-09-12 00:00:05+00:00", "updated_at": "2026-09-12 00:22:15.687216+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-safety"], "entities": ["Claude Code", "Obsidian", "GitHub"], "alternates": {"html": "https://wpnews.pro/news/12-files-in-4-out-the-secret-scanning-gate-between-claude-code-s-audit-memos-and", "markdown": "https://wpnews.pro/news/12-files-in-4-out-the-secret-scanning-gate-between-claude-code-s-audit-memos-and.md", "text": "https://wpnews.pro/news/12-files-in-4-out-the-secret-scanning-gate-between-claude-code-s-audit-memos-and.txt", "jsonld": "https://wpnews.pro/news/12-files-in-4-out-the-secret-scanning-gate-between-claude-code-s-audit-memos-and.jsonld"}}