Obsidian保管庫のルールを機械的に守らせる: 公開領域から非公開ノートへのリンク、カレンダーの色ルール

Obsidian の保管庫を「AI と共有する自分のナレッジレイク」にして、作業の多くを AI(Claude Code / Codex)に任せられるようにするための一式。 作者が自分の保管庫で実際に運用している仕組みから、個人の中身を抜いて汎用化したもの。
| フォルダ | 何か | 最初に読むもの |
|---|---|---|
knowledge-lake-kit/ | 保管庫の器。 フォルダ構成、AI への指示書(AGENTS.md)、各フォルダの README、テンプレート、運用の考え方 | START_HERE.md(人間)/ SETUP_FOR_AI.md(AI) |
agent-tooling/ | AI に持たせる道具。 スキル(/inbox・/prioritize など)、MCP(Gmail 横断・Google Tasks)、Claude Code の mod、フック、CLI、定期実行 | README.md(人間)/ FOR_AI.md(AI) |
git clone https://github.com/KaiMiyazawa/obsidian-ai-knowledge-lake.git
knowledge-lake-kit/START_HERE.md を上から順に進める。最後は AI がインタビューしながら保管庫を組み立てる。<クローンした場所>/agent-tooling/FOR_AI.md を読んで、この道具を私の保管庫に入れるのを手伝ってどちらも「AI が読んで、利用者に確認しながら進める」前提で書いてある。全部を一度に入れなくてよい。
you@gmail.com、<MAIN_LIST_ID> など)にしてある。MIT。ただし agent-tooling/outside-vault/gtasks-mcp/ は alvinjchoi/gtasks-mcp(MIT)を改造したもので、同フォルダの LICENSE に従う。
hooks/register.ts 77 lines1import type { Register } from 'claude-code'
2
3// 公開前提の領域(AGENTS.md「編集時の注意」)。ここへの書き込みに、名前で中身が分かる非公開ノートへの参照を入れさせない。
4const PUBLIC_AREA = /\/Obsidian\/(99_public|89_profile)\//
5const PRIVATE_REF = /(08_people\/|06_health\/|relationship-end)/
6
7// 02_note/calendar-color-rules.md の要約。拒否理由としてモデルに渡すので、mdを読み直さなくて済む量に絞る。
8const CALENDAR_RULES = [
9 '色ルール(02_note/calendar-color-rules.md): 赤→オレンジ→仮→時間だけ→ジャンル、の順に決める。',
10 '11=重要(取り返しがつかない。月5件まで、迷ったら6) / 6=準重要 / 1=仮・未確定 / 9=大学・学び / 10=仕事・収入 /',
11 '3=プライベート / 4=健康・通院 / 8=繰り返しの生活予定 / 5=目印・期間 / 指定なし=雑多な用事 / 7(ピーコック)は使わない。',
12].join('\n')
13
14const CALENDAR = 'mcp__claude_ai_Google_Calendar__'
15
16type CalendarArgs = {
17 summary?: string
18 startTime?: string
19 endTime?: string
20 allDay?: boolean
21 colorId?: string
22}
23
24// 終日予定を+09:00で渡すと1日前にずれる(2026-10-01に実際にずれた)。
25const allDayOffsetProblem = (a: CalendarArgs) =>
26 a.allDay === true && [a.startTime, a.endTime].some(t => t !== undefined && !t.endsWith('Z'))
27
28export const register: Register = on => {
29 on('tool.call', { tool: 'Write' }, ($, e, next) =>
30 PUBLIC_AREA.test(e.file_path) && PRIVATE_REF.test(e.content)
31 ? { deny: publicDeny(e.content) }
32 : next(e),
33 )
34
35 on('tool.call', { tool: 'Edit' }, ($, e, next) =>
36 PUBLIC_AREA.test(e.file_path) && PRIVATE_REF.test(e.new_string)
37 ? { deny: publicDeny(e.new_string) }
38 : next(e),
39 )
40
41 // 色なしの1回目は止めて確認させ、同じ引数での再実行は「雑多な用事」として通す。
42 const confirmedUncolored = new Set<string>()
43
44 on('tool.call', ($, e, next) => {
45 const tool = String(e.tool)
46 if (!tool.startsWith(CALENDAR)) return next(e)
47 const isCreate = tool === `${CALENDAR}create_event`
48 if (!isCreate && tool !== `${CALENDAR}update_event`) return next(e)
49
50 const args = e as unknown as CalendarArgs
51 if (args.colorId === '7') {
52 return { deny: `vault-guard: colorId 7(ピーコック)は使わない。\n${CALENDAR_RULES}` }
53 }
54 if (allDayOffsetProblem(args)) {
55 return {
56 deny: 'vault-guard: 終日予定の日付は 2026-12-23T00:00:00Z のように Z で渡す(+09:00 だと1日ずれる)。終了日は最終日の翌日。',
57 }
58 }
59 if (isCreate && args.colorId === undefined) {
60 const key = `${args.summary}|${args.startTime}`
61 if (!confirmedUncolored.has(key)) {
62 confirmedUncolored.add(key)
63 return {
64 deny: `vault-guard: colorId が未指定。\n${CALENDAR_RULES}\n雑多な用事で本当に色なしなら、同じ引数のまま再実行すれば通る。`,
65 }
66 }
67 }
68
69 return next(e)
70 })
71}
72
73const publicDeny = (text: string) => {
74 const hit = text.match(PRIVATE_REF)?.[0]
75 return `vault-guard: 公開領域(99_public / 89_profile)に非公開ノートへの参照「${hit}」が入っている。公開サイトでも GitHub 上でパスが見えるので外す(AGENTS.md「編集時の注意」)。`
76}
77