claude init inited

This commit is contained in:
2026-04-24 16:42:22 +08:00
commit 4c9f4f250d
39 changed files with 3185 additions and 0 deletions

70
.claude/hooks/README.md Normal file
View File

@@ -0,0 +1,70 @@
# Claude Code Hooks 说明
## 结构
```
hooks/
├── run-hook.ps1 # Windows dispatcher读取 settings.local.json 后分发)
├── session-init.ps1 # SessionStart hookPowerShell 版)
├── session-init.sh # SessionStart hookbash 版)
├── check-changelog.ps1 # Stop hookPowerShell 版)
├── check-changelog.sh # Stop hookbash 版)
└── README.md # 本文件
```
## 事件对应关系
Claude Code 的 hook 事件名与 Cursor 不同:
| Cursor 事件 | Claude Code 事件 | 用途 |
|-------------|------------------|------|
| `sessionStart` | `SessionStart` | 会话启动时触发 |
| `stop` | `Stop` | 会话停止或即将输出最终回复时触发 |
配置位置为 `.claude/settings.json``hooks` 字段,格式参考
[Claude Code 官方 hooks 文档](https://docs.claude.com/en/docs/claude-code/hooks)。
## 跨平台适配
`settings.json` 中的 `command` 字段是**平台绑定的**——Windows 用 `powershell`macOS/Linux 用 `bash`
### Windows当前默认
settings.json 使用 `powershell ... run-hook.ps1` 作为入口dispatcher 根据
`.claude/settings.local.json``shell` 字段决定执行 `.ps1` 还是 `.sh` 脚本。
### 迁移到 macOS / Linux
将 settings.json 的 command 改为直接调用 `.sh` 脚本:
```json
{
"hooks": {
"SessionStart": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/session-init.sh", "timeout": 5 }
]
}
],
"Stop": [
{
"matcher": "",
"hooks": [
{ "type": "command", "command": "bash .claude/hooks/check-changelog.sh", "timeout": 10 }
]
}
]
}
}
```
同时把 `settings.local.example.json` 复制为 `settings.local.json`,将 `shell` 字段改为 `bash``zsh`
## 与 Cursor 版本的关键差异
1. **删除 `composer_mode` 检测** —— Claude Code 没有 IDE 模式概念,相关豁免逻辑已移除
2. **使用 `transcript_path` 做 edit evidence 检查** —— Stop hook 通过读取 Claude Code 传入的 transcript 文件Grep `Write`/`Edit`/`NotebookEdit`/`MultiEdit` 工具调用判断本次会话是否实际修改过文件;没有则静默退出,避免跨会话残留 mtime 误触发
3. **无 `loop_limit` 字段** —— Claude Code hooks 配置中不存在此字段;`stop_hook_active` 由 Claude Code 通过 stdin 的 JSON 传入,避免死循环由 hook 脚本自行判断
4. **SessionStart Hook 双重职责** —— 除清除 ack 标记外,还会检查 `.claude/.init-done` sentinel缺失时创建 `.claude/.pending-init` 标记文件,供 `claude-init-recall` 规则检测,确保首次会话确定性地触发 claude-init 流程。sentinel 存在时自动清理残留的 pending 标记。

View File

@@ -0,0 +1,108 @@
# check-changelog.ps1 — Claude Code Stop hook (PowerShell 版)
#
# 检查源文件是否比 changelog-headlines.md 更新。如果更新了且本次会话确实有
# 文件编辑操作(通过 transcript_path 确认),提醒用户调用 dev-changelog Skill。
#
# 豁免条件(任一满足则静默退出):
# 1. changelog-headlines.md 不存在
# 2. .changelog-ack 文件足够新(本会话已通过 checklist 收尾项 Z 确认过)
# 3. stop_hook_active = trueClaude Code 正在从 Stop hook 里再次调用 Claude避免递归
# 4. transcript_path 中没有 Write/Edit 工具调用(无编辑证据,防跨会话 mtime 误触发)
# 5. 没有源文件比 changelog 更新
$input = [Console]::In.ReadToEnd()
# === 豁免 1stop_hook_active 防递归 ===
$stopHookActive = $false
try {
$jsonData = $input | ConvertFrom-Json
$stopHookActive = $jsonData.stop_hook_active -eq $true
} catch {}
if ($stopHookActive) {
Write-Output '{}'
exit 0
}
$changelog = ".claude\changelog\changelog-headlines.md"
# === 豁免 2changelog 文件不存在 ===
if (-not (Test-Path $changelog)) {
Write-Output '{}'
exit 0
}
$clItem = Get-Item $changelog
$clMtime = $clItem.LastWriteTime.ToFileTime()
# === 豁免 3ack 标记文件足够新 ===
$ackFile = ".claude\changelog\.changelog-ack"
if (Test-Path $ackFile) {
$ackItem = Get-Item $ackFile
$ackMtime = $ackItem.LastWriteTime.ToFileTime()
if ($ackMtime -ge $clMtime) {
Write-Output '{}'
exit 0
}
}
# === 豁免 4transcript 中无编辑证据 ===
$transcriptPath = ""
try {
$jsonData = $input | ConvertFrom-Json
$transcriptPath = $jsonData.transcript_path
} catch {}
$hasEditEvidence = $false
if ($transcriptPath -and (Test-Path $transcriptPath)) {
$content = Get-Content $transcriptPath -Raw
if ($content -match '"name"\s*:\s*"(Write|Edit|MultiEdit|NotebookEdit)"') {
$hasEditEvidence = $true
}
}
if (-not $hasEditEvidence) {
# transcript 不可用或无编辑证据,从保守角度静默
Write-Output '{}'
exit 0
}
# === 源目录配置:若项目未自定义则默认扫描整个仓库根(排除常见生成物/依赖)===
$srcDir = "."
# === 核心检查:是否有源文件比 changelog 更新 ===
$sourceExtensions = @("*.py", "*.ts", "*.tsx", "*.js", "*.jsx", "*.css", "*.cs", "*.go", "*.rs", "*.java")
$excludePatterns = @(
"*\node_modules\*", "*\.next\*", "*\__pycache__\*", "*\venv\*", "*\.venv\*",
"*\target\*", "*\bin\*", "*\obj\*", "*\.claude\*", "*\.cursor\*", "*\.git\*"
)
$newerFile = $null
foreach ($ext in $sourceExtensions) {
$files = Get-ChildItem -Path $srcDir -Filter $ext -Recurse -File -ErrorAction SilentlyContinue |
Where-Object {
$item = $_
$exclude = $false
foreach ($pattern in $excludePatterns) {
if ($item.FullName -like $pattern) {
$exclude = $true
break
}
}
-not $exclude
} |
Where-Object { $_.LastWriteTime.ToFileTime() -gt $clMtime }
if ($files) {
$newerFile = $files | Select-Object -First 1
break
}
}
if ($newerFile) {
$fname = $newerFile.Name
$msg = "[Hook] Source file updated (e.g. $fname) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
Write-Output '{"decision":"block","reason":"' + $msg + '"}'
} else {
Write-Output '{}'
}
exit 0

View File

@@ -0,0 +1,71 @@
#!/usr/bin/env bash
# check-changelog.sh — Claude Code Stop hook (bash 版)
#
# 检查源文件是否比 changelog-headlines.md 更新。如果更新了且本次会话确实有
# 文件编辑操作(通过 transcript_path 确认),提醒用户调用 dev-changelog Skill。
#
# 豁免条件(任一满足则静默退出):
# 1. changelog-headlines.md 不存在
# 2. .changelog-ack 文件足够新(本会话已通过 checklist 收尾项 Z 确认过)
# 3. stop_hook_active = trueClaude Code 正在从 Stop hook 里再次调用 Claude避免递归
# 4. transcript_path 中没有 Write/Edit 工具调用(无编辑证据,防跨会话 mtime 误触发)
# 5. 没有源文件比 changelog 更新
input=$(cat)
# === 豁免 1stop_hook_active 防递归 ===
stop_hook_active=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('stop_hook_active',False))" 2>/dev/null || echo "False")
if [ "$stop_hook_active" = "True" ]; then
echo '{}'; exit 0
fi
changelog=".claude/changelog/changelog-headlines.md"
# === 豁免 2changelog 文件不存在 ===
if [ ! -f "$changelog" ]; then
echo '{}'; exit 0
fi
clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null)
# === 豁免 3ack 标记文件足够新 ===
ackFile=".claude/changelog/.changelog-ack"
if [ -f "$ackFile" ]; then
ackMtime=$(stat -c %Y "$ackFile" 2>/dev/null || stat -f %m "$ackFile" 2>/dev/null)
if [ "$ackMtime" -ge "$clMtime" ] 2>/dev/null; then
echo '{}'; exit 0
fi
fi
# === 豁免 4transcript 中无编辑证据 ===
transcript_path=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('transcript_path',''))" 2>/dev/null || echo "")
if [ -n "$transcript_path" ] && [ -f "$transcript_path" ]; then
if ! grep -qE '"name"\s*:\s*"(Write|Edit|MultiEdit|NotebookEdit)"' "$transcript_path"; then
echo '{}'; exit 0
fi
else
# transcript 不可用则从保守角度静默(宁可漏提醒,不可错误提醒)
echo '{}'; exit 0
fi
# === 源目录配置:若项目未自定义则默认扫描整个仓库根(排除常见生成物/依赖)===
srcDir="."
# === 核心检查:是否有源文件比 changelog 更新 ===
newerFile=$(find "$srcDir" -type f \( \
-name "*.py" -o -name "*.ts" -o -name "*.tsx" -o -name "*.js" -o -name "*.jsx" \
-o -name "*.css" -o -name "*.cs" -o -name "*.go" -o -name "*.rs" -o -name "*.java" \
\) \
! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/__pycache__/*" \
! -path "*/venv/*" ! -path "*/.venv/*" ! -path "*/target/*" ! -path "*/bin/*" \
! -path "*/obj/*" ! -path "*/.claude/*" ! -path "*/.cursor/*" ! -path "*/.git/*" \
-newer "$changelog" -print -quit 2>/dev/null)
if [ -n "$newerFile" ]; then
fname=$(basename "$newerFile")
msg="[Hook] Source file updated (e.g. $fname) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
echo "{\"decision\":\"block\",\"reason\":\"$msg\"}"
else
echo '{}'
fi
exit 0

View File

@@ -0,0 +1,35 @@
# run-hook.ps1 — 通用 hook dispatcher
# 从 .claude/settings.local.json 读取 shell 类型,决定执行 .ps1 还是 .sh 脚本
# 用法powershell -ExecutionPolicy Bypass -File .claude/hooks/run-hook.ps1 <hook-name>
# 示例run-hook.ps1 session-init → 执行 session-init.ps1 或 session-init.sh
param([string]$HookName)
$localEnvPath = ".claude\settings.local.json"
$shell = "powershell"
if (Test-Path $localEnvPath) {
try {
$config = Get-Content $localEnvPath -Raw | ConvertFrom-Json
if ($config.shell) { $shell = $config.shell }
} catch {}
}
$hookDir = ".claude\hooks"
$input = [Console]::In.ReadToEnd()
if ($shell -eq "powershell") {
$scriptPath = Join-Path $hookDir "$HookName.ps1"
if (Test-Path $scriptPath) {
$input | powershell -ExecutionPolicy Bypass -File $scriptPath
} else {
Write-Output '{}'
}
} else {
$scriptPath = Join-Path $hookDir "$HookName.sh"
if (Test-Path $scriptPath) {
$input | bash $scriptPath
} else {
Write-Output '{}'
}
}

View File

@@ -0,0 +1,24 @@
# session-init.ps1 — Claude Code SessionStart hook
# 1. 清除上次会话的 changelog ack 标记
# 2. 检查 .claude/.init-done sentinel缺失时创建 .pending-init 标记文件
# claude-init-recall 规则会检测此文件,确保首次会话确定性触发 claude-init
$input = [Console]::In.ReadToEnd()
# 清除 changelog ack 标记
Remove-Item ".claude\changelog\.changelog-ack" -ErrorAction SilentlyContinue
# 检查 claude-init sentinel
$initDone = ".claude\.init-done"
$pendingInit = ".claude\.pending-init"
if (Test-Path $initDone) {
# sentinel 存在,清理可能残留的 pending 标记
Remove-Item $pendingInit -ErrorAction SilentlyContinue
} else {
# sentinel 缺失,创建 pending 标记供 claude-init-recall 规则检测
New-Item -ItemType File -Path $pendingInit -Force | Out-Null
}
Write-Output '{}'
exit 0

View File

@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# session-init.sh — Claude Code SessionStart hook
# 1. 清除上次会话的 changelog ack 标记
# 2. 检查 .claude/.init-done sentinel缺失时创建 .pending-init 标记文件
# claude-init-recall 规则会检测此文件,确保首次会话确定性触发 claude-init
input=$(cat)
# 清除 changelog ack 标记
rm -f ".claude/changelog/.changelog-ack"
# 检查 claude-init sentinel
if [ -f ".claude/.init-done" ]; then
# sentinel 存在,清理可能残留的 pending 标记
rm -f ".claude/.pending-init"
else
# sentinel 缺失,创建 pending 标记供 claude-init-recall 规则检测
touch ".claude/.pending-init"
fi
echo '{}'
exit 0