#!/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