After a write to a research repo's note/note.md, checks its generated table of contents and heading shape, and tells the model to rebuild the contents when…

Personal dotfiles for zsh, WezTerm, and global Codex / Claude Code Skills.
./init.sh
init.sh は idempotent。以下のいずれかをやったら再実行する:
skills/<name>/ を追加した (新 skill)skills/<name>/ を削除した (broken symlink が link_skills で自動 cleanup)zsh/, wezterm/, nvim/, git/, lazygit/, czg/, cz-git/, starship/ の構成を変えた./bootstrap.sh
Then run:
gh auth login
gh auth refresh -s project
git config --global ghq.root "$HOME/src"
mkdir -p "$HOME/.config/zsh" "$HOME/.config/wezterm" "$HOME/.config/codex/skills" "$HOME/.claude/skills"
./init.sh
既にあるマシンへ 追加分だけ 取り込むときは --no-upgrade を付ける。 付けないと brew bundle は古い formula をまとめて upgrade する。
git pull && brew bundle --file=./Brewfile --no-upgrade
./bootstrap.sh also installs Miniforge3 into ~/miniforge3 when missing. ./init.sh links zsh/env.zsh, which loads conda shell support without auto-activating base.
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
echo "" >> "$HOME/.zprofile"
echo 'eval "$(/opt/homebrew/bin/brew shellenv zsh)"' >> "$HOME/.zprofile"
eval "$(/opt/homebrew/bin/brew shellenv zsh)"
# 公式以外の tap は Homebrew 7 から明示的な信頼が要る(~/.homebrew/trust.json, マシンごと)
brew trust olets/tap
# パッケージの一覧は Brewfile が正本(個別の brew install をここに増やさない)
brew bundle --file=./Brewfile
npm install -g git-cz czg cz-git
npx -y czg --api-key="sk-XXXX"
Clone this repo with ghq, then install:
gh auth login
gh auth refresh -s project
git config --global ghq.root "$HOME/src"
mkdir -p "$HOME/.config/zsh" "$HOME/.config/wezterm" "$HOME/.config/codex/skills" "$HOME/.claude/skills"
ghq get https://github.com/38kta-lab/dotfile
cd "$(ghq root)/github.com/38kta-lab/dotfile"
./install_miniforge.sh
./init.sh
gh auth refresh -s project is required on each machine where Codex or gh updates GitHub Projects, such as the Life project Status field.
# Codex CLI
npm install -g @openai/codex
mkdir -p ~/.config/codex
mv ~/.codex/* ~/.config/codex/
codex sign-in
agy) — Gemini CLI の後継Google は 2026-06-18 に個人 Google アカウント向けの Gemini CLI ログインを サーバ側で打ち切った(This client is no longer supported for Gemini Code Assist for individuals エラー)。後継の Antigravity CLI (agy) に移行する。
# 旧 gemini CLI が残っていれば削除
npm uninstall -g @google/gemini-cli 2>/dev/null; rm -rf ~/.gemini
# Antigravity CLI をインストール (Go 製 single binary、~/.local/bin/agy)
curl -fsSL https://antigravity.google/cli/install.sh | bash
# 初回起動でブラウザ認証 (要 Google アカウント。Pro 系モデルはサブスク必要)
agy
~/.local/bin は zsh/env.zsh で既に PATH 追加済み → 追加設定不要。agy / 非対話 agy -p "プロンプト" / モデル指定 agy --model <id> -p ... / 一覧 agy models / 継続 agy -c。Miniforge3 is installed under:
~/miniforge3
Install or verify it:
./install_miniforge.sh
Use a pinned Miniforge release when needed:
MINIFORGE_VERSION=25.11.0-0 ./install_miniforge.sh
The installer supports Apple Silicon and Intel macOS by selecting the matching installer from the official conda-forge/miniforge GitHub releases:
https://github.com/conda-forge/miniforge/releases
base should not auto-activate:
conda config --set auto_activate_base false
For per-repo environments, prefer environment.yml in that repo:
conda env create -f environment.yml
conda activate <env-name>
If an environment already exists:
conda env update -f environment.yml --prune
For Codex-assisted Calendar reads in the life repo, place the OAuth desktop client JSON at:
~/.config/life/google-calendar-credentials.json
Copy this file between personal Macs using a private secure channel. Do not commit it to git, and do not create a public/shared link.
On a new Mac:
mkdir -p "$HOME/.config/life"
mv "$HOME/Downloads/google-calendar-credentials.json" "$HOME/.config/life/google-calendar-credentials.json"
chmod 600 "$HOME/.config/life/google-calendar-credentials.json"
If the downloaded file has a client_secret_*.json name:
mkdir -p "$HOME/.config/life"
mv "$HOME/Downloads"/client_secret_*.json "$HOME/.config/life/google-calendar-credentials.json"
chmod 600 "$HOME/.config/life/google-calendar-credentials.json"
Do not copy this token between Macs:
~/.config/life/google-calendar-read-token.json
Generate that token separately on each Mac by running the Calendar reader from the life repo after activating its conda environment:
conda activate life
python scripts/google_calendar_read.py --format json
Global user Skills are managed in this repo under:
skills/
./init.sh links each directory under skills/ into both:
~/.config/codex/skills/
~/.claude/skills/
System Skills under ~/.config/codex/skills/.system/ are not managed here. Claude Code's auto memory under ~/.claude/projects/.../memory/ is also not managed here.
Use PRs with squash merge to keep main clean and reduce cross-machine conflicts. Rule of thumb: update main, but do not work directly on it.
Branch naming (one branch per machine):
work/<hostname> (example: work/kta38-mini-lab)Get <hostname> with:
hostname -s
Initial setup (first time on a machine):
HOST="$(hostname -s)"
git switch main
git pull --rebase
git switch -c "work/$HOST"
git push -u origin "work/$HOST"
Flow (manual):
# start work (every time)
git switch main
git pull --rebase
git switch work/<hostname>
git rebase main
# work + commit
git add -A
git commit -m "feat: ..."
git push -u origin work/<hostname>
Create a PR from work/<hostname> to main:
gh pr create --base main --head work/<hostname> --fill
Then Squash and merge it on GitHub, or with gh:
gh pr merge <PR_NUMBER> --squash
Merge commit (no squash):
gh pr merge <PR_NUMBER> --merge
Aliases (see zsh/alias.zsh):
winit # initial setup: create/push work/<hostname> branch
wmain # update main only (before switching)
wstart # start work: update main -> switch work/<hostname> -> rebase
wrebase # rebase current work branch onto main
prc # gh pr create --base main --head work/<hostname> --fill
prs # gh pr merge --squash
prm # gh pr merge --merge
Then on other machines:
git switch main
git pull --rebase
Notes:
work/<hostname> rebased onto main to avoid long-lived divergence.main.wezterm/keybinds.lua and linked to ~/.config/wezterm/keybinds.lua.skills/ and linked to both ~/.config/codex/skills/ and ~/.claude/skills/.~/.config/.czrc is not committed; see czrc/.czrc.example for a template.Keyboard
日本語 - ローマ字入力英字オフの時「英字」を入力Trackpad
MaxオンPointer
1つ大きくする#D05654#464758iCloud
オンDesktop
スタックを使用表示オプションを表示10種類名前36x36下から4番目Dock
左オンDefault browser
Chromehooks/register.ts 84 lines1import type { Register } from 'claude-code'
2
3// note.md's table of contents is generated from its headings by
4// scripts/note_toc.py. After every write to a note.md, check that the
5// contents still match and that every analysis heading has its three lines,
6// and tell the model when not. The mod never rewrites the file itself: the
7// model may still be editing it, and a write from outside would make its
8// next Edit fail.
9const NOTE = /(^|\/)note\/note\.md$/
10
11export type Verdict = { stale: boolean; problems: string[] }
12
13export function verdict(exitCode: number, stderr: string): Verdict {
14 const problems = stderr
15 .split('\n')
16 .map(l => l.trim())
17 .filter(l => l.startsWith('note_toc: ') && !l.includes('書き込まない'))
18 .map(l => l.slice('note_toc: '.length))
19 return { stale: exitCode === 1, problems }
20}
21
22export function message(path: string, command: string, v: Verdict): string | undefined {
23 if (!v.stale && v.problems.length === 0) return undefined
24 const parts: string[] = []
25 if (v.problems.length > 0) {
26 parts.push(`note-toc: ${path} の見出しが型に合っていません(目次を作れません): ${v.problems.join(' / ')}。` +
27 '型は「## <解析番号> — 題(YYYY-MM-DD|日付未記載)」と直下の「- 問い: / - 結論: / - 状態:」の 3 行、1 つの番号に ## は 1 つ。')
28 }
29 if (v.stale) {
30 parts.push(`note-toc: ${path} の冒頭の目次が古くなっています。note の編集が終わったら \`${command}\` を実行して作り直してください(目次は手で書かない)。`)
31 }
32 return parts.join(' ')
33}
34
35// Rules.md「研究 PJ の解析番号の作法」5: the 結論 field holds the user's words
36// only, marked (user YYYY-MM-DD); anything else stays 未記載. Only the text this
37// call wrote is looked at, so conclusions written before the rule stay quiet.
38export function unmarkedConclusions(text: string): string[] {
39 return text
40 .split('\n')
41 .map(l => l.trim())
42 .filter(l => /^- 結論:/.test(l))
43 .map(l => l.replace(/^- 結論:\s*/, ''))
44 .filter(c => c !== '' && !c.startsWith('未記載') && !/(user[\s ]/.test(c) && !/\(user\s/.test(c))
45}
46
47export function conclusionMessage(path: string, found: string[]): string | undefined {
48 if (found.length === 0) return undefined
49 const shown = found.slice(0, 3).map(c => `「${c.slice(0, 60)}」`).join(' ')
50 return `note-toc: ${path} の「結論」に user の印の無い文があります: ${shown}${found.length > 3 ? ` ほか ${found.length - 3} 件` : ''}。` +
51 '結果の解釈は user が持つ(Rules.md「研究 PJ の解析番号の作法」5)。user が言った結論なら末尾に「(user YYYY-MM-DD)」を付け、そうでなければ「未記載」に戻して、観察(数値・差・件数)は本文の「結果」に書く。'
52}
53
54async function check($: any, python: string, repo: string, path: string): Promise<string | undefined> {
55 const script = `${repo}/scripts/note_toc.py`
56 const r = await $.process.run([python, script, path, '--check'], { timeoutMs: 30000 })
57 return message(path, `${python} ${script} ${path} --write`, verdict(r.exitCode, r.stderr))
58}
59
60// One hook for both tools; each is registered with a literal matcher, because
61// a session reads the matcher from the source (a matcher built from a loop
62// variable passed the tests but never ran in a session).
63async function afterWrite($: any, path: string, written: string, ran: any, python: string, repo: string): Promise<any> {
64 if (!NOTE.test(path) || ran.deny !== undefined || ran.isError) return ran
65 const unmarked = conclusionMessage(path, unmarkedConclusions(written))
66 if (unmarked) ran = { ...ran, context: [...(ran.context ?? []), unmarked] }
67 // Say so instead of staying silent: an unset path means the check never runs.
68 if (!repo) return { ...ran, context: [...(ran.context ?? []), 'note-toc: life_repo が未設定のため、note.md の目次と見出しの型を確かめられませんでした(settings.json の pluginConfigs)。'] }
69 try {
70 const note = await check($, python, repo, path)
71 return note ? { ...ran, context: [...(ran.context ?? []), note] } : ran
72 } catch (err) {
73 return { ...ran, context: [...(ran.context ?? []), `note-toc: note.md の確認に失敗しました(${String(err).slice(0, 300)})。`] }
74 }
75}
76
77export const register: Register = (on, options) => {
78 const python = String(options.python || 'python3')
79 const repo = String(options.life_repo ?? '')
80
81 on('tool.call', { tool: 'Edit' }, async ($, e, next) => afterWrite($, e.file_path, String(e.new_string ?? ''), await next(e), python, repo))
82 on('tool.call', { tool: 'Write' }, async ($, e, next) => afterWrite($, e.file_path, String(e.content ?? ''), await next(e), python, repo))
83}
84