claude init inited
This commit is contained in:
70
.claude/hooks/README.md
Normal file
70
.claude/hooks/README.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# Claude Code Hooks 说明
|
||||
|
||||
## 结构
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── run-hook.ps1 # Windows dispatcher(读取 settings.local.json 后分发)
|
||||
├── session-init.ps1 # SessionStart hook(PowerShell 版)
|
||||
├── session-init.sh # SessionStart hook(bash 版)
|
||||
├── check-changelog.ps1 # Stop hook(PowerShell 版)
|
||||
├── check-changelog.sh # Stop hook(bash 版)
|
||||
└── 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 标记。
|
||||
108
.claude/hooks/check-changelog.ps1
Normal file
108
.claude/hooks/check-changelog.ps1
Normal 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 = true(Claude Code 正在从 Stop hook 里再次调用 Claude,避免递归)
|
||||
# 4. transcript_path 中没有 Write/Edit 工具调用(无编辑证据,防跨会话 mtime 误触发)
|
||||
# 5. 没有源文件比 changelog 更新
|
||||
|
||||
$input = [Console]::In.ReadToEnd()
|
||||
|
||||
# === 豁免 1:stop_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"
|
||||
|
||||
# === 豁免 2:changelog 文件不存在 ===
|
||||
if (-not (Test-Path $changelog)) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$clItem = Get-Item $changelog
|
||||
$clMtime = $clItem.LastWriteTime.ToFileTime()
|
||||
|
||||
# === 豁免 3:ack 标记文件足够新 ===
|
||||
$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
|
||||
}
|
||||
}
|
||||
|
||||
# === 豁免 4:transcript 中无编辑证据 ===
|
||||
$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
|
||||
71
.claude/hooks/check-changelog.sh
Normal file
71
.claude/hooks/check-changelog.sh
Normal 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 = true(Claude Code 正在从 Stop hook 里再次调用 Claude,避免递归)
|
||||
# 4. transcript_path 中没有 Write/Edit 工具调用(无编辑证据,防跨会话 mtime 误触发)
|
||||
# 5. 没有源文件比 changelog 更新
|
||||
|
||||
input=$(cat)
|
||||
|
||||
# === 豁免 1:stop_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"
|
||||
|
||||
# === 豁免 2:changelog 文件不存在 ===
|
||||
if [ ! -f "$changelog" ]; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
|
||||
clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null)
|
||||
|
||||
# === 豁免 3:ack 标记文件足够新 ===
|
||||
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
|
||||
|
||||
# === 豁免 4:transcript 中无编辑证据 ===
|
||||
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
|
||||
35
.claude/hooks/run-hook.ps1
Normal file
35
.claude/hooks/run-hook.ps1
Normal 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 '{}'
|
||||
}
|
||||
}
|
||||
24
.claude/hooks/session-init.ps1
Normal file
24
.claude/hooks/session-init.ps1
Normal 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
|
||||
22
.claude/hooks/session-init.sh
Normal file
22
.claude/hooks/session-init.sh
Normal 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
|
||||
Reference in New Issue
Block a user