Logs every function-hook event (on '*') to JSONL for inspection.

kawaz の Claude Code 用ルール / スキルの central リポジトリ。 claude-rules-* overlay 群を束ね、just setup で各 CLAUDE_CONFIG_DIR に配備する。
claude-rules-* は 1 つの central + 複数の overlay で構成される。
| リポ | 役割 | 専用環境 (CLAUDE_CONFIG_DIR) |
|---|---|---|
| kawaz/claude-rules-personal (これ) | central。全 overlay を束ね、配備 recipe (justfile + scripts/) を持つ。環境一覧 repos_mapping.local.json は gitignore 対象で各マシンに置く (雛形は repos_mapping.example.json) | ~/.claude-personal |
| 業務用 overlay (private) | 業務面のルール差分 | 専用環境 |
| kawaz/claude-rules-zunsystem | zunsystem 識別子 overlay (private) | (専用環境なし) |
| 業務識別子 overlay (private) | 特定業務面の識別子差分 | (専用環境なし) |
repos_mapping.local.json は追跡しないjust setup を実行するdocs/ に集約する各リポ共通:
for-all/rules/ — 全環境向けルール (全 ~/.claude*/rules/ に注入)for-all/plugins.json — 全環境に入れる Claude Code plugin の宣言 (just plugins-setup が install)for-me/rules/ — その面の専用環境にのみ注入for-me/plugins.json — その面の専用環境にのみ install する plugin の宣言。 plugin の skill / agent description は全セッションの context に載るので、 面固有の plugin はこちらに置く (for-all に置くと他の面にも語彙が漏れる)for-others/rules/ — 他環境から参照される情報 (固有名詞リスト等のサニタイズ規定)skill と agent は リポ自体を Claude Code plugin として配布する (.claude-plugin/plugin.json + リポ直下の skills/<slug>/ agents/ hooks/)。 各リポの for-all/plugins.json に自リポの plugin を宣言し、just plugins-setup が install することで配備される。Skill tool からは <plugin名>:<slug> (例: rules-personal:eli5) で呼ぶ。skill はユーザが /名前 で起動する実行系だけを置き、 読むだけの手順書は reference/ に置く (判定は for-all/rules/rule-writing-guidelines.md)。 AI に自動起動させたくないユーザ専用の skill は frontmatter に disable-model-invocation: true を付け (AI の一覧から消える)、 本文は reference の該当ファイルを読ませる 1〜2 行に留める。
読むだけの参照知識はこのリポの直下に平置きする (索引と本文の 2 段構成、本文は必要時にだけ Read):
reference/ — 公開してよい体系知識。索引は reference/_index.mdmemory/ — rule にするほど一般化していない横断メモ。索引は memory/_index.md個人情報系 (本人の表記・アカウント名・連絡先) は public 候補のこのリポではなく private リポ kawaz/privacy-personal に同じ構造で置き、その索引だけを reference/_index.md の「private 層」節に写す (業務面でしか効かないものは各 overlay の for-me/)。 書き先の判定は for-all/rules/knowledge-guide.md。
for-me の "me" は「個人 vs 他者」ではなく、kawaz が持つ複数の面 (個人開発 / emrd 業務 / ...) のうちの その overlay の面を指す。
personal リポ固有:
scripts/rules.sh / scripts/plugins.sh — 配備スクリプト (subject ごと。justfile の recipe から呼ぶ)repos_mapping.local.json — 全 overlay リポと各 home (CLAUDE_CONFIG_DIR) の定義docs/ — 設計判断・課題 (issue/)、運用手順 (runbooks/) 等配備は just の recipe で行う。subject (rules / plugins) ごとに <subject>-{setup,check,update} があり、setup / check / update がそれらを束ねる。home 引数を省略すると repos_mapping.local.json に宣言された全面が対象:
just setup # 全面: rules の symlink + plugin の install
just check # 全面: 配備状態の検査 (変更しない)
just update # 全面: plugin の update
just rules-setup ~/.claude-emrd # 1 面だけ
rules-setup: for-*/rules/ を $HOME_DIR/rules/ 配下にディレクトリ symlink し、dangling link を掃除 (rules-check は期待する link が揃って実体を指しているかを検査)plugins-setup: for-all/plugins.json (全 repo) と自面の for-me/plugins.json に宣言された plugin を marketplace add + install。bare 面 (~/.claude-bare) は repos_mapping.local.json の pluginOnlyHomes に列挙した ccmsg だけを入れるplugins-check: rules 面は宣言の不足だけ、bare 面は ccmsg 以外が入っていないことも検査。宣言外に手で入れた plugin と enable / disable 状態は触らないdocs/issue/ — TODO・課題docs/runbooks/ — 運用・セットアップ手順業務固有名詞は public 候補の personal には置かず、各 overlay リポ (private) で管理。 共通サニタイズの仕組みは for-all/rules/sanitize-work-identifiers.md を参照。
hooks/register.ts 89 lines1// Logs every event this plugin can see to JSONL.
2// <dir>/events-<sessionId>-<part>.jsonl one line per event: seq, time, event, caller, keys, origin
3// <dir>/samples-<sessionId>.jsonl the first `e` of each event name (clipped)
4// <dir> is $MODS_LAB_DIR, or <plugin root>/log when unset.
5
6const CLIP = 1500
7const PART_BYTES = 3 * 1024 * 1024 // $.fs.read / write reject over 4 MiB
8
9let dir: string | undefined
10let sid = 'unknown'
11let part = 0
12let seq = 0
13let buf: unknown[] = []
14let samples: unknown[] = []
15const seen = new Set<string>()
16let flushing = false
17
18function clip(v: unknown): unknown {
19 try {
20 const s = JSON.stringify(v)
21 if (s === undefined) return String(v)
22 return s.length > CLIP ? s.slice(0, CLIP) + '...<clipped>' : JSON.parse(s)
23 } catch (err) {
24 return `<unserializable: ${String(err)}>`
25 }
26}
27
28async function append($: any, path: string, lines: unknown[]): Promise<number> {
29 let prev = ''
30 try { prev = await $.fs.read(path) } catch { prev = '' }
31 const next = prev + lines.map((l) => JSON.stringify(l)).join('\n') + '\n'
32 await $.fs.write(path, next)
33 return next.length
34}
35
36async function flush($: any): Promise<void> {
37 if (flushing || dir === undefined) return
38 if (buf.length === 0 && samples.length === 0) return
39 flushing = true
40 try {
41 const lines = buf; buf = []
42 const firsts = samples; samples = []
43 if (lines.length > 0) {
44 const size = await append($, `${dir}/events-${sid}-${part}.jsonl`, lines)
45 if (size > PART_BYTES) part += 1
46 }
47 if (firsts.length > 0) await append($, `${dir}/samples-${sid}.jsonl`, firsts)
48 } finally {
49 flushing = false
50 }
51}
52
53export function register(on: any): void {
54 // Registered first, so it is this plugin's outermost hook. It must not touch `$`:
55 // at engine.create `$` is still empty.
56 on('*', ($: any, e: any, next: any) => {
57 const ev = String(next.event)
58 seq += 1
59 const entry: Record<string, unknown> = {
60 n: seq,
61 t: Date.now(),
62 ev,
63 by: next.origin,
64 keys: e !== null && typeof e === 'object' ? Object.keys(e) : typeof e,
65 }
66 if (e && typeof e === 'object' && 'origin' in e) entry.origin = clip(e.origin)
67 if (ev === 'ui.render') entry.component = e?.component
68 if (ev.startsWith('tool.')) entry.tool = e?.tool
69 buf.push(entry)
70 if (!seen.has(ev)) {
71 seen.add(ev)
72 samples.push({ n: seq, ev, by: next.origin, e: clip(e) })
73 }
74 return next(e)
75 })
76
77 on('session.start', async ($: any, e: any, next: any) => {
78 dir = (await $.env.get('MODS_LAB_DIR')) ?? `${$.plugin.root}/log`
79 sid = await $.session.id()
80 $.clock.every(1000, () => { void flush($) })
81 return next(e)
82 })
83
84 on('session.end', async ($: any, e: any, next: any) => {
85 await flush($)
86 return next(e)
87 })
88}
89