oh-my-customcode 2.0.7 → 2.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -2
- package/dist/cli/index.js +28 -7
- package/dist/index.js +28 -7
- package/package.json +1 -1
- package/templates/.claude/hooks/hooks.json +29 -13
- package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +47 -22
- package/templates/.claude/hooks/scripts/auto-continue-guard.sh +5 -5
- package/templates/.claude/hooks/scripts/auto-dev-token-tracker.sh +12 -1
- package/templates/.claude/hooks/scripts/bash-cd-advisor.sh +172 -0
- package/templates/.claude/hooks/scripts/content-hash-validator.sh +65 -48
- package/templates/.claude/hooks/scripts/destructive-git-guard.sh +32 -17
- package/templates/.claude/hooks/scripts/fail-axis-cause-advisor.sh +2 -2
- package/templates/.claude/hooks/scripts/failure-ledger.sh +18 -0
- package/templates/.claude/hooks/scripts/file-change-validator.sh +6 -3
- package/templates/.claude/hooks/scripts/git-delegation-guard.sh +178 -27
- package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +43 -20
- package/templates/.claude/hooks/scripts/omcustom-auto-update.sh +19 -5
- package/templates/.claude/hooks/scripts/rule-deletion-guard.sh +118 -1
- package/templates/.claude/hooks/scripts/schema-validator.sh +8 -11
- package/templates/.claude/hooks/scripts/session-env-check.sh +20 -5
- package/templates/.claude/hooks/scripts/stall-detection-advisor.sh +9 -5
- package/templates/.claude/hooks/scripts/stuck-detector.sh +21 -4
- package/templates/.claude/hooks/scripts/subagent-failure-advisor.sh +48 -29
- package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +15 -8
- package/templates/.claude/ontology/agents.yaml +71 -7
- package/templates/.claude/ontology/graphs/agent-skill.json +64 -5
- package/templates/.claude/ontology/graphs/full-graph.json +236 -3
- package/templates/.claude/ontology/skills.yaml +65 -1
- package/templates/.claude/rules/MUST-agent-design.md +1 -11
- package/templates/.claude/rules/MUST-enforcement-policy.md +7 -7
- package/templates/.claude/rules/MUST-orchestrator-coordination.md +1 -1
- package/templates/.claude/rules/SHOULD-memory-integration.md +1 -1
- package/templates/.claude/settings.json +29 -13
- package/templates/.claude/skills/fsd/SKILL.md +10 -9
- package/templates/.claude/skills/goal/SKILL.md +5 -5
- package/templates/.claude/skills/model-escalation/SKILL.md +16 -12
- package/templates/.claude/skills/omcustom-loop/SKILL.md +4 -4
- package/templates/.claude/skills/pipeline/workflows/auto-dev.yaml +13 -0
- package/templates/guides/claude-code/04-agent-skills.md +1 -1
- package/templates/guides/claude-code/15-version-compatibility.md +47 -47
- package/templates/guides/git-safety/README.md +4 -1
- package/templates/guides/middleware-patterns/README.md +4 -4
- package/templates/guides/multi-model-routing/README.md +1 -7
- package/templates/manifest.json +1 -1
- package/templates/workflows/auto-dev.yaml +13 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# bash-cd-advisor.sh — PreToolUse(Bash): 서브셸 밖 최상위 `cd` 감지 (advisory)
|
|
3
|
+
#
|
|
4
|
+
# 배경 (#1834 / #1816 H2, R005 "Bash `cd`는 세션 cwd로 남습니다"):
|
|
5
|
+
# Bash 도구에서 괄호 밖에 놓인 `cd`는 세션 cwd로 남아(CwdChanged 발화로 확인) 이후 명령의
|
|
6
|
+
# 상대 경로를 어긋나게 한다. `cd X && ( … )`도 `cd`가 서브셸 밖이므로 지속 `cd`다.
|
|
7
|
+
# 안전한 형태는 절대 경로 또는 `( cd <dir> && … )`.
|
|
8
|
+
#
|
|
9
|
+
# 역할:
|
|
10
|
+
# .tool_input.command 에서 괄호 깊이 0, 따옴표/`$( … )`/백틱/히어독 본문 밖의 `cd`/`pushd`/`popd`
|
|
11
|
+
# 명령어 단어를 찾으면 한국어 advisory 한 단락을 전달한다. 그 외에는 아무것도 출력하지 않는다.
|
|
12
|
+
# 명령 시작 위치 = 문장 처음, `&&` `||` `;` 개행 `|` `&` 뒤, 그리고 접두 키워드(`then` `do` `else`
|
|
13
|
+
# `elif` `if` `while` `until` `{` `!` `time` `builtin` `command`)와 선행 `VAR=value` 할당 뒤.
|
|
14
|
+
# 파이프의 마지막이 아닌 구간(`cd x | b`)과 백그라운드(`cd x &`)의 cd 는 서브셸에서 실행되어 cwd가
|
|
15
|
+
# 남지 않으므로 감지하지 않는다. 파이프의 마지막 구간(`a | cd x`)은 감지한다: 이 규칙은 zsh 의미론을
|
|
16
|
+
# 따른다 — Bash 도구의 셸이 zsh(측정: $ZSH_VERSION=5.9)이고 zsh 는 마지막 파이프 요소를 현재 셸에서
|
|
17
|
+
# 실행한다(`zsh -fc 'cd /; echo x | cd /tmp; pwd'` → /tmp; bash 는 `/` 라 남지 않는다).
|
|
18
|
+
#
|
|
19
|
+
# 구현: 스캐너는 awk 한 번의 선형 패스(LC_ALL=C, 바이트 단위)다. 측정(macOS, awk 20200816): 30K자 0.03s.
|
|
20
|
+
# `cd`/`pushd`/`popd` 부분문자열이 없는 명령은 스캐너를 건너뛴다.
|
|
21
|
+
#
|
|
22
|
+
# 전달 경로 (Measured):
|
|
23
|
+
# CC 2.1.310, `claude -p`, haiku(claude-haiku-5-5), 1회 실행, PreToolUse(Bash) 훅.
|
|
24
|
+
# - exit 0 + stderr : 모델에 도달하지 않음 (마커 미관측)
|
|
25
|
+
# - exit 0 + stdout JSON hookSpecificOutput.additionalContext : 모델에 도달
|
|
26
|
+
# ("PreToolUse:Bash hook additional context: <text>" 형태로 모델이 인용)
|
|
27
|
+
# 따라서 additionalContext 로만 전달한다. 미측정: 대화형 세션, 다른 모델.
|
|
28
|
+
#
|
|
29
|
+
# 차단하지 않는다 (R021 advisory-first):
|
|
30
|
+
# 항상 exit 0. permissionDecision/decision 필드를 출력하지 않고 tool input도 수정하지 않는다.
|
|
31
|
+
# fail-open: stdin이 객체가 아니거나 command 가 없거나 jq 가 없으면 조용히 exit 0.
|
|
32
|
+
#
|
|
33
|
+
# 환경변수 override:
|
|
34
|
+
# OMCUSTOM_CD_ADVISOR=off — advisory 비활성화
|
|
35
|
+
#
|
|
36
|
+
# 알려진 한계 (documents … 테스트로 고정):
|
|
37
|
+
# - `eval "cd x"`, `bash -c 'cd x'` 등 문자열 안의 cd 는 따옴표 안이라 감지하지 않는다.
|
|
38
|
+
# - 셸 함수 본문 `f() { cd x; }` 의 cd 는 `)` 뒤라 명령 시작이 아니어서 감지하지 않는다
|
|
39
|
+
# (`{ cd x; }` 그룹 자체는 감지한다).
|
|
40
|
+
# - `case $x in a) cd x ;; esac` 의 case arm 안 cd 는 감지하지 않는다(`)` 뒤).
|
|
41
|
+
# - 히어독 구분자 줄은 `<<[-]WORD` 의 WORD(따옴표 제거)와 일치하는 줄로 판정한다. 히어독 본문은
|
|
42
|
+
# 건너뛰지만, 큰따옴표 문자열 안의 `$( … <<EOF … )` 형태는 큰따옴표 구간을 통째로 불투명하게 취급한다.
|
|
43
|
+
# - bash 로 실행되는 환경에서는 `ls | cd x` 가 cwd 를 바꾸지 않으나 zsh 기준으로 감지한다(위 참조).
|
|
44
|
+
# - `command -p cd x` 처럼 옵션이 낀 접두 형태는 감지하지 않는다.
|
|
45
|
+
|
|
46
|
+
set -uo pipefail
|
|
47
|
+
|
|
48
|
+
[ "${OMCUSTOM_CD_ADVISOR:-}" = "off" ] && exit 0
|
|
49
|
+
command -v jq >/dev/null 2>&1 || exit 0
|
|
50
|
+
|
|
51
|
+
input=$(cat) || exit 0
|
|
52
|
+
cmd=$(printf '%s' "$input" | jq -r 'if type == "object" then (.tool_input.command // "" | if type == "string" then . else "" end) else "" end' 2>/dev/null) || exit 0
|
|
53
|
+
[ -n "$cmd" ] || exit 0
|
|
54
|
+
|
|
55
|
+
# 사전 필터: cwd 변경 명령어 부분문자열이 없으면 스캔하지 않는다.
|
|
56
|
+
case "$cmd" in
|
|
57
|
+
*cd*|*pushd*|*popd*) ;;
|
|
58
|
+
*) exit 0 ;;
|
|
59
|
+
esac
|
|
60
|
+
|
|
61
|
+
# 선형 awk 스캐너: 따옴표 / 괄호 깊이(`(`, `$(` 포함) / 백틱 / 히어독 / 명령 시작 위치 / 구간 종결자 추적.
|
|
62
|
+
# 종료코드 0 = 지속 cd 발견. 입력은 stdin 단일 레코드(RS=\001)로 받는다.
|
|
63
|
+
# shellcheck disable=SC2016
|
|
64
|
+
AWK_PROG='
|
|
65
|
+
BEGIN { RS = "\001" }
|
|
66
|
+
{ s = s $0 }
|
|
67
|
+
function isdelim(c) { return (c == " " || c == "\t" || c == "\n" || c == ";" || c == "&" || c == "|" || c == "(" || c == ")" || c == "<" || c == ">" || c == "\"" || c == "\047" || c == "\\" || c == "`") }
|
|
68
|
+
function scan( n, ch, i, c, nx, pv, q, depth, bt, start, assign, cand, arith, akind, sep, adv, j, w, hn, hd, hdash, k, line, dq, d, dash) {
|
|
69
|
+
n = split(s, ch, "")
|
|
70
|
+
q = ""; depth = 0; bt = 0; start = 1; assign = 0; cand = 0; arith = 0; hn = 0
|
|
71
|
+
i = 1
|
|
72
|
+
while (i <= n) {
|
|
73
|
+
c = ch[i]
|
|
74
|
+
if (q == "\047") { if (c == "\047") q = ""; i++; continue }
|
|
75
|
+
if (q == "\"") {
|
|
76
|
+
if (c == "\\") { i += 2; continue }
|
|
77
|
+
if (c == "\"") q = ""
|
|
78
|
+
i++; continue
|
|
79
|
+
}
|
|
80
|
+
if (c == "\\") { i += 2; start = 0; continue }
|
|
81
|
+
if (c == "\047") { q = "\047"; start = 0; i++; continue }
|
|
82
|
+
if (c == "\"") { q = "\""; start = 0; i++; continue }
|
|
83
|
+
if (c == "`") { bt = 1 - bt; start = bt; assign = 0; i++; continue }
|
|
84
|
+
if (c == "(") { depth++; if (i > 1 && ch[i - 1] == "(") { akind[depth] = 1; arith++ } start = 1; assign = 0; i++; continue }
|
|
85
|
+
if (c == ")") { if (akind[depth]) { akind[depth] = 0; arith-- } if (depth > 0) depth--; start = 0; assign = 0; i++; continue }
|
|
86
|
+
if (c == " " || c == "\t") { if (assign) { start = 1; assign = 0 } i++; continue }
|
|
87
|
+
if (c == "<") {
|
|
88
|
+
nx = ch[i + 1]
|
|
89
|
+
if (nx == "<") {
|
|
90
|
+
if (ch[i + 2] == "<") { i += 3; start = 0; continue }
|
|
91
|
+
if (arith > 0) { i += 2; start = 0; continue }
|
|
92
|
+
i += 2; dash = 0
|
|
93
|
+
if (ch[i] == "-") { dash = 1; i++ }
|
|
94
|
+
while (i <= n && (ch[i] == " " || ch[i] == "\t")) i++
|
|
95
|
+
dq = ""
|
|
96
|
+
while (i <= n) {
|
|
97
|
+
d = ch[i]
|
|
98
|
+
if (d == "\047" || d == "\"" || d == "\\") { i++; continue }
|
|
99
|
+
if (d == " " || d == "\t" || d == "\n" || d == ";" || d == "&" || d == "|" || d == "(" || d == ")" || d == "<" || d == ">") break
|
|
100
|
+
dq = dq d; i++
|
|
101
|
+
}
|
|
102
|
+
hn++; hd[hn] = dq; hdash[hn] = dash
|
|
103
|
+
start = 0; continue
|
|
104
|
+
}
|
|
105
|
+
start = 0; i++; continue
|
|
106
|
+
}
|
|
107
|
+
sep = ""; adv = 1
|
|
108
|
+
if (c == ";" || c == "\n") sep = "term"
|
|
109
|
+
else if (c == "|") {
|
|
110
|
+
nx = ch[i + 1]
|
|
111
|
+
if (nx == "|") { sep = "term"; adv = 2 }
|
|
112
|
+
else if (nx == "&") { sep = "pipe"; adv = 2 }
|
|
113
|
+
else sep = "pipe"
|
|
114
|
+
} else if (c == "&") {
|
|
115
|
+
nx = ch[i + 1]; pv = (i > 1) ? ch[i - 1] : ""
|
|
116
|
+
if (nx == "&") { sep = "term"; adv = 2 }
|
|
117
|
+
else if (pv == ">" || pv == "<" || nx == ">") { start = 0; i++; continue }
|
|
118
|
+
else sep = "bg"
|
|
119
|
+
}
|
|
120
|
+
if (sep != "") {
|
|
121
|
+
if (cand) { if (sep == "term") return 1; cand = 0 }
|
|
122
|
+
start = 1; assign = 0
|
|
123
|
+
i += adv
|
|
124
|
+
if (c == "\n" && hn > 0) {
|
|
125
|
+
for (k = 1; k <= hn; k++) {
|
|
126
|
+
while (i <= n) {
|
|
127
|
+
line = ""
|
|
128
|
+
while (i <= n && ch[i] != "\n") { line = line ch[i]; i++ }
|
|
129
|
+
i++
|
|
130
|
+
if (hdash[k]) sub(/^\t+/, "", line)
|
|
131
|
+
if (line == hd[k]) break
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
hn = 0
|
|
135
|
+
}
|
|
136
|
+
continue
|
|
137
|
+
}
|
|
138
|
+
if (c == "#" && start == 1) {
|
|
139
|
+
while (i <= n && ch[i] != "\n") i++
|
|
140
|
+
continue
|
|
141
|
+
}
|
|
142
|
+
if (start == 1) {
|
|
143
|
+
j = i; w = ""
|
|
144
|
+
while (j <= n && !isdelim(ch[j])) { w = w ch[j]; j++ }
|
|
145
|
+
if (j == i) { start = 0; i++; continue }
|
|
146
|
+
if ((w == "cd" || w == "pushd" || w == "popd") && depth == 0 && bt == 0 && !cand) {
|
|
147
|
+
cand = 1; start = 0; i = j; continue
|
|
148
|
+
}
|
|
149
|
+
if (w == "then" || w == "do" || w == "else" || w == "elif" || w == "if" || w == "while" || w == "until" || w == "time" || w == "builtin" || w == "command" || w == "!" || w == "{") {
|
|
150
|
+
i = j; continue
|
|
151
|
+
}
|
|
152
|
+
if (w ~ /^[A-Za-z_][A-Za-z0-9_]*=/) { assign = 1; start = 0; i = j; continue }
|
|
153
|
+
start = 0; i = j; continue
|
|
154
|
+
}
|
|
155
|
+
i++
|
|
156
|
+
}
|
|
157
|
+
return cand ? 1 : 0
|
|
158
|
+
}
|
|
159
|
+
END { exit (scan() ? 0 : 1) }
|
|
160
|
+
'
|
|
161
|
+
|
|
162
|
+
has_toplevel_cd() {
|
|
163
|
+
printf '%s' "$1" | LC_ALL=C awk "$AWK_PROG" 2>/dev/null
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if has_toplevel_cd "$cmd"; then
|
|
167
|
+
advisory_text='[CD Advisory] 이 Bash 명령에는 서브셸 밖에 놓인 최상위 `cd`(또는 `pushd`/`popd`)가 있습니다. Bash 도구의 `cd`는 세션 cwd로 남아 이후 명령의 상대 경로를 어긋나게 합니다(`cd X && ( … )` 형태도 `cd`가 괄호 밖이므로 동일합니다). 절대 경로를 쓰거나 `( cd <dir> && … )`처럼 서브셸 안에서만 이동하십시오(R005). 이미 의도한 지속 이동이라면 이후 명령의 경로가 이를 전제하는지 확인하십시오.'
|
|
168
|
+
jq -cn --arg ctx "$advisory_text" \
|
|
169
|
+
'{hookSpecificOutput: {hookEventName: "PreToolUse", additionalContext: $ctx}}'
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
exit 0
|
|
@@ -1,11 +1,22 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# Content-Hash Validator — Detect stale file state for Edit operations
|
|
3
|
-
# Trigger: PostToolUse on Read (stores
|
|
4
|
-
# Purpose: Advisory warning when file content changed
|
|
5
|
-
# Protocol
|
|
3
|
+
# Trigger: PostToolUse on Read/Edit/Write (stores or refreshes the hash), PreToolUse on Edit (validates)
|
|
4
|
+
# Purpose: Advisory warning when file content changed outside the model's own Read/Edit/Write
|
|
5
|
+
# Protocol (#1885, per event):
|
|
6
|
+
# PostToolUse (Read|Edit|Write): store/refresh hash of .tool_input.file_path, stdin pass-through to stdout.
|
|
7
|
+
# PreToolUse (Edit): validate; stdout is one JSON line (hookSpecificOutput.additionalContext) or nothing.
|
|
8
|
+
# No stderr, no passthrough (exit-0 stderr never reaches the model; measured CC 2.1.310, #1775).
|
|
9
|
+
# Missing hook_event_name: Read and Write store, Edit VALIDATES (Edit was historically PreToolUse-only).
|
|
10
|
+
# Store: per-path atomic files in /tmp/.claude-content-hashes-${PPID}.d/<cksum of path>
|
|
11
|
+
# (line 1 = hash, line 2 = path). Each entry is written to a unique temp file in the same dir, then
|
|
12
|
+
# mv -f'd onto the entry, so parallel hook calls never share a read-modify-write file. No ring buffer:
|
|
13
|
+
# the directory is session-scoped (PPID) and entries are tiny. Every state operation is wrapped so a
|
|
14
|
+
# failure never aborts or leaks stderr.
|
|
15
|
+
# Known limitation: a file rewritten after its hash was stored (a parallel PostToolUse formatter hook such
|
|
16
|
+
# as prettier/gofmt/ruff, or a Bash edit) can still trigger the warning on the next Edit.
|
|
6
17
|
# Always exits 0 (advisory only, never blocks)
|
|
7
18
|
|
|
8
|
-
set -
|
|
19
|
+
set -uo pipefail
|
|
9
20
|
|
|
10
21
|
# Dependency check: exit silently if jq not available
|
|
11
22
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
@@ -13,67 +24,73 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
13
24
|
input=$(cat)
|
|
14
25
|
|
|
15
26
|
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0.
|
|
16
|
-
# Hooks must never crash (R021)
|
|
27
|
+
# Hooks must never crash (R021). (#1650)
|
|
17
28
|
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
18
29
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
30
|
+
STORE_DIR="/tmp/.claude-content-hashes-${PPID}.d"
|
|
31
|
+
|
|
32
|
+
ctx=""
|
|
33
|
+
add() { if [ -z "$ctx" ]; then ctx="$1"; else ctx="${ctx}"$'\n'"$1"; fi; }
|
|
34
|
+
|
|
35
|
+
tool_name=$(printf '%s\n' "$input" | jq -r '.tool_name // "unknown"' 2>/dev/null || echo unknown)
|
|
36
|
+
event=$(printf '%s\n' "$input" | jq -r '.hook_event_name // ""' 2>/dev/null || echo "")
|
|
37
|
+
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // ""' 2>/dev/null || echo "")
|
|
38
|
+
|
|
39
|
+
mode="other"
|
|
40
|
+
if [ "$event" = "PreToolUse" ]; then
|
|
41
|
+
[ "$tool_name" = "Edit" ] && mode="validate"
|
|
42
|
+
elif [ "$event" = "" ]; then
|
|
43
|
+
case "$tool_name" in
|
|
44
|
+
Edit) mode="validate" ;;
|
|
45
|
+
Read|Write) mode="store" ;;
|
|
46
|
+
esac
|
|
47
|
+
else
|
|
48
|
+
case "$tool_name" in Read|Edit|Write) mode="store" ;; esac
|
|
49
|
+
fi
|
|
23
50
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# Store content hash for the file that was just read
|
|
27
|
-
file_path=$(printf '%s\n' "$input" | jq -r '.tool_input.file_path? // ""')
|
|
51
|
+
file_hash() { md5 -q "$1" 2>/dev/null || md5sum "$1" 2>/dev/null | cut -d' ' -f1 || echo "unknown"; }
|
|
52
|
+
entry_for() { printf '%s' "$1" | cksum | cut -d' ' -f1; }
|
|
28
53
|
|
|
54
|
+
case "$mode" in
|
|
55
|
+
"store")
|
|
29
56
|
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
mv "${HASH_STORE}.tmp" "$HASH_STORE" 2>/dev/null || true
|
|
38
|
-
fi
|
|
39
|
-
|
|
40
|
-
jq -cn \
|
|
41
|
-
--arg path "$file_path" \
|
|
42
|
-
--arg hash "$content_hash" \
|
|
43
|
-
--arg ts "$timestamp" \
|
|
44
|
-
'{path: $path, hash: $hash, stored_at: $ts}' >> "$HASH_STORE" 2>/dev/null || true
|
|
57
|
+
{
|
|
58
|
+
content_hash=$(file_hash "$file_path")
|
|
59
|
+
mkdir -p "$STORE_DIR"
|
|
60
|
+
tmp=$(mktemp "$STORE_DIR/.tmp.XXXXXX")
|
|
61
|
+
printf '%s\n%s\n' "$content_hash" "$file_path" > "$tmp"
|
|
62
|
+
mv -f "$tmp" "$STORE_DIR/$(entry_for "$file_path")"
|
|
63
|
+
} 2>/dev/null || true
|
|
45
64
|
fi
|
|
46
65
|
;;
|
|
47
66
|
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
stored_hash=$(
|
|
67
|
+
"validate")
|
|
68
|
+
if [ -n "$file_path" ] && [ -f "$file_path" ]; then
|
|
69
|
+
entry="$STORE_DIR/$(entry_for "$file_path")"
|
|
70
|
+
stored_hash=""
|
|
71
|
+
stored_path=""
|
|
72
|
+
{ stored_hash=$(sed -n 1p "$entry"); stored_path=$(sed -n 2p "$entry"); } 2>/dev/null || true
|
|
54
73
|
|
|
55
|
-
if [ -n "$stored_hash" ] && [ "$stored_hash" != "unknown" ]; then
|
|
56
|
-
current_hash=$(
|
|
74
|
+
if [ -n "$stored_hash" ] && [ "$stored_hash" != "unknown" ] && [ "$stored_path" = "$file_path" ]; then
|
|
75
|
+
current_hash=$(file_hash "$file_path")
|
|
57
76
|
|
|
58
77
|
if [ "$current_hash" != "unknown" ] && [ "$stored_hash" != "$current_hash" ]; then
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
78
|
+
add "[Content-Hash] WARNING: $(basename "$file_path") may have changed since last Read"
|
|
79
|
+
add "[Content-Hash] Stored hash: ${stored_hash:0:8}... Current: ${current_hash:0:8}..."
|
|
80
|
+
add "[Content-Hash] Advisory: re-read the file before editing if unsure"
|
|
62
81
|
fi
|
|
63
82
|
fi
|
|
64
83
|
fi
|
|
65
84
|
;;
|
|
66
85
|
esac
|
|
67
86
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
tail -200 "$HASH_STORE" > "${HASH_STORE}.tmp"
|
|
73
|
-
mv "${HASH_STORE}.tmp" "$HASH_STORE"
|
|
87
|
+
if [ "$mode" = "validate" ]; then
|
|
88
|
+
# Emit advisory to the model (#1885); silent when nothing to advise.
|
|
89
|
+
if [ -n "$ctx" ]; then
|
|
90
|
+
jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
|
|
74
91
|
fi
|
|
92
|
+
else
|
|
93
|
+
# PostToolUse paths and others: unchanged pass-through
|
|
94
|
+
printf '%s\n' "$input"
|
|
75
95
|
fi
|
|
76
|
-
|
|
77
|
-
# Always pass through
|
|
78
|
-
printf '%s\n' "$input"
|
|
79
96
|
exit 0
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
2
|
# R001/R021 destructive git guard hook
|
|
3
3
|
# Warns before commands that can discard working tree, untracked files, or branches.
|
|
4
|
-
# Advisory tier only: emits
|
|
4
|
+
# Advisory tier only: emits guidance, records PPID-scoped warnings, exits 0.
|
|
5
|
+
# Channel (#1885): exit-0 stderr never reaches the model; advice is delivered via stdout JSON
|
|
6
|
+
# hookSpecificOutput.additionalContext (measured CC 2.1.310, #1775). No stdin passthrough, no stderr.
|
|
5
7
|
#
|
|
6
8
|
# PPID Scoping Convention:
|
|
7
9
|
# Session-scoped temp files MUST use $PPID (Claude Code parent PID), not $$.
|
|
@@ -26,35 +28,48 @@ fi
|
|
|
26
28
|
|
|
27
29
|
VIOLATION_FILE="/tmp/.claude-destructive-git-guard-${PPID}"
|
|
28
30
|
matched=""
|
|
31
|
+
ctx=""
|
|
32
|
+
# Optional `-C <path>` / `-c <k=v>` option pairs between `git` and the subcommand (L10).
|
|
33
|
+
GIT_PFX='(^|[;&|[:space:]])git([[:space:]]+(-C[[:space:]]+("[^"]*"|'"'"'[^'"'"']*'"'"'|[^[:space:]]+)|-c[[:space:]]+[^[:space:]]+))*[[:space:]]+'
|
|
34
|
+
add() { if [ -z "$ctx" ]; then ctx="$1"; else ctx="${ctx}"$'\n'"$1"; fi; }
|
|
29
35
|
|
|
30
|
-
if printf '%s\n' "$command_text" | grep -Eq
|
|
36
|
+
if printf '%s\n' "$command_text" | grep -Eq "${GIT_PFX}reset[[:space:]]+--hard([[:space:]]|$)"; then
|
|
31
37
|
matched="git reset --hard"
|
|
32
|
-
elif printf '%s\n' "$command_text" | grep -Eq
|
|
38
|
+
elif printf '%s\n' "$command_text" | grep -Eq "${GIT_PFX}clean[[:space:]]+([^;&|]*[[:space:]])?(-[A-Za-z]*f[A-Za-z]*|--force)([[:space:]]|$)"; then
|
|
33
39
|
matched="git clean"
|
|
34
|
-
elif printf '%s\n' "$command_text" | grep -Eq
|
|
40
|
+
elif printf '%s\n' "$command_text" | grep -Eq "${GIT_PFX}restore([[:space:]]+--[^;&|[:space:]]+)*([[:space:]]+(\.|\*|/|--worktree|--staged))*([[:space:]]|$)"; then
|
|
35
41
|
matched="git restore"
|
|
36
|
-
elif printf '%s\n' "$command_text" | grep -Eq
|
|
42
|
+
elif printf '%s\n' "$command_text" | grep -Eq "${GIT_PFX}checkout[[:space:]]+--[[:space:]]+(\.|\*|/)([[:space:]]|$)"; then
|
|
37
43
|
matched="git checkout -- ."
|
|
38
|
-
elif printf '%s\n' "$command_text" | grep -Eq
|
|
44
|
+
elif printf '%s\n' "$command_text" | grep -Eq "${GIT_PFX}branch[[:space:]]+-D([[:space:]]|$)"; then
|
|
39
45
|
matched="git branch -D"
|
|
40
46
|
fi
|
|
41
47
|
|
|
42
48
|
if [ -n "$matched" ]; then
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
49
|
+
add "[Hook] DESTRUCTIVE GIT WARNING: detected '$matched' in Bash command"
|
|
50
|
+
add "[Hook] Advisory only (R021): command is not blocked, but it may discard local work."
|
|
51
|
+
add "[Hook] Before proceeding, verify git status and preserve work with git stash or a WIP commit."
|
|
52
|
+
add "[Hook] Recovery: use 'git reflog' to find prior HEADs; for deleted branches, check 'git reflog --all'."
|
|
47
53
|
|
|
48
|
-
if printf '%s\n' "$command_text" | grep -Eq
|
|
49
|
-
|
|
54
|
+
if printf '%s\n' "$command_text" | grep -Eq "${GIT_PFX}branch[[:space:]]+-D([[:space:]]|$)"; then
|
|
55
|
+
add "[Hook] Branch deletion note: prefer 'git branch -d' first so Git checks for unmerged commits."
|
|
50
56
|
fi
|
|
51
57
|
|
|
52
|
-
|
|
58
|
+
# State writes are silent and non-fatal (no stderr, no abort).
|
|
59
|
+
{ printf '%s %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$matched" >> "$VIOLATION_FILE"; } 2>/dev/null || true
|
|
53
60
|
violation_count=$(wc -l < "$VIOLATION_FILE" 2>/dev/null | tr -d ' ')
|
|
54
|
-
|
|
55
|
-
|
|
61
|
+
# Count line once per session: count >= 3 AND the atomic marker mkdir wins (parallel-safe).
|
|
62
|
+
if [ "${violation_count:-0}" -ge 3 ] && { mkdir "/tmp/.claude-destructive-git-threshold-${PPID}"; } 2>/dev/null; then
|
|
63
|
+
add "[Hook] Destructive git warnings: ${violation_count} this session (advisory telemetry; R021 promotion needs user approval)"
|
|
56
64
|
fi
|
|
57
65
|
fi
|
|
58
66
|
|
|
59
|
-
#
|
|
60
|
-
|
|
67
|
+
# Emit advisory to the model (#1885); silent when nothing matched.
|
|
68
|
+
if [ -n "$ctx" ]; then
|
|
69
|
+
if command -v jq >/dev/null 2>&1; then
|
|
70
|
+
jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
|
|
71
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
72
|
+
CTX="$ctx" python3 -c 'import json,os; print(json.dumps({"hookSpecificOutput":{"hookEventName":"PreToolUse","additionalContext":os.environ["CTX"]}}))'
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
exit 0
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
#
|
|
10
10
|
# 역할:
|
|
11
11
|
# (1) 짧은 진행 지시이고 (2) 원인 언급이 없으며 (3) 이 세션에 기록된 도구 실패가 있을 때,
|
|
12
|
-
# Claude에게 "
|
|
12
|
+
# Claude에게 "재시도 전에 원인 가설 한 줄을 명시하고, 사용자가 묻지 말라고 지시하지 않았을 때만 확인하라"는 advisory를 전달한다 (#1834: 훅은 세션 지시를 볼 수 없다).
|
|
13
13
|
#
|
|
14
14
|
# 왜 사용자가 아니라 Claude에게 전달하는가:
|
|
15
15
|
# hookSpecificOutput.additionalContext는 모델 컨텍스트로만 들어간다(사용자에게 표시되지 않음).
|
|
@@ -121,7 +121,7 @@ fail_tools=$(tail -n 300 "$LEDGER" 2>/dev/null \
|
|
|
121
121
|
| sort | uniq -c | sort -rn | head -3 \
|
|
122
122
|
| awk '{printf "%s(%s) ", $2, $1}') || fail_tools=""
|
|
123
123
|
|
|
124
|
-
advisory_text=$(printf '[FAIL Advisory] 이 세션에 도구 실패 %s건이 기록되어 있습니다 (%s). 방금 입력은 원인 언급이 없는 진행 지시입니다. 곧바로 재시도하지 말고, 먼저 직전 실패의 원인 가설을 한 줄로
|
|
124
|
+
advisory_text=$(printf '[FAIL Advisory] 이 세션에 도구 실패 %s건이 기록되어 있습니다 (%s). 방금 입력은 원인 언급이 없는 진행 지시입니다. 곧바로 재시도하지 말고, 먼저 직전 실패의 원인 가설을 한 줄로 명시하십시오. 사용자가 묻지 말고 진행하라고 지시하지 않은 경우에만 "이 진단이 맞는지" 짧게 한 번 확인하고, 그런 지시가 있었다면 확인 없이 가설만 기록하고 진행하십시오.' \
|
|
125
125
|
"$fail_count" "${fail_tools:-unknown}")
|
|
126
126
|
|
|
127
127
|
# 사람이 보는 감사 추적용 (exit 0에서 stderr는 모델에 전달되지 않음)
|
|
@@ -18,6 +18,15 @@
|
|
|
18
18
|
# - 어떤 실패에도 exit 0 — 원장 기록 실패가 본 작업을 막아서는 안 된다 (R021 advisory-first).
|
|
19
19
|
# - 명령/에러 문자열은 절단하여 기록한다 (원장 비대화 방지).
|
|
20
20
|
#
|
|
21
|
+
# exit_code 필드 (#1834 / #1814 R2):
|
|
22
|
+
# PostToolUseFailure에는 숫자 exit code 필드가 없고 `error` 문자열("Exit code 3")만 온다
|
|
23
|
+
# (실측: CC 2.1.310, `claude -p`, haiku, 1회, `sh -c 'exit 3'`). 그래서 `^Exit code ([0-9]+)`
|
|
24
|
+
# 일 때만 정수를 파싱해 exit_code로 기록하고, 아니면 null이다.
|
|
25
|
+
# 기대 exit 화이트리스트는 두지 않는다 — 동기였던 `git grep` no-match(exit 1)는 같은
|
|
26
|
+
# 조건에서 PostToolUseFailure를 발생시키지 않고, returnCodeInterpretation "No matches found"를
|
|
27
|
+
# 가진 PostToolUse로만 도달했다(실측). `false`, 일반 `grep` 등 다른 exit-1 명령, 대화형 세션,
|
|
28
|
+
# 다른 모델/버전은 미측정이다.
|
|
29
|
+
#
|
|
21
30
|
# 환경변수 override:
|
|
22
31
|
# OMCUSTOM_FAILURE_LEDGER=off — 기록 완전 비활성화
|
|
23
32
|
# OMCUSTOM_ERROR_LEDGER=<path> — 원장 경로 override (기본: ~/.claude/error-ledger.jsonl)
|
|
@@ -36,6 +45,14 @@ if ! command -v jq >/dev/null 2>&1; then
|
|
|
36
45
|
exit 0
|
|
37
46
|
fi
|
|
38
47
|
|
|
48
|
+
# HOME guard: with no override, skip when HOME is unset, empty or not absolute
|
|
49
|
+
# (otherwise `set -u` exits 1 on an unset HOME, or the ledger lands in a relative path).
|
|
50
|
+
if [ -z "${OMCUSTOM_ERROR_LEDGER:-}" ]; then
|
|
51
|
+
case "${HOME:-}" in
|
|
52
|
+
/*) ;;
|
|
53
|
+
*) exit 0 ;;
|
|
54
|
+
esac
|
|
55
|
+
fi
|
|
39
56
|
LEDGER="${OMCUSTOM_ERROR_LEDGER:-${HOME}/.claude/error-ledger.jsonl}"
|
|
40
57
|
|
|
41
58
|
if ! mkdir -p "$(dirname "$LEDGER")" 2>/dev/null; then
|
|
@@ -68,6 +85,7 @@ printf '%s' "$input" \
|
|
|
68
85
|
then (.tool_input.command // .tool_input.file_path // "")
|
|
69
86
|
else "" end) | tostring | .[0:160]),
|
|
70
87
|
interrupt: (.is_interrupt == true),
|
|
88
|
+
exit_code: ((.error | if type == "string" then (capture("^Exit code (?<c>[0-9]+)") | .c | tonumber) else null end) // null),
|
|
71
89
|
err: ((.error
|
|
72
90
|
// .tool_error
|
|
73
91
|
// (if (.tool_response | type) == "object"
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
input=$(cat)
|
|
6
6
|
file_path=$(printf '%s\n' "$input" | jq -r '.file_path // ""' 2>/dev/null)
|
|
7
|
-
change_type=$(printf '%s\n' "$input" | jq -r '.change_type // "modified"' 2>/dev/null)
|
|
7
|
+
change_type=$(printf '%s\n' "$input" | jq -r '.event // .change_type // "modified"' 2>/dev/null)
|
|
8
8
|
|
|
9
9
|
if [ -z "$file_path" ]; then
|
|
10
10
|
printf '%s\n' "$input"
|
|
@@ -14,11 +14,14 @@ fi
|
|
|
14
14
|
echo "[Hook] External file change detected: ${change_type} ${file_path}" >&2
|
|
15
15
|
|
|
16
16
|
# Warn about important files
|
|
17
|
+
# Arms mirror the hooks.json FileChanged matcher (literal working-directory filenames).
|
|
18
|
+
# Nested paths such as .claude/hooks/hooks.json or .claude/settings*.json cannot be matched
|
|
19
|
+
# (the matcher only registers literal filenames in the working directory), so no arm for them.
|
|
17
20
|
case "$file_path" in
|
|
18
|
-
|
|
21
|
+
CLAUDE.md|*/CLAUDE.md)
|
|
19
22
|
echo "[Hook] WARNING: Configuration file changed externally — re-read recommended" >&2
|
|
20
23
|
;;
|
|
21
|
-
|
|
24
|
+
package-lock.json|*/package-lock.json|pnpm-lock.yaml|*/pnpm-lock.yaml|*.lock)
|
|
22
25
|
echo "[Hook] Lock file changed — dependency state may have shifted" >&2
|
|
23
26
|
;;
|
|
24
27
|
esac
|