Skip to content

Commit cacdf80

Browse files
DanMeonclaude
andcommitted
chore: last_updated 자동 갱신 hook (Claude Code PostToolUse)
Edit / Write / MultiEdit 시 docs/*.md 의 frontmatter last_updated 를 오늘 날짜로 in-place 갱신. CONVENTIONS § Status 메타데이터 의 자동화 정책 구현 — 수기 갱신 절차 폐기. skip 조건: - docs/ 외부 파일 - frontmatter 없는 파일 (Living: CONVENTIONS / roadmap/README / traces/coverage) - last_updated 가 이미 오늘 날짜 - status 가 Frozen 또는 Superseded — 본문 의미 변경 금지가 원칙. 면제 조항 활용 일괄 마이그 vs 오타·링크 fix 둘 다 가능 → 자동 처리는 위험, 사용자가 명시 결정 settings.json 의 PostToolUse hook 배열에 등록 — docs-lint 보다 먼저 실행되어 lint 가 갱신된 frontmatter 를 검증. 검증: 합성 Draft 파일 (last_updated: 2026-01-01) → 오늘 날짜로 갱신, 합성 Frozen 파일은 그대로 유지. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b4f474c commit cacdf80

2 files changed

Lines changed: 81 additions & 0 deletions

File tree

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
#!/usr/bin/env python3
2+
"""docs/*.md 편집 시 frontmatter 의 last_updated 를 오늘 날짜로 자동 갱신.
3+
4+
PostToolUse hook 으로 Edit / Write / MultiEdit 후 실행. stdin 으로 받은 hook
5+
event 의 ``tool_input.file_path`` 가 ``docs/*.md`` 면서 frontmatter 가 있으면
6+
``last_updated:`` 라인을 오늘 날짜로 in-place 교체.
7+
8+
skip 조건:
9+
- ``docs/`` 외부 파일
10+
- frontmatter 없는 파일 (Living: CONVENTIONS / roadmap/README / traces/coverage)
11+
- last_updated 가 이미 오늘 날짜
12+
- frontmatter 의 status 가 Frozen 또는 Superseded — 본문 의미 변경 금지가
13+
원칙. 이런 파일을 편집한 경우는 (a) Frozen 면제 조항 활용 일괄 마이그
14+
(PR 단위 수기 처리) 또는 (b) 오타·링크 fix (last_updated 갱신 적절) 둘
15+
다 가능 → 자동 처리는 위험하니 hook 은 skip, 사용자가 명시 결정.
16+
17+
본 hook 은 silent (exit 0) — 갱신 결과는 git diff 로 확인.
18+
"""
19+
20+
import json
21+
import re
22+
import sys
23+
from datetime import date
24+
from pathlib import Path
25+
26+
REPO = Path(__file__).resolve().parents[2]
27+
28+
try:
29+
event = json.loads(sys.stdin.read() or "{}")
30+
except json.JSONDecodeError:
31+
sys.exit(0)
32+
33+
tool_input = event.get("tool_input") or {}
34+
file_path = tool_input.get("file_path") or ""
35+
if not file_path:
36+
sys.exit(0)
37+
38+
try:
39+
rel = Path(file_path).resolve().relative_to(REPO)
40+
except ValueError:
41+
sys.exit(0)
42+
43+
rel_str = str(rel).replace("\\", "/")
44+
if not (rel_str.startswith("docs/") and rel.suffix == ".md"):
45+
sys.exit(0)
46+
47+
target = REPO / rel
48+
if not target.is_file():
49+
sys.exit(0)
50+
51+
text = target.read_text(encoding="utf-8")
52+
if not text.startswith("---\n"):
53+
sys.exit(0)
54+
end = text.find("\n---\n", 4)
55+
if end < 0:
56+
sys.exit(0)
57+
58+
block = text[4:end]
59+
status_match = re.search(r"^status:\s*(\S+)", block, re.MULTILINE)
60+
if status_match and status_match.group(1) in ("Frozen", "Superseded"):
61+
# ^ Frozen / Superseded 자동 갱신 금지 — 사용자 명시 결정 필요
62+
sys.exit(0)
63+
64+
today = date.today().isoformat()
65+
new_block, n = re.subn(
66+
r"^last_updated:\s*\S+",
67+
f"last_updated: {today}",
68+
block,
69+
count=1,
70+
flags=re.MULTILINE,
71+
)
72+
if n == 0 or new_block == block:
73+
sys.exit(0)
74+
75+
new_text = "---\n" + new_block + text[end:]
76+
target.write_text(new_text, encoding="utf-8")
77+
sys.exit(0)

.claude/settings.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
{
55
"matcher": "Edit|Write|MultiEdit",
66
"hooks": [
7+
{
8+
"type": "command",
9+
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/update-last-updated.py"
10+
},
711
{
812
"type": "command",
913
"command": "python3 ${CLAUDE_PROJECT_DIR}/.claude/hooks/docs-lint.py"

0 commit comments

Comments
 (0)