oh-my-customcode 2.0.8 → 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/dist/cli/index.js +1 -1
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/templates/.claude/hooks/hooks.json +1 -1
- package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +47 -22
- 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/git-delegation-guard.sh +178 -27
- package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +43 -20
- package/templates/.claude/hooks/scripts/schema-validator.sh +8 -11
- package/templates/.claude/rules/MUST-agent-design.md +1 -11
- package/templates/.claude/rules/MUST-enforcement-policy.md +4 -4
- package/templates/.claude/settings.json +1 -1
- package/templates/.claude/skills/model-escalation/SKILL.md +16 -12
- package/templates/guides/claude-code/15-version-compatibility.md +3 -3
- 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/dist/cli/index.js
CHANGED
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -505,7 +505,7 @@
|
|
|
505
505
|
"description": "Advisory cost cap monitoring — warn when session cost approaches configurable limit"
|
|
506
506
|
},
|
|
507
507
|
{
|
|
508
|
-
"matcher": "tool == \"Read\"",
|
|
508
|
+
"matcher": "tool == \"Read\" || tool == \"Edit\" || tool == \"Write\"",
|
|
509
509
|
"hooks": [
|
|
510
510
|
{
|
|
511
511
|
"type": "command",
|
|
@@ -7,7 +7,11 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
7
7
|
# Agent Teams Advisor Hook
|
|
8
8
|
# Trigger: PreToolUse, tool == "Task" || tool == "Agent"
|
|
9
9
|
# Purpose: Track Agent/Task tool usage count per session and warn when Agent Teams may be more appropriate
|
|
10
|
-
# Protocol: stdin JSON -> process ->
|
|
10
|
+
# Protocol: stdin JSON -> process -> exit 0 always (advisory only)
|
|
11
|
+
# Once per session (#1885): each distinct advisory key is shown at most once, claimed atomically via a
|
|
12
|
+
# per-key marker dir under /tmp/.claude-teams-advisory-shown-${PPID}.d/ (parallel-hook safe).
|
|
13
|
+
# Channel (#1885): exit-0 stderr never reaches the model (CC 2.1.310 probe, #1775 comment 6405197553);
|
|
14
|
+
# the advisory is delivered as stdout JSON hookSpecificOutput.additionalContext. No stdin passthrough.
|
|
11
15
|
|
|
12
16
|
input=$(cat)
|
|
13
17
|
|
|
@@ -25,7 +29,6 @@ ENV_STATUS="/tmp/.claude-env-status-${PPID}"
|
|
|
25
29
|
if [ -f "$ENV_STATUS" ]; then
|
|
26
30
|
teams_status=$(grep "agent_teams=" "$ENV_STATUS" 2>/dev/null | cut -d= -f2 || echo "unknown")
|
|
27
31
|
if [ "$teams_status" != "enabled" ]; then
|
|
28
|
-
printf '%s\n' "$input"
|
|
29
32
|
exit 0
|
|
30
33
|
fi
|
|
31
34
|
fi
|
|
@@ -36,6 +39,9 @@ BATCH_ISSUES=0
|
|
|
36
39
|
WORKFLOW_FILE=$(ls /tmp/.claude-workflow-*-"${PPID}".json 2>/dev/null | head -1 || true)
|
|
37
40
|
if [ -n "$WORKFLOW_FILE" ] && [ -f "$WORKFLOW_FILE" ]; then
|
|
38
41
|
BATCH_ISSUES=$(jq -r '.issue_count // 0' "$WORKFLOW_FILE" 2>/dev/null || echo 0)
|
|
42
|
+
# Sanitize before arithmetic: non-numeric/over-long -> 0; 10# avoids base-8 errors on leading zeros.
|
|
43
|
+
case "$BATCH_ISSUES" in ''|*[!0-9]*|??????????*) BATCH_ISSUES=0 ;; esac
|
|
44
|
+
BATCH_ISSUES=$((10#$BATCH_ISSUES))
|
|
39
45
|
fi
|
|
40
46
|
|
|
41
47
|
# Also check release-plan context (existence only — if file exists, treat as batch)
|
|
@@ -48,38 +54,57 @@ fi
|
|
|
48
54
|
|
|
49
55
|
# Extract task info from input
|
|
50
56
|
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // "unknown"')
|
|
51
|
-
prompt_preview=$(printf '%s\n' "$input" | jq -r '.tool_input.description? // ""
|
|
57
|
+
prompt_preview=$(printf '%s\n' "$input" | jq -r '(.tool_input.description? // "") | tostring | .[0:60]')
|
|
52
58
|
|
|
53
59
|
# Session-scoped counter using parent PID as session identifier
|
|
54
60
|
COUNTER_FILE="/tmp/.claude-task-count-${PPID}"
|
|
55
61
|
|
|
56
62
|
# Read and increment counter
|
|
57
63
|
if [ -f "$COUNTER_FILE" ]; then
|
|
58
|
-
COUNT=$(cat "$COUNTER_FILE")
|
|
64
|
+
COUNT=$(cat "$COUNTER_FILE" 2>/dev/null || echo 0)
|
|
65
|
+
case "$COUNT" in ''|*[!0-9]*|??????????*) COUNT=0 ;; esac
|
|
66
|
+
COUNT=$((10#$COUNT))
|
|
59
67
|
COUNT=$((COUNT + 1))
|
|
60
68
|
else
|
|
61
69
|
COUNT=1
|
|
62
70
|
fi
|
|
63
|
-
echo "$COUNT" > "$COUNTER_FILE"
|
|
71
|
+
{ echo "$COUNT" > "$COUNTER_FILE"; } 2>/dev/null || true
|
|
72
|
+
|
|
73
|
+
SHOWN_DIR="/tmp/.claude-teams-advisory-shown-${PPID}.d"
|
|
74
|
+
# fresh KEY: atomic claim via mkdir of a per-key marker dir (hash of KEY; no shared file, so no
|
|
75
|
+
# check-then-append race and no newline pollution). Success => emit. State failures are silent;
|
|
76
|
+
# if the marker dir itself is unusable we fail open (emit) rather than lose the advisory.
|
|
77
|
+
fresh() {
|
|
78
|
+
{ mkdir -p "$SHOWN_DIR"; } 2>/dev/null || true
|
|
79
|
+
[ -d "$SHOWN_DIR" ] || return 0
|
|
80
|
+
local h
|
|
81
|
+
h=$(printf '%s' "$1" | cksum | cut -d' ' -f1) || h=""
|
|
82
|
+
[ -n "$h" ] || return 0
|
|
83
|
+
if { mkdir "$SHOWN_DIR/$h"; } 2>/dev/null; then return 0; fi
|
|
84
|
+
return 1
|
|
85
|
+
}
|
|
64
86
|
|
|
65
87
|
# Warn when batch context detected (even on first call) or from 2nd call onward
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
88
|
+
ctx=""
|
|
89
|
+
if [ "$BATCH_ISSUES" -ge 3 ] && [ "$COUNT" -eq 1 ] && fresh "batch:${BATCH_ISSUES}"; then
|
|
90
|
+
ctx="
|
|
91
|
+
--- [R018 Advisor] Batch context detected (${BATCH_ISSUES} issues) ---
|
|
92
|
+
RECOMMENDATION: Use Agent Teams (TeamCreate) for this batch.
|
|
93
|
+
Current: Agent(${agent_type}) -- ${prompt_preview}
|
|
94
|
+
-----------------------------------------------------------"
|
|
95
|
+
elif [ "$COUNT" -ge 2 ] && fresh "repeat"; then
|
|
96
|
+
ctx="
|
|
97
|
+
--- [R018 Advisor] Agent/Task tool call #${COUNT} in this session ---
|
|
98
|
+
WARNING: Multiple Task calls detected. Consider Agent Teams if:
|
|
99
|
+
* 3+ agents needed for this work
|
|
100
|
+
* Review -> fix -> re-review cycle exists
|
|
101
|
+
* Agents need shared state or coordination
|
|
102
|
+
Current: Agent(${agent_type}) -- ${prompt_preview}
|
|
103
|
+
-----------------------------------------------------------"
|
|
81
104
|
fi
|
|
82
105
|
|
|
83
|
-
#
|
|
84
|
-
|
|
106
|
+
# Advisory only, never blocks: emit additionalContext when there is something to say
|
|
107
|
+
if [ -n "$ctx" ]; then
|
|
108
|
+
jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
|
|
109
|
+
fi
|
|
85
110
|
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
|
|
@@ -1,7 +1,69 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
2
|
+
# git-delegation-guard.sh — PreToolUse(Task|Agent): R010 git 위임 감지 (advisory, #1885)
|
|
3
|
+
#
|
|
4
|
+
# Warns when a delegation prompt to a non-mgr-gitnerd agent mentions git write keywords.
|
|
5
|
+
# Advisory only: always exit 0, never blocks, never modifies tool input.
|
|
6
|
+
#
|
|
7
|
+
# Delivery (#1885): exit 0 + stderr never reaches the model (measured, #1775 comment
|
|
8
|
+
# 6405197553). The warning is therefore emitted as ONE stdout JSON line
|
|
9
|
+
# hookSpecificOutput.additionalContext; nothing is written to stderr and the input is
|
|
10
|
+
# no longer echoed back. No warning -> no output.
|
|
11
|
+
#
|
|
12
|
+
# Precision (#1885): a keyword occurrence does NOT count when (case-insensitive, LC_ALL=C bytes;
|
|
13
|
+
# U+2019 is folded to `'`; `git -C <path>` / `git -c k=v` option groups are stripped first so
|
|
14
|
+
# `git -C /repo commit` is treated like `git commit`):
|
|
15
|
+
# (a) a negation token appears BEFORE it on the same line. English tokens are whole words
|
|
16
|
+
# (`-` and `_` stay inside words, so `--no-rebase`, `no-op`, `not_found` do not count):
|
|
17
|
+
# not, never, no, don't, avoid, without, forbidden, prohibited (so "do not" is covered);
|
|
18
|
+
# Korean substrings 금지, 말 것, 하지 마 (bare "않" was removed: too broad);
|
|
19
|
+
# (b) it sits in a prohibition block. Header words (word-bounded, after list/markdown markers):
|
|
20
|
+
# Forbidden, Prohibited, Do not, Don't, Never, Avoid, 금지, 하지 말 것.
|
|
21
|
+
# - colon header: a line starting with a header word whose `:` or full-width `:` ends the
|
|
22
|
+
# header phrase (at most 4 words before it). The block runs to the next blank line
|
|
23
|
+
# or the next non-prohibition label line (`Allowed: …`, `Steps:`, `**Steps**:`); a
|
|
24
|
+
# prohibition label (`Never: …`) continues it.
|
|
25
|
+
# - markdown heading (`#`..`######` + space; `#1885` is not a heading) starting with a header
|
|
26
|
+
# word (colon optional). The block covers only the FIRST paragraph or list after the
|
|
27
|
+
# heading: blank lines directly after the heading are skipped, then it runs through
|
|
28
|
+
# consecutive non-blank lines and ends at the next blank line, heading, or label line
|
|
29
|
+
# (`**Bold**` or a non-list line ending in a colon);
|
|
30
|
+
# - header colons count only when followed by whitespace/end of line (`https://x`, `10:30`
|
|
31
|
+
# do not); Korean header words need a boundary (whitespace, colon or end: `금지어` is not
|
|
32
|
+
# a header). Lines inside ``` fences never open or close blocks (fence lines themselves
|
|
33
|
+
# count as block content);
|
|
34
|
+
# (c) it is a read-only query:
|
|
35
|
+
# - `git branch` followed by --list, -l, --show-current, --contains, --all, --merged,
|
|
36
|
+
# --no-merged, -a, -r, -v or -vv, unless the same command also has -d/-D/--delete/-m/-M/
|
|
37
|
+
# -c/-C (`git branch -r -d origin/x` is a write and fires);
|
|
38
|
+
# - `git tag` followed by --list, -l, --contains, --points-at or --sort;
|
|
39
|
+
# - bare `git tag` / `git branch` used as a noun: followed by end of line, punctuation, `+`,
|
|
40
|
+
# a backtick, an opening bracket, or whitespace + a common prose word (is/the/and/...).
|
|
41
|
+
# `git branch feature-x` / `git tag v1` are creations and still fire.
|
|
42
|
+
# Keyword boundary: a keyword matches only if the next char is not [A-Za-z0-9_-] and the
|
|
43
|
+
# previous char is not [A-Za-z0-9_] (`git merge-base` does not match `git merge`; "legit
|
|
44
|
+
# commit" does not match `git commit`). Quoted `-C "/my repo"` / `-c 'k=v w'` arguments with
|
|
45
|
+
# spaces are stripped like unquoted ones.
|
|
46
|
+
# Warn only if at least one occurrence survives. The keyword set and the mgr-gitnerd
|
|
47
|
+
# exemption are unchanged. A non-string prompt is treated as empty; a missing subagent_type
|
|
48
|
+
# is shown as (unspecified).
|
|
49
|
+
#
|
|
50
|
+
# Measured precision: replay (8 newest main transcripts, dedup by tool_use id; subagent
|
|
51
|
+
# transcripts carry no Agent calls): old 167 -> 23 of 752 inputs; 0 clear true positives found
|
|
52
|
+
# in this repo's corpus. The hook stays model-visible (additionalContext, worded as a
|
|
53
|
+
# conditional advisory) because the base rate in installed projects differs [가설].
|
|
54
|
+
#
|
|
55
|
+
# Known false negatives (accepted trade-offs):
|
|
56
|
+
# - a real command after a negation word on the same line ("Do not skip this: git commit now"
|
|
57
|
+
# is read as a colon header and skipped with its block);
|
|
58
|
+
# - a real command inside a prohibition block not separated by a blank line (colon header) or
|
|
59
|
+
# placed in the first paragraph/list under a Forbidden heading;
|
|
60
|
+
# - prose "git tag"/"git branch" followed by a stop-listed word is treated as a noun.
|
|
61
|
+
# Known false positive (SOV trade-off): Korean "git commit 금지" (token after the keyword) is
|
|
62
|
+
# NOT excluded by (a) unless it sits in a block (b), so it still warns.
|
|
63
|
+
#
|
|
64
|
+
# State writes (violation file, threshold marker) are best-effort: failures are silent and never
|
|
65
|
+
# abort or leak to stderr. The per-session count line is emitted once, when the count is >= 3
|
|
66
|
+
# and the atomic marker dir /tmp/.claude-r010-threshold-${PPID} is created by this firing.
|
|
5
67
|
#
|
|
6
68
|
# PPID Scoping Convention:
|
|
7
69
|
# All session-scoped temp files MUST use $PPID (Claude Code parent PID),
|
|
@@ -14,22 +76,18 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
14
76
|
|
|
15
77
|
input=$(cat)
|
|
16
78
|
|
|
17
|
-
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0
|
|
18
|
-
# instead of reflecting garbage back on stdout. The sibling hooks (audit-log.sh,
|
|
19
|
-
# model-escalation-advisor.sh, task-outcome-recorder.sh) all carry this guard;
|
|
20
|
-
# this one did not, so a non-object payload was echoed through verbatim. (#1656 F)
|
|
79
|
+
# Guard: non-object stdin (non-JSON / JSON array / empty) — swallow and exit 0. (#1656 F)
|
|
21
80
|
printf '%s' "$input" | jq -e 'type=="object"' >/dev/null 2>&1 || exit 0
|
|
22
81
|
|
|
23
|
-
# `?` suppresses jq
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
# and advisory-only even if jq itself fails for an unrelated reason.
|
|
27
|
-
agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type? // ""' 2>/dev/null || echo "")
|
|
28
|
-
prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt? // ""' 2>/dev/null || echo "")
|
|
82
|
+
# `?` suppresses jq errors when tool_input is a scalar/array (#1656 A).
|
|
83
|
+
agent_type=$(printf '%s\n' "$input" | jq -r '(.tool_input.subagent_type? | strings) // ""' 2>/dev/null || echo "")
|
|
84
|
+
prompt=$(printf '%s\n' "$input" | jq -r '(.tool_input.prompt? | strings) // ""' 2>/dev/null || echo "")
|
|
29
85
|
|
|
30
86
|
# R010 violation tracking file (PPID-scoped for session persistence)
|
|
31
87
|
VIOLATION_FILE="/tmp/.claude-r010-violations-${PPID}"
|
|
32
88
|
|
|
89
|
+
ctx=""
|
|
90
|
+
|
|
33
91
|
# Only warn when the delegated agent is NOT mgr-gitnerd
|
|
34
92
|
if [ "$agent_type" != "mgr-gitnerd" ]; then
|
|
35
93
|
git_keywords=(
|
|
@@ -45,23 +103,116 @@ if [ "$agent_type" != "mgr-gitnerd" ]; then
|
|
|
45
103
|
"git cherry-pick"
|
|
46
104
|
"git tag"
|
|
47
105
|
)
|
|
106
|
+
kw_list=$(IFS='|'; printf '%s' "${git_keywords[*]}")
|
|
48
107
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
108
|
+
# First keyword (in list order) with at least one surviving occurrence.
|
|
109
|
+
keyword=$(printf '%s\n' "$prompt" | LC_ALL=C awk -v kws="$kw_list" '
|
|
110
|
+
function hw(x) {
|
|
111
|
+
return (x ~ /^(forbidden|prohibited|do not|don.t|never|avoid)([^a-z0-9_]|$)/ || x ~ /^(금지|하지 말 것)([ \t:]|$|\357\274\232)/)
|
|
112
|
+
}
|
|
113
|
+
# header colon: ":" or full-width ":" followed by whitespace/end of line, at most 4 words before it
|
|
114
|
+
function colon_header(x, i, len, ch, pre, k, tmp) {
|
|
115
|
+
len = length(x)
|
|
116
|
+
for (i = 1; i <= len; i++) {
|
|
117
|
+
ch = substr(x, i, 1)
|
|
118
|
+
if (ch == ":" && (i == len || substr(x, i + 1, 1) ~ /[ \t]/)) break
|
|
119
|
+
if (substr(x, i, 3) == "\357\274\232" && (i + 2 == len || substr(x, i + 3, 1) ~ /[ \t]/)) break
|
|
120
|
+
}
|
|
121
|
+
if (i > len) return 0
|
|
122
|
+
pre = substr(x, 1, i - 1)
|
|
123
|
+
k = split(pre, tmp, /[ \t]+/)
|
|
124
|
+
return (k <= 4)
|
|
125
|
+
}
|
|
126
|
+
BEGIN {
|
|
127
|
+
n = split(kws, K, "|")
|
|
128
|
+
inblock = 0; btype = ""; hstate = ""; infence = 0
|
|
129
|
+
neg = " (not|never|no|don.t|avoid|without|forbidden|prohibited) "
|
|
130
|
+
prose = "(is|was|are|and|or|the|a|an|to|of|in|on|at|for|with|before|after|then|that|which|when|if|as|from|by|name|names|first|output|result|state|you|we|it)"
|
|
131
|
+
}
|
|
132
|
+
{
|
|
133
|
+
l = tolower($0)
|
|
134
|
+
gsub(/\342\200\231/, "\047", l)
|
|
135
|
+
gsub(/git[ \t]+(-[cC][ \t]+([\"][^\"]*[\"]|\047[^\047]*\047|[^ \t]+)[ \t]+)+/, "git ", l)
|
|
136
|
+
isfence = (l ~ /^[ \t]*```/)
|
|
137
|
+
if (isfence) infence = !infence
|
|
138
|
+
if (l ~ /^[ \t\r]*$/) {
|
|
139
|
+
if (inblock && !infence) {
|
|
140
|
+
if (btype == "C") inblock = 0
|
|
141
|
+
else if (hstate == "body") inblock = 0
|
|
142
|
+
}
|
|
143
|
+
next
|
|
144
|
+
}
|
|
145
|
+
s = l
|
|
146
|
+
sub(/^[ \t>*#+_`-]+/, "", s)
|
|
147
|
+
sub(/^[0-9]+[.)][ \t]+/, "", s)
|
|
148
|
+
sub(/^[*_`]+/, "", s)
|
|
149
|
+
if (!isfence && !infence) {
|
|
150
|
+
if (l ~ /^[ \t]*(#|##|###|####|#####|######)[ \t]/) {
|
|
151
|
+
# heading: ends any block; a prohibition heading opens an H block (first paragraph/list only)
|
|
152
|
+
if (hw(s)) { inblock = 1; btype = "H"; hstate = "lead"; next }
|
|
153
|
+
inblock = 0; btype = ""
|
|
154
|
+
} else {
|
|
155
|
+
# a NEW colon-header line that is not itself a prohibition header ends a colon block
|
|
156
|
+
if (inblock && btype == "C" && colon_header(s) && !hw(s)) { inblock = 0; btype = "" }
|
|
157
|
+
# a label line (**Bold** or "text:") ends an H block
|
|
158
|
+
if (inblock && btype == "H" && (l ~ /^[ \t]*\*\*[^*]+\*\*[ \t]*[::]?[ \t]*$/ || (l !~ /^[ \t]*([-*+]|[0-9]+[.)])[ \t]/ && l ~ /(:|\357\274\232)[ \t]*$/))) { inblock = 0; btype = "" }
|
|
159
|
+
if (!inblock && hw(s) && colon_header(s)) { inblock = 1; btype = "C"; next }
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
if (inblock) { if (btype == "H") hstate = "body"; next }
|
|
163
|
+
for (i = 1; i <= n; i++) {
|
|
164
|
+
kw = K[i]
|
|
165
|
+
if (kw in surv) continue
|
|
166
|
+
off = 0
|
|
167
|
+
rest = l
|
|
168
|
+
while ((p = index(rest, kw)) > 0) {
|
|
169
|
+
pos = off + p
|
|
170
|
+
pre = substr(l, 1, pos - 1)
|
|
171
|
+
post = substr(l, pos + length(kw))
|
|
172
|
+
rest = substr(l, pos + 1)
|
|
173
|
+
off = pos
|
|
174
|
+
if (post ~ /^[a-z0-9_-]/) continue
|
|
175
|
+
if (pos > 1 && substr(l, pos - 1, 1) ~ /[a-z0-9_]/) continue
|
|
176
|
+
w = pre
|
|
177
|
+
gsub(/[^a-z0-9\047_-]/, " ", w)
|
|
178
|
+
w = " " w " "
|
|
179
|
+
if (w ~ neg) continue
|
|
180
|
+
if (index(pre, "금지") || index(pre, "말 것") || index(pre, "하지 마")) continue
|
|
181
|
+
if (kw == "git branch" || kw == "git tag") {
|
|
182
|
+
cmd = post
|
|
183
|
+
sub(/[;&|].*$/, "", cmd)
|
|
184
|
+
if (kw == "git branch" && cmd ~ /(^|[ \t])(--delete|-[a-z]*[dmc][a-z]*)([ \t]|$)/) { surv[kw] = 1; break }
|
|
185
|
+
if (kw == "git branch" && post ~ /^[ \t]+(--list|-l|--show-current|--contains|--all|--merged|--no-merged|-a|-r|-v|-vv)([^a-z0-9-]|$)/) continue
|
|
186
|
+
if (kw == "git tag" && post ~ /^[ \t]+(--list|-l|--contains|--points-at|--sort)([^a-z0-9-]|$)/) continue
|
|
187
|
+
if (post ~ /^[ \t]*$/ || post ~ /^[+`.,;:)\]\047"!?|\/]/ || post ~ /^[ \t]+[(\[]/ || post ~ ("^[ \t]+" prose "([^a-z0-9_-]|$)")) continue
|
|
188
|
+
}
|
|
189
|
+
surv[kw] = 1
|
|
190
|
+
break
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
END {
|
|
195
|
+
for (i = 1; i <= n; i++) if (K[i] in surv) { print K[i]; exit }
|
|
196
|
+
}
|
|
197
|
+
')
|
|
53
198
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
echo "[Hook] R010 violations: ${violation_count} this session — R021 promotion threshold reached" >&2
|
|
59
|
-
fi
|
|
199
|
+
if [ -n "$keyword" ]; then
|
|
200
|
+
if [ -n "$agent_type" ]; then shown="'$agent_type'"; else shown="(unspecified)"; fi
|
|
201
|
+
ctx="[Hook] ADVISORY: delegation prompt to $shown mentions '$keyword' — if this delegates a git operation, route it to mgr-gitnerd (R010)."
|
|
202
|
+
ctx="$ctx"$'\n'"[Hook] Per R010, all git operations (commit/push/branch/merge/etc.) MUST be delegated to mgr-gitnerd"
|
|
60
203
|
|
|
61
|
-
|
|
204
|
+
# Record mention for R021 promotion tracking (best-effort, silent on failure)
|
|
205
|
+
{ echo "$(date -u +%Y-%m-%dT%H:%M:%SZ) $keyword ${agent_type:-(unspecified)}" >> "$VIOLATION_FILE"; } 2>/dev/null || true
|
|
206
|
+
violation_count=$({ wc -l < "$VIOLATION_FILE" | tr -d ' '; } 2>/dev/null || true)
|
|
207
|
+
# Count line once: count >= 3 AND the atomic marker dir is created by this firing.
|
|
208
|
+
if [ "${violation_count:-0}" -ge 3 ] 2>/dev/null && { mkdir "/tmp/.claude-r010-threshold-${PPID}"; } 2>/dev/null; then
|
|
209
|
+
ctx="$ctx"$'\n'"[Hook] R010 git-keyword mentions: ${violation_count} this session (advisory telemetry; R021 promotion needs user approval)"
|
|
62
210
|
fi
|
|
63
|
-
|
|
211
|
+
fi
|
|
212
|
+
fi
|
|
213
|
+
|
|
214
|
+
if [ -n "$ctx" ]; then
|
|
215
|
+
jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
|
|
64
216
|
fi
|
|
65
217
|
|
|
66
|
-
|
|
67
|
-
printf '%s\n' "$input"
|
|
218
|
+
exit 0
|
|
@@ -7,7 +7,13 @@ command -v jq >/dev/null 2>&1 || exit 0
|
|
|
7
7
|
# Model Escalation Advisor Hook
|
|
8
8
|
# Trigger: PreToolUse, tool == "Task" || tool == "Agent"
|
|
9
9
|
# Purpose: Advise model escalation when failure patterns detected
|
|
10
|
-
# Protocol: stdin JSON -> process ->
|
|
10
|
+
# Protocol: stdin JSON -> process -> exit 0 always
|
|
11
|
+
# Once per session (#1885): each distinct advisory key is shown at most once, claimed atomically via a
|
|
12
|
+
# per-key marker dir under /tmp/.claude-model-advisory-shown-${PPID}.d/ (parallel-hook safe).
|
|
13
|
+
# De-escalation is opt-in (OMCUSTOM_MODEL_DEESCALATION=on): task-outcome-recorder.sh records every
|
|
14
|
+
# SubagentStop as outcome=success (no error signal), so "N consecutive successes" only means N agents finished (#1885).
|
|
15
|
+
# Channel (#1885): exit-0 stderr never reaches the model; advice is delivered via stdout JSON
|
|
16
|
+
# hookSpecificOutput.additionalContext (measured CC 2.1.310, #1775). No stdin passthrough, no stderr.
|
|
11
17
|
|
|
12
18
|
input=$(cat)
|
|
13
19
|
|
|
@@ -24,10 +30,25 @@ OUTCOME_FILE="/tmp/.claude-task-outcomes-${PPID}"
|
|
|
24
30
|
|
|
25
31
|
# Skip if no history
|
|
26
32
|
if [ ! -f "$OUTCOME_FILE" ]; then
|
|
27
|
-
printf '%s\n' "$input"
|
|
28
33
|
exit 0
|
|
29
34
|
fi
|
|
30
35
|
|
|
36
|
+
ctx=""
|
|
37
|
+
SHOWN_DIR="/tmp/.claude-model-advisory-shown-${PPID}.d"
|
|
38
|
+
# fresh KEY: atomic claim via mkdir of a per-key marker dir (hash of KEY; no shared file, so no
|
|
39
|
+
# check-then-append race and no newline pollution). Success => emit. State failures are silent;
|
|
40
|
+
# if the marker dir itself is unusable we fail open (emit) rather than lose the advisory.
|
|
41
|
+
fresh() {
|
|
42
|
+
{ mkdir -p "$SHOWN_DIR"; } 2>/dev/null || true
|
|
43
|
+
[ -d "$SHOWN_DIR" ] || return 0
|
|
44
|
+
local h
|
|
45
|
+
h=$(printf '%s' "$1" | cksum | cut -d' ' -f1) || h=""
|
|
46
|
+
[ -n "$h" ] || return 0
|
|
47
|
+
if { mkdir "$SHOWN_DIR/$h"; } 2>/dev/null; then return 0; fi
|
|
48
|
+
return 1
|
|
49
|
+
}
|
|
50
|
+
add() { if [ -z "$ctx" ]; then ctx="$1"; else ctx="${ctx}"$'\n'"$1"; fi; }
|
|
51
|
+
|
|
31
52
|
# Thresholds
|
|
32
53
|
FAILURE_THRESHOLD=2
|
|
33
54
|
CONSECUTIVE_THRESHOLD=3
|
|
@@ -85,20 +106,20 @@ if [ -n "$next_model" ]; then
|
|
|
85
106
|
reason="${consecutive_failures} consecutive failures"
|
|
86
107
|
fi
|
|
87
108
|
|
|
88
|
-
if [ "$should_advise" = true ]; then
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
109
|
+
if [ "$should_advise" = true ] && fresh "esc:${agent_type}:${current_model}->${next_model}"; then
|
|
110
|
+
add ""
|
|
111
|
+
add "--- [Model Escalation Advisory] ---"
|
|
112
|
+
add " Agent type: ${agent_type}"
|
|
113
|
+
add " Current model: ${current_model}"
|
|
114
|
+
add " ⚡ Recommended: Escalate to ${next_model}"
|
|
115
|
+
add " Cost impact: ${cost_multiplier} per task"
|
|
116
|
+
add " Reason: ${reason}"
|
|
117
|
+
add "------------------------------------"
|
|
97
118
|
fi
|
|
98
119
|
fi
|
|
99
120
|
|
|
100
121
|
# De-escalation check
|
|
101
|
-
if [ "$current_model" != "haiku" ] && [ "$current_model" != "inherit" ] && [ "$current_model" != "" ]; then
|
|
122
|
+
if [ "${OMCUSTOM_MODEL_DEESCALATION:-}" = "on" ] && [ "$current_model" != "haiku" ] && [ "$current_model" != "inherit" ] && [ "$current_model" != "" ]; then
|
|
102
123
|
# Same `grep -c` hygiene as above (#1656 E).
|
|
103
124
|
recent_successes=$(tail -${COOLDOWN} "$OUTCOME_FILE" 2>/dev/null | grep -c '"outcome":"success"' 2>/dev/null || true)
|
|
104
125
|
recent_successes=${recent_successes:-0}
|
|
@@ -111,16 +132,18 @@ if [ "$current_model" != "haiku" ] && [ "$current_model" != "inherit" ] && [ "$c
|
|
|
111
132
|
sonnet) lower_model="haiku" ;;
|
|
112
133
|
esac
|
|
113
134
|
|
|
114
|
-
if [ -n "$lower_model" ]; then
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
135
|
+
if [ -n "$lower_model" ] && fresh "deesc:${current_model}->${lower_model}"; then
|
|
136
|
+
add ""
|
|
137
|
+
add "--- [Model De-escalation Advisory] ---"
|
|
138
|
+
add " ↓ Consider: ${current_model} → ${lower_model}"
|
|
139
|
+
add " ${recent_successes} consecutive successes"
|
|
140
|
+
add "--------------------------------------"
|
|
120
141
|
fi
|
|
121
142
|
fi
|
|
122
143
|
fi
|
|
123
144
|
|
|
124
|
-
#
|
|
125
|
-
|
|
145
|
+
# Emit advisory to the model (#1885); silent when nothing to advise.
|
|
146
|
+
if [ -n "$ctx" ]; then
|
|
147
|
+
jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
|
|
148
|
+
fi
|
|
126
149
|
exit 0
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
# Schema Validator Hook — PreToolUse input validation
|
|
3
3
|
# Trigger: PreToolUse on Write, Edit, Bash
|
|
4
4
|
# Purpose: Validate tool inputs against JSON Schema definitions
|
|
5
|
-
# Phase 1: Advisory only (exit 0
|
|
6
|
-
# Protocol: stdin JSON -> validate ->
|
|
5
|
+
# Phase 1: Advisory only (exit 0; warnings via stdout JSON additionalContext, #1885)
|
|
6
|
+
# Protocol: stdin JSON -> validate -> exit 0
|
|
7
|
+
# Channel (#1885): exit-0 stderr never reaches the model (CC 2.1.310 probe, #1775 comment 6405197553);
|
|
8
|
+
# warnings are delivered as stdout JSON hookSpecificOutput.additionalContext. No stdin passthrough.
|
|
7
9
|
|
|
8
10
|
set -euo pipefail
|
|
9
11
|
|
|
@@ -29,7 +31,6 @@ SCHEMA_FILE="$ROOT/.claude/schemas/tool-inputs.json"
|
|
|
29
31
|
|
|
30
32
|
# Skip if schema file doesn't exist
|
|
31
33
|
if [ ! -f "$SCHEMA_FILE" ]; then
|
|
32
|
-
printf '%s\n' "$input"
|
|
33
34
|
exit 0
|
|
34
35
|
fi
|
|
35
36
|
|
|
@@ -72,7 +73,7 @@ case "$tool_name" in
|
|
|
72
73
|
fi
|
|
73
74
|
|
|
74
75
|
# Check dangerous patterns
|
|
75
|
-
if echo "$command" | grep -qE 'rm\s+-rf\s
|
|
76
|
+
if echo "$command" | grep -qE 'rm\s+-rf\s+["'"'"']?/["'"'"']?(\s|\*|[;&|)]|$)'; then
|
|
76
77
|
warnings+=("[Schema] Bash: DANGER — recursive delete from root detected")
|
|
77
78
|
fi
|
|
78
79
|
if echo "$command" | grep -qE '^\s*sudo\s+'; then
|
|
@@ -102,14 +103,10 @@ case "$tool_name" in
|
|
|
102
103
|
;;
|
|
103
104
|
esac
|
|
104
105
|
|
|
105
|
-
# Output warnings (advisory only)
|
|
106
|
+
# Output warnings (advisory only) via additionalContext
|
|
106
107
|
if [ ${#warnings[@]} -gt 0 ]; then
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
done
|
|
110
|
-
echo "[Schema] Phase 1: advisory only — not blocking" >&2
|
|
108
|
+
ctx=$(printf '%s\n' "${warnings[@]}"; printf '%s' "[Schema] Phase 1: advisory only — not blocking")
|
|
109
|
+
jq -cn --arg ctx "$ctx" '{hookSpecificOutput:{hookEventName:"PreToolUse",additionalContext:$ctx}}'
|
|
111
110
|
fi
|
|
112
111
|
|
|
113
|
-
# Always pass through (Phase 1)
|
|
114
|
-
printf '%s\n' "$input"
|
|
115
112
|
exit 0
|
|
@@ -212,10 +212,6 @@ source: # For external agents
|
|
|
212
212
|
origin: github | npm
|
|
213
213
|
url: https://...
|
|
214
214
|
version: 1.0.0
|
|
215
|
-
escalation: # Model escalation policy (optional)
|
|
216
|
-
enabled: true # Enable auto-escalation advisory
|
|
217
|
-
path: haiku → sonnet → opus # Escalation sequence (Tier-3 Agent-tool aliases only)
|
|
218
|
-
threshold: 2 # Failures before advisory
|
|
219
215
|
soul: true # Enable SOUL.md identity injection
|
|
220
216
|
isolation: worktree | sandbox # worktree = git worktree, sandbox = restricted bash
|
|
221
217
|
sandboxFailIfUnavailable: true # Exit if sandbox unavailable (v2.1.83+)
|
|
@@ -422,13 +418,7 @@ The `limitations` field declares what an agent explicitly CANNOT or SHOULD NOT d
|
|
|
422
418
|
|
|
423
419
|
### Escalation Policy
|
|
424
420
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
| Field | Default | Description |
|
|
428
|
-
|-------|---------|-------------|
|
|
429
|
-
| `enabled` | false | Enable escalation tracking for this agent |
|
|
430
|
-
| `path` | haiku → sonnet → opus | Model upgrade sequence (Tier-3 Agent-tool aliases only) |
|
|
431
|
-
| `threshold` | 2 | Failure count before escalation advisory |
|
|
421
|
+
Model escalation is not configured via agent frontmatter; thresholds are hard-coded in model-escalation-advisor.sh; see the model-escalation skill.
|
|
432
422
|
-->
|
|
433
423
|
|
|
434
424
|
## Memory Scopes
|
|
@@ -19,7 +19,7 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
|
|
|
19
19
|
| Hard Block | PreToolUse hook, exit 2 | stage-blocker, rule-deletion-guard | Prevents tool execution |
|
|
20
20
|
| Soft Block | Stop hook prompt | R011 session-end saves | Auto-performs then approves |
|
|
21
21
|
| Conversation Block | PostToolUse hook + `continueOnBlock` (CC v2.1.139+), exit 2 | stuck-detector, context-budget-advisor, cost-cap-advisor | Feeds rejection reason into conversation; Claude continues with awareness |
|
|
22
|
-
| Advisory | PostToolUse hooks | R007, R008, R009, R010, R018 | Warns via stderr
|
|
22
|
+
| Advisory | PreToolUse + PostToolUse + SubagentStop hooks | R007, R008, R009, R010, R018 | Warns via stderr or stdout `additionalContext`, never blocks |
|
|
23
23
|
| Advisory (proactive) | UserPromptSubmit + SubagentStop + PostToolUse hooks | R007, R008 (`r007-r008-drift-advisor.sh` — #1229 UserPromptSubmit, #1545 SubagentStop, #1553 PostToolUse) | Reads last assistant turn; emits advisory if header/prefix absent. SubagentStop wiring (#1545) covers subagent stops in no-user-input loops (`/fsd`); PostToolUse (#1553) covers the orchestrator-only stretch before the first subagent spawn. Complements retroactive Stop-hook (`session-reflection.sh`, #1190). **v1.1.43부터 실제 발화**(파서 셀렉터 `.role`→`.message.role` 수정 — 상세는 Read 도구로 원문 주석 참조). v1.1.49부터 역방향(announce > tool_use) 신호 포함, 기본 off 옵트인 (#1595 #6). v1.1.73의 narration 채널 분리 옵션(`OMCUSTOM_R008_NARRATION`, #1701 #2)은 실 세션 5건(CC 2.1.233~2.1.274) 실측에서 narration 마커 매칭 0/354·판정 on/off 동일로 전제가 성립하지 않아 v1.1.74에서 은퇴했습니다(#1703) — R008 누락은 채널 오선택이 아니라 마커 미직렬화 형태입니다. **v1.1.62(#1650 D)부터 서브에이전트 세션에서는 침묵**: hook stdin의 `agent_id`(CC 스키마상 서브에이전트 내부 발화에만 존재)가 있으면 exit 0 — 단 `SubagentStart`/`SubagentStop`은 `agent_id`가 대상 식별자라 예외(판정 수행). 중첩 서브에이전트 안의 SubagentStop은 억제 못 함(R010 정책상 도달 불가 경로). 완료 보고가 advisory 응답으로 대체되던 훅 피드백 잠식(R020 8항, #1652 #3)의 advisory 계열 차단. |
|
|
24
24
|
| Advisory (telemetry + advisory) | PostToolUseFailure hook | — (계측+어드바이저리, 규칙 강제 없음) | `failure-ledger.sh` (#1561, v1.1.44) — 실패를 JSONL 원장에 append, 무출력(모델 미도달)·비차단. 같은 이벤트의 `subagent-failure-advisor.sh`(Agent/Task)는 additionalContext advisory 전달 |
|
|
25
25
|
| Advisory (proactive) | UserPromptSubmit hook | R020 (원인 진단) | `fail-axis-cause-advisor.sh` (#1561, v1.1.44) — 원장에 실패 기록이 있는데 원인 진술 없는 재촉 프롬프트가 오면 `hookSpecificOutput.additionalContext`로 "원인 가설 되묻기" advisory 전달. 원장 부재 시 조용히 통과 |
|
|
@@ -80,14 +80,14 @@ oh-my-customcode uses an **advisory-first enforcement model**. Most rules are en
|
|
|
80
80
|
3. **Composability**: External skills and internal rules can coexist without deadlocks
|
|
81
81
|
4. **Post-compact reinforcement**: R007/R008/R009/R010/R018 are re-injected after context compaction via `SessionStart`(matcher `*`/`compact`) — not via the PostCompact event itself[^postcompact]
|
|
82
82
|
|
|
83
|
-
## Hard Enforcement Candidates —
|
|
83
|
+
## Hard Enforcement Candidates — R007/R008 advisory **implemented & firing** (#1229 UserPromptSubmit, proactive) + **#1545 SubagentStop** (covers subagent stops) + **#1553 PostToolUse** + retroactive Stop-hook (#1190); `additionalContext` 전달 경로는 #1547(v1.1.40)에서 구현됐으나 파서 셀렉터 결함으로 v1.1.42까지 **양 계층 모두 미발화**였고, **v1.1.43에서 수정 완료·발화 확인**(#1553); hard-block variant still candidate if advisory insufficient (#1096). Promoted: rule-deletion-guard.sh (2026-04-08). See details via Read tool.
|
|
84
84
|
|
|
85
85
|
<!-- DETAIL: Hard Enforcement Candidates (Future)
|
|
86
86
|
If advisory enforcement proves insufficient for specific rules, these are candidates for promotion to hard-block:
|
|
87
87
|
|
|
88
88
|
| Rule | Candidate Hook | Status | Condition for Promotion |
|
|
89
89
|
|------|---------------|--------|------------------------|
|
|
90
|
-
| R010 | git-delegation-guard.sh |
|
|
90
|
+
| R010 | git-delegation-guard.sh | Advisory (not a candidate) | Flags git keywords in delegation prompts to non-mgr-gitnerd agents; does not detect orchestrator direct writes; transcript replays found no clear true positive among residual fires (five borderline), so hard-block promotion is not supported (#1885) |
|
|
91
91
|
| R007/R008 | `r007-r008-drift-advisor.sh` (UserPromptSubmit #1229 + SubagentStop #1545 + PostToolUse #1553) | **Advisory implemented and firing** — proactive pre-response check wired to three trigger points; the SubagentStop leg (#1545) covers subagent stops in no-user-input loops (`/fsd`), and the PostToolUse leg (#1553) covers the orchestrator-only stretch before the first subagent spawn. Retroactive: `session-reflection.sh` (Stop, #1190). Two-layer drift detection: proactive (#1229/#1545/#1553) + retroactive (#1190). **Historical defect (v1.1.40–v1.1.42): delivery path implemented but never fired** — #1547 (v1.1.40) switched delivery from stderr (never model-visible on exit 0) to `hookSpecificOutput.additionalContext` on JSON stdout, non-blocking (no top-level `decision`/`continue`/`stopReason`), but both scripts short-circuited BEFORE emitting: `jq -r '.role'` read a key absent at transcript top level (it is `.message.role`), so `last_assistant` was always empty and the script exited 0 silently. Measured then: 0 `"additionalContext":` occurrences across 771 transcripts; live probe emitted 0 bytes on both streams. **Resolved in v1.1.43** — parser fix on both layers + `PostToolUse` wiring, first fire confirmed by live probe (#1553). Follow-ups: v1.1.44 turn-level R008 prefix counting (#1563), v1.1.45 Skill-tool exemption (#1569), v1.1.49 reverse signal — announce > tool_use, own anchored regex, fires only when ALL tool_use blocks (Skill included) are 0, opt-in via `OMCUSTOM_R008_REVERSE=on` and NOT replicated into `session-reflection.sh` (#1595 #6). | Promote to hard-block if advisory proves insufficient (#1096) |
|
|
92
92
|
|
|
93
93
|
Promotion requires: (1) measured violation rate data, (2) user approval, (3) rollback plan.
|
|
@@ -103,6 +103,6 @@ Promotion requires: (1) measured violation rate data, (2) user approval, (3) rol
|
|
|
103
103
|
|
|
104
104
|
| Rule | Interaction |
|
|
105
105
|
|------|-------------|
|
|
106
|
-
| R010 | git-delegation-guard.sh is advisory
|
|
106
|
+
| R010 | git-delegation-guard.sh is model-visible advisory (#1885) |
|
|
107
107
|
| R016 | Violations trigger rule updates, not enforcement changes |
|
|
108
108
|
| SessionStart (compact re-entry) | Re-injects critical rules to combat context compaction amnesia — see [^postcompact] |
|
|
@@ -510,7 +510,7 @@
|
|
|
510
510
|
"command": "bash \"${CLAUDE_PROJECT_DIR:-.}/.claude/hooks/scripts/content-hash-validator.sh\""
|
|
511
511
|
}
|
|
512
512
|
],
|
|
513
|
-
"matcher": "Read",
|
|
513
|
+
"matcher": "Read|Edit|Write",
|
|
514
514
|
"description": "Store content hashes for Read operations — enables Edit staleness detection"
|
|
515
515
|
},
|
|
516
516
|
{
|
|
@@ -19,33 +19,37 @@ haiku → sonnet → opus
|
|
|
19
19
|
|
|
20
20
|
| Condition | Action |
|
|
21
21
|
|-----------|--------|
|
|
22
|
-
| 2+ failures
|
|
23
|
-
| 3+ consecutive failures across any agent type | Advise global
|
|
24
|
-
| Sustained success after escalation | Advise de-escalation |
|
|
22
|
+
| 2+ recorded failures for the same agent type (any model; evaluated only for haiku/sonnet spawns) | Advise escalation |
|
|
23
|
+
| 3+ consecutive failures across any agent type | Advise escalation (per spawn, not global; only when the spawn's `model` parameter is haiku or sonnet) |
|
|
24
|
+
| Sustained success after escalation | Advise de-escalation (opt-in: `OMCUSTOM_MODEL_DEESCALATION=on`) |
|
|
25
25
|
|
|
26
26
|
## Thresholds
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
|
31
|
-
|
|
32
|
-
| `
|
|
28
|
+
Hard-coded in `model-escalation-advisor.sh` (not configurable via frontmatter or settings):
|
|
29
|
+
|
|
30
|
+
| Constant | Value | Description |
|
|
31
|
+
|----------|-------|-------------|
|
|
32
|
+
| `FAILURE_THRESHOLD` | 2 | Failures before escalation advisory |
|
|
33
|
+
| `CONSECUTIVE_THRESHOLD` | 3 | Consecutive failures for the per-spawn advisory |
|
|
34
|
+
| `COOLDOWN` | 5 | Successes before de-escalation advisory (opt-in only) |
|
|
35
|
+
|
|
36
|
+
Each distinct advisory is shown at most once per session (atomic per-key marker dir `/tmp/.claude-model-advisory-shown-${PPID}.d/`). De-escalation is off by default because `task-outcome-recorder.sh` records every subagent completion as success, so consecutive successes carry no signal.
|
|
33
37
|
|
|
34
38
|
## Cost Guard
|
|
35
39
|
|
|
36
40
|
- Advisory includes estimated cost multiplier
|
|
37
|
-
- De-escalation suggested after sustained success at higher tier
|
|
41
|
+
- De-escalation (opt-in via `OMCUSTOM_MODEL_DEESCALATION=on`) suggested after sustained success at higher tier
|
|
38
42
|
- Cost tracked per session via PPID-scoped temp file
|
|
39
43
|
|
|
40
44
|
## Architecture
|
|
41
45
|
|
|
42
46
|
```
|
|
43
|
-
|
|
44
|
-
Records: agent_type, model, success
|
|
47
|
+
SubagentStop → task-outcome-recorder.sh (the only wiring; not PostToolUse)
|
|
48
|
+
Records: agent_type, model ("inherit"), outcome (always success; no failure signal, so the escalation advisory is currently unreachable)
|
|
45
49
|
Storage: /tmp/.claude-task-outcomes-$PPID (JSON lines, max 50)
|
|
46
50
|
|
|
47
51
|
PreToolUse (Task) → model-escalation-advisor.sh
|
|
48
|
-
Reads outcomes → counts failures → advises escalation via
|
|
52
|
+
Reads outcomes → counts failures → advises escalation via additionalContext
|
|
49
53
|
Advisory only — never blocks, never modifies tool input
|
|
50
54
|
```
|
|
51
55
|
|
|
@@ -2029,7 +2029,7 @@ Opus 4.8에서 thinking blocks가 수정되어 API 오류가 발생하던 버그
|
|
|
2029
2029
|
### 권한 · 훅 · 도구 입력
|
|
2030
2030
|
|
|
2031
2031
|
- CHANGELOG 원문: "Security: Fixed PreToolUse hook approvals and auto mode bypassing the permission prompt for file reads from network (UNC) paths"
|
|
2032
|
-
(292) PreToolUse 훅의 승인과 auto mode가 네트워크(UNC) 경로의 파일 읽기에서 권한 프롬프트를 우회하던 보안 결함이 수정되었습니다. 관측한 범위에서 이 저장소의 훅은 승인 결정 필드를 출력하지 않습니다(`git grep -n permissionDecision -- .claude/hooks` 결과 0건 — 추적되지 않은 `bash-cd-advisor.sh`는 `git grep`에 잡히지 않으며 커밋 후에는 이 필드를 출력하지 않는다고 밝히는 주석 `:30` 1줄이 걸립니다, `.claude/settings.json` 인라인 훅도 `command grep -c permissionDecision` 0; 훅 출력의 `hookSpecificOutput`은 `.claude/hooks/scripts/`의 `claude-md-reinject.sh:74`, `fail-axis-cause-advisor.sh:132`, `r007-r008-drift-advisor.sh:457`, `user-prompt-preprocessor.sh:99`의 jq 출력 식 4곳(v2.0.7 기준)에 더해 v2.0.8에서 `subagent-failure-advisor.sh:60`, `bash-cd-advisor.sh:169`의 jq 출력 식과 `.claude/hooks/hooks.json:20`의 dev-server 인라인 명령 출력이 추가되어 출력 생성 지점은 7곳이며, 모두 `additionalContext` 전달에만 씁니다; `git grep -n hookSpecificOutput -- .claude/hooks`는 작업 트리에서 주석과 `.claude/hooks/hooks.json:249`의 description을 포함해 16줄을 반환하고(v2.0.7: 13줄), 추적되지 않은 `bash-cd-advisor.sh`의 2줄(`:25`, `:169`)을 포함하는 `grep -rn hookSpecificOutput .claude/hooks`는 18줄이므로 커밋 후에는 18줄이 적용됩니다). 또한 이 저장소 실행 환경은 macOS이며 UNC 경로 사용은 관측하지 못했습니다 [가설 — UNC 경로 사용 여부 미관측]. 영향이 없을 것으로 봅니다.
|
|
2032
|
+
(292) PreToolUse 훅의 승인과 auto mode가 네트워크(UNC) 경로의 파일 읽기에서 권한 프롬프트를 우회하던 보안 결함이 수정되었습니다. 관측한 범위에서 이 저장소의 훅은 승인 결정 필드를 출력하지 않습니다(`git grep -n permissionDecision -- .claude/hooks` 결과 0건 — 추적되지 않은 `bash-cd-advisor.sh`는 `git grep`에 잡히지 않으며 커밋 후에는 이 필드를 출력하지 않는다고 밝히는 주석 `:30` 1줄이 걸립니다, `.claude/settings.json` 인라인 훅도 `command grep -c permissionDecision` 0; 훅 출력의 `hookSpecificOutput`은 `.claude/hooks/scripts/`의 `claude-md-reinject.sh:74`, `fail-axis-cause-advisor.sh:132`, `r007-r008-drift-advisor.sh:457`, `user-prompt-preprocessor.sh:99`의 jq 출력 식 4곳(v2.0.7 기준)에 더해 v2.0.8에서 `subagent-failure-advisor.sh:60`, `bash-cd-advisor.sh:169`의 jq 출력 식과 `.claude/hooks/hooks.json:20`의 dev-server 인라인 명령 출력이 추가되어 출력 생성 지점은 7곳이며, 모두 `additionalContext` 전달에만 씁니다; `git grep -n hookSpecificOutput -- .claude/hooks`는 작업 트리에서 주석과 `.claude/hooks/hooks.json:249`의 description을 포함해 16줄을 반환하고(v2.0.7: 13줄), 추적되지 않은 `bash-cd-advisor.sh`의 2줄(`:25`, `:169`)을 포함하는 `grep -rn hookSpecificOutput .claude/hooks`는 18줄이므로 커밋 후에는 18줄이 적용됩니다). 또한 이 저장소 실행 환경은 macOS이며 UNC 경로 사용은 관측하지 못했습니다 [가설 — UNC 경로 사용 여부 미관측]. 영향이 없을 것으로 봅니다. v2.0.9(#1885)에서 PreToolUse advisory 훅 6종이 `additionalContext` 출력으로 바뀌었으므로 위 수치는 v2.0.8 시점의 스냅샷으로 남기고, 현재 값은 2026-10-26 작업 트리에서 다시 쟀습니다: `git ls-files --others --exclude-standard .claude/hooks`가 출력 없음이라 `bash-cd-advisor.sh`는 이제 추적되며, `git grep -n hookSpecificOutput -- .claude/hooks`와 `grep -rn hookSpecificOutput .claude/hooks`가 모두 31줄이고 `git grep -l hookSpecificOutput -- .claude/hooks/scripts`는 스크립트 12개입니다. 출력 생성 지점은 스크립트 12개의 jq 출력 식 12곳과 `.claude/hooks/hooks.json:20` 인라인 명령 1곳을 합해 13곳입니다(v2.0.8의 7곳에 v2.0.9의 6곳이 더해진 값이며, `destructive-git-guard.sh`에는 jq가 없을 때를 위한 python3 대체 출력 식이 `:72`에 1줄 더 있습니다). `git grep -n permissionDecision -- .claude/hooks .claude/settings.json templates/.claude/settings.json`의 히트는 출력하지 않는다고 밝히는 `bash-cd-advisor.sh:30` 주석 1줄뿐입니다.
|
|
2033
2033
|
- CHANGELOG 원문: "Fixed a skill's or slash command's `allowed-tools` rule coming back in a later turn when you leave auto mode or plan mode partway through that turn"
|
|
2034
2034
|
(292) auto mode나 plan mode를 턴 도중에 벗어날 때 스킬·슬래시 커맨드의 `allowed-tools` 규칙이 이후 턴에 다시 돌아오던 결함이 수정되었습니다. 관측한 범위(`git grep -n '^allowed-tools:' -- '.claude/skills/*/SKILL.md'`)에서 `allowed-tools`를 선언한 스킬은 2개입니다(`.claude/skills/diagram-design/SKILL.md:12`, `.claude/skills/profile/SKILL.md:7`). 영향 범위가 작고 규칙 문안은 바뀌지 않습니다.
|
|
2035
2035
|
- CHANGELOG 원문: "Improved hook output handling: `<system-reminder>` tags written in a hook's output are escaped before they reach Claude"
|
|
@@ -2246,7 +2246,7 @@ Opus 4.8에서 thinking blocks가 수정되어 API 오류가 발생하던 버그
|
|
|
2246
2246
|
- CHANGELOG 원문: "Fixed PostToolUse hooks in managed settings not applying `updatedMCPToolOutput` in some sessions"
|
|
2247
2247
|
(296) managed settings의 PostToolUse 훅이 일부 세션에서 `updatedMCPToolOutput`을 적용하지 못하던 결함이 수정되었습니다. 이 저장소의 `playwright-compress` 훅이 `updatedMCPToolOutput`을 씁니다(`.claude/hooks/scripts/playwright-compress.sh:81`, 배선 `.claude/settings.json`의 `playwright-compress.sh` 훅 항목). 다만 그 훅은 managed가 아니라 프로젝트 settings에 있으므로 이 수정의 대상이 아닙니다. 프로젝트 settings 경로에서 같은 증상이 있었는지는 관측하지 못했습니다 [가설 — 증상이 managed 한정이라는 CHANGELOG 문구를 따르면 영향 없음]. 또한 이 훅은 입력 JSON 전체를 되돌려 주면서 `updatedMCPToolOutput`을 최상위 키로 넣으며(`.claude/hooks/scripts/playwright-compress.sh:81`의 jq 식), 다른 훅이 쓰는 `hookSpecificOutput` 객체 형태(예: `.claude/hooks/scripts/claude-md-reinject.sh:74`)와 다릅니다. CC가 최상위 위치의 이 키를 적용하는지는 실측하지 않았습니다 [가설 — 훅 적용 전후 MCP 도구 결과를 비교하면 판정].
|
|
2248
2248
|
- CHANGELOG 원문: "Fixed hook output being altered when it contained text resembling a plugin hint tag"
|
|
2249
|
-
(296) 훅 출력에 플러그인 힌트 태그와 비슷한 텍스트가 있으면 출력이 변형되던 결함이 수정되었습니다. 이 저장소의 advisory 훅은 stdout·stderr로 규칙 경고를 전달하므로(R021 Advisory 계층) 이론상 해당될 수 있으나, 관측한 범위에서 저장소에 `plugin hint` 문자열은 0건입니다(`git grep -n 'plugin hint' -- . ':!guides/claude-code/15-version-compatibility.md' ':!templates/guides/claude-code/15-version-compatibility.md'` rc=1). 훅 출력이 실행 시점에 이런 텍스트를 포함하는지는 관측하지 않았으므로, 영향은 없을 것으로 보되 [가설 — 실제 훅 출력 내용으로 판정]입니다.
|
|
2249
|
+
(296) 훅 출력에 플러그인 힌트 태그와 비슷한 텍스트가 있으면 출력이 변형되던 결함이 수정되었습니다. 이 저장소의 advisory 훅은 stdout·stderr로 규칙 경고를 전달하므로(R021 Advisory 계층) 이론상 해당될 수 있으나, 관측한 범위에서 저장소에 `plugin hint` 문자열은 0건입니다(`git grep -n 'plugin hint' -- . ':!guides/claude-code/15-version-compatibility.md' ':!templates/guides/claude-code/15-version-compatibility.md'` rc=1). 훅 출력이 실행 시점에 이런 텍스트를 포함하는지는 관측하지 않았으므로, 영향은 없을 것으로 보되 [가설 — 실제 훅 출력 내용으로 판정]입니다. 다만 v2.0.9(#1885)부터 PreToolUse advisory 훅 6종은 stderr가 아니라 stdout JSON `additionalContext`로만 경고하므로 위 "stdout·stderr" 서술 중 이 6종에 대한 부분은 v2.0.8까지의 상태입니다(상세는 `claude_code.safety_event` OpenTelemetry 이벤트를 다룬 (307) 항목 끝의 v2.0.9 설명). 다른 훅 스크립트 27개는 여전히 `>&2`를 씁니다(`git grep -l '>&2' -- .claude/hooks/scripts | wc -l` = 27).
|
|
2250
2250
|
- CHANGELOG 원문: "Fixed Esc or an interrupt during a `UserPromptSubmit` hook or a mod's `prompt.submit` hook ending headless sessions, clearing the typed prompt, or letting the unchecked prompt through"
|
|
2251
2251
|
(296) `UserPromptSubmit` 훅이나 mod의 `prompt.submit` 훅이 실행되는 동안의 Esc·인터럽트가 headless 세션을 끝내거나 입력한 프롬프트를 지우거나 검사되지 않은 프롬프트를 통과시키던 결함이 수정되었습니다. 이 저장소는 `UserPromptSubmit` 훅 4개(`user-prompt-preprocessor.sh`, `r007-r008-drift-advisor.sh`, `session-autofix-prompt.sh`, `fail-axis-cause-advisor.sh`)를 배선합니다(`.claude/settings.json`의 `UserPromptSubmit` 블록). 특히 R007/R008 drift advisor와 원인 되묻기 advisor가 이 이벤트에 걸려 있으므로, 구버전의 인터럽트 시 "검사되지 않은 프롬프트 통과"는 advisory 전달 누락의 가능한 경로였습니다 [가설 — 해당 인터럽트 시나리오의 실제 발생은 미관측]. 룰 변경은 필요하지 않습니다.
|
|
2252
2252
|
- CHANGELOG 원문: "Fixed SessionStart hooks of a plugin loaded after a headless session starts being skipped when a different plugin with the same name, or another spelling of it, had already run"
|
|
@@ -2777,7 +2777,7 @@ Opus 4.8에서 thinking blocks가 수정되어 API 오류가 발생하던 버그
|
|
|
2777
2777
|
### 텔레메트리
|
|
2778
2778
|
|
|
2779
2779
|
- CHANGELOG 원문: "Added the `claude_code.safety_event` OpenTelemetry event for each tool call a permission rule, auto mode, a safety check, a hook or the sandbox stopped, prompted or sent back"
|
|
2780
|
-
(307) 권한 규칙·auto mode·안전 검사·훅·샌드박스가 도구 호출을 멈추거나, 확인을 요청하거나, 되돌려 보낼 때마다 발생하는 `claude_code.safety_event` OpenTelemetry 이벤트가 추가되었습니다. 이 저장소의 `monitoring-setup` 스킬은 2.1.307이 속하는 「2.1.282 이상」 갈래(`.claude/skills/monitoring-setup/SKILL.md:58`)에서 `CLAUDE_CODE_ENABLE_TELEMETRY`·`OTEL_METRICS_EXPORTER`·`OTEL_LOGS_EXPORTER`를 console로 켜는 env 블록(`:66`~`:68`)을 user-scope 설정이나 셸 export로 배치하도록 안내합니다(`:73`~`:74`). 원문은 이 이벤트가 어느 exporter로 나가는지 밝히지 않으므로, 그 절차로 켠 console 출력에 이 이벤트가 나타나는지는 [가설 — 텔레메트리를 켠 세션에서 훅 차단 1건을 일으켜 console 출력에 `claude_code.safety_event`가 찍히는지 확인하면 판정 가능]입니다. R021 표는 Hard Block 훅으로 stage-blocker, dev-server tmux, rule-deletion-guard를 들었습니다(v2.0.7 기준 `.claude/rules/MUST-enforcement-policy.md:19`, 보이는 텍스트; v2.0.8부터 dev-server는 이 행에서 빠져 현재 `:19`는 stage-blocker, rule-deletion-guard입니다). 이 중 `exit 2`로 끝나는 차단 경로가 코드에서 확인되는 것은 `.claude/hooks/scripts/stage-blocker.sh:13`과 `.claude/hooks/scripts/rule-deletion-guard.sh`의 `exit 2` 경로(`legacy_check`의 차단 분기들과 분석기 차단 분기, 내용 앵커)이며(`git show HEAD:.claude/hooks/scripts/stage-blocker.sh | grep -n 'exit 2'`, `git grep -n 'exit 2' -- .claude/hooks/scripts/rule-deletion-guard.sh`), 이 이벤트는 이 두 훅이 도구 호출을 멈춘 경우를 세는 결정론적 소스 후보입니다. dev-server tmux 훅은 `.claude/settings.json`의 dev-server 인라인 명령(v2.0.7 기준 `:52`)이 `exit 1`로 끝나므로(`git show 060613cd:.claude/settings.json | awk 'NR==52' | grep -o -E 'exit [0-9]+'` = `exit 1`; v2.0.8에서 이 훅은 항상 `exit 0`인 advisory로 바뀌어 도구 호출을 멈추지 않고, 포그라운드 dev server가 tmux·run_in_background·nohup·`&` 밖에서 명령 위치로 시작될 때만 모델에 `additionalContext`를 전달하며 R021 Hard Block 행에서 빠집니다) 실제로 도구 호출을 멈추는지는 #1775 코멘트(https://github.com/baekenough/oh-my-customcode/issues/1775#issuecomment-6404861043)에서 실측되었습니다: PreToolUse 훅 `exit 1`은 도구가 실행되었고 훅 stderr는 모델에 전달되지 않았습니다(CC 2.1.310, `claude -p`, haiku, 1회 실행 — 조건이 좁은 단일 관측). 이 이벤트가 그 훅에 대해 기록되는지는 [가설 — 텔레메트리를 켠 세션에서 판정 가능]입니다. R010 「Subagent Scope-Creep STOP Protocol」의 2회 trip 계수(`.claude/rules/MUST-orchestrator-coordination.md:262`, 보이는 텍스트)에도 쓸 수 있을지는, 이 이벤트가 서브에이전트의 classifier trip을 다른 차단과 구분해 기록하는지 원문이 밝히지 않으므로 [가설 — 이벤트 속성에 차단 주체와 에이전트 식별자가 있는지 확인하면 판정 가능]입니다. R021 표의 `failure-ledger.sh`(`.claude/rules/MUST-enforcement-policy.md:24`, 보이는 텍스트)는 PostToolUseFailure 훅으로 도구 실패를 원장에 남기는 별도 장치이며, 관측한 범위(`git grep -n safety_event HEAD -- . ':!guides/claude-code/15-version-compatibility.md' ':!templates/guides/claude-code/15-version-compatibility.md'` 0건)에서 이 저장소는 아직 이 이벤트를 참조하지 않습니다.
|
|
2780
|
+
(307) 권한 규칙·auto mode·안전 검사·훅·샌드박스가 도구 호출을 멈추거나, 확인을 요청하거나, 되돌려 보낼 때마다 발생하는 `claude_code.safety_event` OpenTelemetry 이벤트가 추가되었습니다. 이 저장소의 `monitoring-setup` 스킬은 2.1.307이 속하는 「2.1.282 이상」 갈래(`.claude/skills/monitoring-setup/SKILL.md:58`)에서 `CLAUDE_CODE_ENABLE_TELEMETRY`·`OTEL_METRICS_EXPORTER`·`OTEL_LOGS_EXPORTER`를 console로 켜는 env 블록(`:66`~`:68`)을 user-scope 설정이나 셸 export로 배치하도록 안내합니다(`:73`~`:74`). 원문은 이 이벤트가 어느 exporter로 나가는지 밝히지 않으므로, 그 절차로 켠 console 출력에 이 이벤트가 나타나는지는 [가설 — 텔레메트리를 켠 세션에서 훅 차단 1건을 일으켜 console 출력에 `claude_code.safety_event`가 찍히는지 확인하면 판정 가능]입니다. R021 표는 Hard Block 훅으로 stage-blocker, dev-server tmux, rule-deletion-guard를 들었습니다(v2.0.7 기준 `.claude/rules/MUST-enforcement-policy.md:19`, 보이는 텍스트; v2.0.8부터 dev-server는 이 행에서 빠져 현재 `:19`는 stage-blocker, rule-deletion-guard입니다). 이 중 `exit 2`로 끝나는 차단 경로가 코드에서 확인되는 것은 `.claude/hooks/scripts/stage-blocker.sh:13`과 `.claude/hooks/scripts/rule-deletion-guard.sh`의 `exit 2` 경로(`legacy_check`의 차단 분기들과 분석기 차단 분기, 내용 앵커)이며(`git show HEAD:.claude/hooks/scripts/stage-blocker.sh | grep -n 'exit 2'`, `git grep -n 'exit 2' -- .claude/hooks/scripts/rule-deletion-guard.sh`), 이 이벤트는 이 두 훅이 도구 호출을 멈춘 경우를 세는 결정론적 소스 후보입니다. dev-server tmux 훅은 `.claude/settings.json`의 dev-server 인라인 명령(v2.0.7 기준 `:52`)이 `exit 1`로 끝나므로(`git show 060613cd:.claude/settings.json | awk 'NR==52' | grep -o -E 'exit [0-9]+'` = `exit 1`; v2.0.8에서 이 훅은 항상 `exit 0`인 advisory로 바뀌어 도구 호출을 멈추지 않고, 포그라운드 dev server가 tmux·run_in_background·nohup·`&` 밖에서 명령 위치로 시작될 때만 모델에 `additionalContext`를 전달하며 R021 Hard Block 행에서 빠집니다) 실제로 도구 호출을 멈추는지는 #1775 코멘트(https://github.com/baekenough/oh-my-customcode/issues/1775#issuecomment-6404861043)에서 실측되었습니다: PreToolUse 훅 `exit 1`은 도구가 실행되었고 훅 stderr는 모델에 전달되지 않았습니다(CC 2.1.310, `claude -p`, haiku, 1회 실행 — 조건이 좁은 단일 관측). 이 이벤트가 그 훅에 대해 기록되는지는 [가설 — 텔레메트리를 켠 세션에서 판정 가능]입니다. R010 「Subagent Scope-Creep STOP Protocol」의 2회 trip 계수(`.claude/rules/MUST-orchestrator-coordination.md:262`, 보이는 텍스트)에도 쓸 수 있을지는, 이 이벤트가 서브에이전트의 classifier trip을 다른 차단과 구분해 기록하는지 원문이 밝히지 않으므로 [가설 — 이벤트 속성에 차단 주체와 에이전트 식별자가 있는지 확인하면 판정 가능]입니다. R021 표의 `failure-ledger.sh`(`.claude/rules/MUST-enforcement-policy.md:24`, 보이는 텍스트)는 PostToolUseFailure 훅으로 도구 실패를 원장에 남기는 별도 장치이며, 관측한 범위(`git grep -n safety_event HEAD -- . ':!guides/claude-code/15-version-compatibility.md' ':!templates/guides/claude-code/15-version-compatibility.md'` 0건)에서 이 저장소는 아직 이 이벤트를 참조하지 않습니다. v2.0.9(#1885)에서는 위 #1775 후속 프로브(https://github.com/baekenough/oh-my-customcode/issues/1775#issuecomment-6405197553, CC 2.1.310, `claude -p`, haiku, 1회 실행 — 조건이 좁은 단일 관측)와 공식 문서의 "Claude never sees stderr from a hook that exits 0."를 근거로, exit 0 stderr로만 경고하던 PreToolUse advisory 훅 6종(`destructive-git-guard.sh`, `model-escalation-advisor.sh`, `content-hash-validator.sh`의 Edit 경로, `agent-teams-advisor.sh`, `schema-validator.sh`, `git-delegation-guard.sh`)이 경고할 때 stdout JSON 1줄 `hookSpecificOutput.additionalContext`를 내도록 바뀌었습니다. 경고할 것이 없으면 아무것도 출력하지 않고, PreToolUse 경로의 stdin 패스스루는 없앴으며, 종료 코드는 계속 0입니다. 전환 전 발화 빈도는 최근 8개 세션의 메인 트랜스크립트를 샌드박스에서 재생해 쟀습니다. 첫 스냅샷은 도구 호출 2347건(Bash 입력 1187건, Bash·Write·Edit 입력 1196건)이었고, 이 값은 트랜스크립트가 자라기 전의 첫 스냅샷으로만 읽어야 합니다. `git-delegation-guard.sh`는 트랜스크립트가 늘어난 시점마다 같은 8개 세션의 메인 트랜스크립트로 다시 쟀습니다. 이력은 162/736(첫 실행, 중복 제거 없음), 163/740, 165/747 → 22(`tool_use` id로 중복 제거)이고, 현재 값은 최종 스크립트의 재생 결과인 전환 전 167 → 전환 후 23(입력 752건 중)입니다. 23건은 오탐 18건과 경계 사례 5건이고 명백한 진양성은 0건입니다. 오탐 18건은 산문·인용문·예시 14건, 이 훅의 머리말 문구를 다룬 브리프 1건, 버전 측정 참조로 쓴 `git tag` 2건, 키워드가 뒤에 오는 어순의 금지문 "(`git add`, `reset`, `restore` 금지)" 1건입니다. 경계 사례 5건은 에이전트에게 임시 픽스처·프로브 저장소 안에서 git commit/add를 실행하라고 한 경우입니다. 서브에이전트 트랜스크립트 746개에는 Agent 호출이 0건이므로 이 훅의 코퍼스는 성질상 메인 트랜스크립트뿐입니다. 전환 전 발화는 대부분 이 저장소가 위임 프롬프트에 요구하는 금지 목록과 `git branch --list` 사전 실측 리터럴에 반응한 것이었습니다. 제외 분류는 스크립트 머리말 기준으로 (a) 같은 줄에서 키워드 앞의 부정 토큰(영어 not·never·no·don't·avoid·without·forbidden·prohibited는 단어 단위이고 `--no-rebase`·`no-op` 같은 하이픈·밑줄 포함 단어는 제외하며, 한국어는 금지·말 것·하지 마 부분 문자열, 과하게 넓었던 단독 "않"은 뺐습니다), (b) 금지 블록(Forbidden·Prohibited·Do not·Don't·Never·Avoid·금지·하지 말 것으로 시작하는 머리말; 콜론 머리말은 콜론 뒤가 공백이나 줄 끝일 때만 인정하고 다음 빈 줄 또는 다음 비금지 라벨 줄(`Allowed: …`, `Steps:`, `**Steps**:`)까지이며 `Never: …` 같은 금지 라벨은 블록을 잇고(757건 재생에서 이 보강으로 결과가 바뀐 입력은 0건, 마지막 전체 측정은 167 → 23/752), 같은 단어로 시작하는 마크다운 제목은 제목 직후 빈 줄을 건너뛴 첫 문단 또는 목록만 덮고 빈 줄·다음 제목·라벨 줄에서 끝납니다; 한국어 머리말 단어는 경계가 필요해 `금지어`는 머리말이 아니며, ``` 펜스 안의 줄은 블록을 열거나 닫지 않습니다), (c) 읽기 전용 조회(`git branch --list|-l|--show-current|--contains|--all|--merged|--no-merged|-a|-r|-v|-vv`는 같은 명령에 -d·-D·--delete·-m·-M·-c·-C가 없을 때만, `git tag --list|-l|--contains|--points-at|--sort`, 그리고 명사로 쓰인 맨 `git tag`·`git branch`)입니다. 키워드는 앞뒤 경계를 요구해 `git merge-base`는 `git merge`에 걸리지 않고 "legit commit"은 `git commit`에 걸리지 않으며, 공백이 든 따옴표 인자를 포함한 `git -C <path>`·`git -c k=v` 옵션 묶음은 먼저 벗겨 `git -C /repo commit`을 `git commit`과 같게 다룹니다. 알려진 위음성(부정어 뒤 같은 줄의 실제 명령, 빈 줄 없는 콜론 금지 블록이나 Forbidden 제목 아래 첫 문단·목록 안의 실제 명령)과 알려진 오탐("git commit 금지" 어순)은 스크립트 머리말이 수용된 절충으로 적고 있습니다. 경고 문구는 조건형("… mentions '<keyword>' — if this delegates a git operation, route it to mgr-gitnerd (R010)")으로 바꿨습니다. 명백한 진양성이 없는데도 모델에 보이게 둔 이유는 설치된 프로젝트의 기저 비율이 이 저장소와 다르기 때문이라는 것이며 [가설 — 설치 프로젝트의 위임 프롬프트 분포 미측정]입니다. 세션 누적 횟수 줄은 횟수가 3 이상이고 원자적 마커 디렉터리 `/tmp/.claude-r010-threshold-${PPID}`를 이번 발화가 만들었을 때 세션당 한 번만 나옵니다. 상태가 필요 없는 훅의 메인 트랜스크립트 첫 스냅샷 결과는 `destructive-git-guard.sh` 0/1187(Bash 입력), `schema-validator.sh` 0/1196(Bash·Write·Edit 입력)이며, 이 "측정된 0"은 메인 트랜스크립트에 한정됩니다. 서브에이전트 트랜스크립트(Bash·Write·Edit 호출 9,875건)에서 재생하면 `destructive-git-guard.sh`는 4건 발화했고 그중에 실제 `git branch -D`가 있었으며, `schema-validator.sh`는 17건 발화했는데 15건이 일반 절대 경로에 걸린 "recursive delete from root" 오탐이었습니다. v2.0.9는 이 패턴을 루트 자체로 좁혔습니다(`rm\s+-rf\s+["']?/["']?(\s|\*|[;&|)]|$)`, 곧 선택적 따옴표로 감싼 `/`와 `/*`이며 `/tmp/x` 같은 일반 절대 경로는 계속 무시합니다). `destructive-git-guard.sh`는 `git reset --hard`, 단축 플래그 묶음에 `f`가 든 `git clean`(`-f`·`-df`·`-xdf`·`-fdX` 등)과 `--force`, `checkout -- .`·`*`·`/`, `branch -D`를 감지하고, `git restore`는 경로 하나만 준 형태를 포함해 어떤 형태든 감지합니다(R001이 `--source` 없는 `git restore <path>`를 파괴적으로 보기 때문입니다). `git clean -nf`도 git은 dry run으로 다루지만 보수적으로 발화하고, `-n`·`--dry-run`·`-nd`는 조용합니다. 모든 형태가 따옴표 경로를 포함한 `git -C <path>`·`git -c k=v`를 받으며, 횟수 줄은 `/tmp/.claude-destructive-git-threshold-${PPID}` 마커로 같은 방식으로 한 번만 냅니다. 문구는 "(advisory telemetry; R021 promotion needs user approval)"이며 승격을 주장하지 않습니다. 나머지 3종의 0회는 샌드박스 재생이 세션 상태를 심지 않아 생긴 산물이지 측정이 아닙니다. `model-escalation-advisor.sh`는 `/tmp/.claude-task-outcomes-$PPID`가 있어야 발화합니다. `agent-teams-advisor.sh`는 env-status 파일(`/tmp/.claude-env-status-$PPID`)이 있으면서 `agent_teams=enabled`가 아닐 때만 건너뛰고 파일이 없으면 진행하므로, 재생에 없던 상태는 PPID별 작업 카운터(2번째 호출부터)나 workflow·release-plan 파일입니다. `content-hash-validator.sh`는 앞선 Read가 남긴 해시 저장소가 필요합니다. `content-hash-validator.sh`는 이제 PostToolUse의 Read·Edit·Write가 모두 해시를 갱신하므로 Edit·Write 도구로 한 모델 자신의 편집은 더 이상 경고를 일으키지 않습니다. 리뷰어의 모델 기반 추정으로는 Edit의 약 55%가 이 오탐 조건이었습니다 [가설 — 모델 기반 추정, 트랜스크립트 실측 아님]. 이후 파일을 다시 쓰는 병렬 PostToolUse 포매터 훅이나 Bash 편집은 여전히 경고를 일으킬 수 있습니다. 저장소는 경로별 원자적 파일 `/tmp/.claude-content-hashes-${PPID}.d/<cksum>`(1행 해시, 2행 경로, 임시 파일 후 `mv`)이며 링 버퍼는 없습니다. 병렬 저장이 exit 1·stderr 누출·항목 유실을 일으키던 문제는 리뷰어 측정으로 전 68/240 실패에서 후 0/240이 되었습니다. 이벤트명이 없는 Edit도 검증하고, 대조는 고정 문자열이며, hooks.json의 description은 소유 키라서 바꾸지 않았습니다. `model-escalation-advisor.sh`와 `agent-teams-advisor.sh`는 서로 다른 advisory를 세션당 최대 한 번만 보입니다. 같은 세션에서 `model-escalation-advisor`가 동일한 강등 advisory("sonnet → haiku, 5 consecutive successes")를 연속 Agent 스폰 4건 모두에 낸 관측이 이유입니다. 키에는 횟수를 넣지 않으며 `model-escalation-advisor.sh`의 키는 `esc:<agent>:<from>-><to>`와 `deesc:<from>-><to>`, `agent-teams-advisor.sh`의 키는 `batch:<N>`과 `repeat`입니다. 보인 키는 키별 원자적 마커 디렉터리 `/tmp/.claude-model-advisory-shown-${PPID}.d/<cksum>`, `/tmp/.claude-teams-advisory-shown-${PPID}.d/<cksum>`로 기록합니다. 한 파일에 줄을 덧붙이던 초기 방식을 대체해 병렬 이중 발화와 개행 키 오염을 없앴습니다. "세션당 한 번"은 마커 디렉터리가 있거나 만들 수 있을 때 성립하며, 만들 수 없으면 advisory가 반복되는 fail-open이고 이미 있는 읽기 전용 마커 디렉터리는 advisory를 조용하게 만듭니다. 강등 advisory는 `OMCUSTOM_MODEL_DEESCALATION=on`일 때만 나옵니다. `task-outcome-recorder`가 SubagentStop에서만 돌며 모든 완료를 success로 기록하므로 "연속 N회 성공"은 에이전트 N개가 끝났다는 뜻일 뿐이기 때문입니다. 승격 advisory는 haiku·sonnet 스폰에서만 평가되고 기록기는 실패를 남기지 않으므로 현재는 도달할 수 없습니다. `agent-teams-advisor.sh`는 손상된 숫자 상태를 정리한 뒤 계산합니다. 이 세션에서 작업 트리 변경 후 오케스트레이터 모델은 `model-escalation-advisor`의 강등 advisory(강등 옵트인 변경 전의 중간 작업 트리에서 받은 것이며, 최종 코드의 기본 설정에서는 승격이 도달 불가이고 강등이 옵트인이라 이 훅은 어떤 advisory도 내지 못합니다. 이 반복이 중복 제거와 옵트인 도입의 계기였습니다)와 `git-delegation-guard`의 advisory를 두 차례 훅 additional context로 받았고(`git reset`이 든 arch-documenter 브리프, `git add`가 든 mgr-updater 브리프), 이후 스폰에서도 같은 종류가 더 전달되었으며 정확한 총 횟수는 세지 않았습니다. 서브에이전트 1개는 `schema-validator`의 advisory를 받았습니다(나머지 3종의 모델 도달은 직접 관측하지 않았습니다). `git-delegation-guard`의 이 전달들은 모두 산문 언급에 걸린 오탐이었습니다. 위 (292) 항목(UNC 경로 읽기 우회 수정)의 `hookSpecificOutput` 줄 수·생성 지점 수는 이 변경으로 v2.0.8 시점 스냅샷이 되었고 현재 값은 그 항목 끝에 다시 적었습니다.
|
|
2781
2781
|
|
|
2782
2782
|
기타 31건 — 이 저장소 비해당으로 판단한 항목입니다: Claude API 코드를 API 버전 `2026-10-21`로 옮기는 옵트인 명령 `/claude-api api-version`, 맞춤 `ANTHROPIC_BASE_URL` 게이트웨이 뒤와 Foundry에서의 `prompt-caching-scope` 헤더(저장소가 추적하는 `.claude/settings.json`·`templates/.claude/settings.json`에 `ANTHROPIC_BASE_URL` 0건, `git grep -c ANTHROPIC_BASE_URL HEAD -- .claude/settings.json templates/.claude/settings.json` rc=1), Agent SDK 전용 항목(백그라운드 작업이 도는 동안 턴이 때때로 SDK MCP 서버의 최신 도구 없이 시작되던 결함, API 확인이 5초를 넘을 때의 `setModel()` 대기), cloud sessions·self-hosted runner 전용 항목(재시작 뒤 `/autocompact` 창 유지, 재시작 시 `/clear`한 대화가 때때로 되살아나던 결함, 웹 검색 403 사유 표시, 데스크톱 앱 MCP 연결 실패 시의 첫 응답 지연, checkout 훅·인증서·git proxy 등), Claude apps gateway 전용 항목(`CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS`, Desktop 텔레메트리), 앱·도구 연동 세부(Cowork cloud 세션의 WebFetch, 데스크톱 앱 Code 탭의 GitHub MCP 양식, Artifact 도구 읽기의 연결 재사용), CLI 브라우저 로그인 성공 페이지가 뜨는 시점, 큰 붙여넣기 이미지·PDF 읽기가 든 세션의 Remote Control 연결 시간, `/help`·`/` 메뉴의 번들 스킬 설명 표시, WebSearch 예산 소진 안내 문구, VSCode·Cloud sessions·Code Review 전용 항목. 기능어 표본 검색(`git grep -l -i -E 'api-version|prompt-caching-scope|setModel|createSdkMcpServer|CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS|OTEL_EXPORTER_OTLP_HEADERS|Cowork|remote.control|checkout-hook|use-anthropic-git-proxy|self-hosted|WebSearch budget' HEAD -- .claude CLAUDE.md .github package.json`)의 히트 파일은 `.claude/rules/` 아래 8개와 `.github/scripts/send-report.ts` 하나이며, 후자의 히트는 GitHub REST 요청 헤더 `X-GitHub-Api-Version`(`:44`)의 부분 문자열 일치입니다. 룰 파일 8개의 히트는 문맥별로 판정하지 않았고, 기능어가 없는 표시·성능 항목도 검색으로 판정하지 않았습니다. 항목별 저장소 측정은 위 본문 항목에 한해 실행했으므로, 이 31건의 비해당 분류는 [가설 — 룰 파일 8개 히트의 문맥과 기능어 없는 항목의 관련 경로를 항목별로 읽으면 판정 가능]입니다. 실측: `gh release view v2.1.307 --repo anthropics/claude-code --json body --jq .body | grep -c '^- '` = 43건 중 위 본문 12건을 다뤘으므로 43 − 12 = 31건입니다. 계수 범위는 해당 릴리즈 노트의 최상위 불릿 줄(`- `로 시작하는 줄)입니다.
|
|
2783
2783
|
|
|
@@ -94,4 +94,7 @@ safe_reset() {
|
|
|
94
94
|
|
|
95
95
|
### Destructive op detection (advisory)
|
|
96
96
|
|
|
97
|
-
|
|
97
|
+
Two advisory PreToolUse hooks cover this area; both always exit 0 and never block. Since #1885 each delivers its warning as stdout JSON `hookSpecificOutput.additionalContext` (visible to the model) and prints nothing when silent.
|
|
98
|
+
|
|
99
|
+
- `.claude/hooks/scripts/destructive-git-guard.sh` (T2 from #1146) warns before Bash commands that can discard work (R001): `git reset --hard`, `git clean` with any short-flag cluster containing `f` (`-f`, `-df`, `-xdf`, ...) or `--force`, `git restore` in any form (R001 treats even `git restore <path>` without `--source` as destructive), `git checkout -- .`/`*`/`/`, and `git branch -D`. All forms accept `git -C <path>` (quoted paths too) and `git -c k=v`. `git clean -nf` also fires (conservative); `-n`, `--dry-run` and `-nd` stay silent. The count line (`/tmp/.claude-destructive-git-threshold-${PPID}` marker) appears once per session when the count reaches 3.
|
|
100
|
+
- `.claude/hooks/scripts/git-delegation-guard.sh` flags git write keywords (commit, branch, checkout, merge, tag, ...) in delegation prompts sent to agents other than `mgr-gitnerd` (R010). It skips keywords after a negation on the same line, keywords inside a prohibition block (a `Forbidden:`-style colon header runs to the next blank line or the next non-prohibition label line such as `Allowed:` or `Steps:`, while a prohibition label like `Never:` continues it; a `Forbidden`-style markdown heading covers only its first paragraph or list), read-only `git branch`/`git tag` queries (`--list`, `-l`, `--contains`, `--show-current`, `-a`, `-r`, `-v`, ...; for `git branch` only, not when the same command also has `-d`/`-D`/`--delete`/`-m`/`-M`/`-c`/`-C`), and bare `git tag`/`git branch` used as a noun. ``` fences never open or close blocks. `git -C <path>` and `git -c k=v` (quoted arguments too) are stripped first, and keywords need word boundaries (`git merge-base` and "legit commit" do not match). The count line appears once per session when the count reaches 3.
|
|
@@ -25,9 +25,9 @@ LangChain이 정의한 6개의 lifecycle hook은 다음과 같습니다. 각 단
|
|
|
25
25
|
| `before_agent` | 에이전트 전체 실행 시작 전 | `SessionStart`, `UserPromptSubmit` hook | MEMORY.md 자동 주입 |
|
|
26
26
|
| `before_model` | 모델 호출 직전 (매 추론 루프마다) | `PreToolUse(Agent)`, pre-generation-arch-check | pre-generation-arch-check 스킬 |
|
|
27
27
|
| `wrap_model_call` | 모델 호출 자체를 래핑 (실제 호출 제어) | Tier 4 permission approval (Bash/WebFetch) | permission hooks, bypassPermissions 게이트 |
|
|
28
|
-
| `wrap_tool_call` | 도구 호출을 래핑 (전후 모두 제어) | `PreToolUse(*) + PostToolUse(*)` | action-validator
|
|
28
|
+
| `wrap_tool_call` | 도구 호출을 래핑 (전후 모두 제어) | `PreToolUse(*) + PostToolUse(*)` | action-validator |
|
|
29
29
|
| `after_model` | 모델 응답 수신 직후 | `PostToolUse(Agent)`, result-aggregation | result-aggregation 스킬, evaluator-optimizer |
|
|
30
|
-
| `after_agent` | 에이전트 전체 실행 완료 후 | `Stop`, `SubagentStop`, post-release-followup | SubagentStop 어드바이저리 훅 (omcustom-loop 참조) |
|
|
30
|
+
| `after_agent` | 에이전트 전체 실행 완료 후 | `Stop`, `SubagentStop`, post-release-followup | SubagentStop 어드바이저리 훅 (omcustom-loop 참조), task-outcome-recorder.sh |
|
|
31
31
|
|
|
32
32
|
### 매핑 해설
|
|
33
33
|
|
|
@@ -158,9 +158,8 @@ oh-my-customcode에서 이 패턴은 R004의 재시도 정책으로 명문화되
|
|
|
158
158
|
| `PreToolUse(*)` | 훅 이벤트 | 모든 도구 호출 직전 검증 |
|
|
159
159
|
| `PostToolUse(*)` | 훅 이벤트 | 모든 도구 호출 직후 결과 기록 |
|
|
160
160
|
| `action-validator` | 스킬 | 에이전트가 선언된 도구 범위 내에서 실행하는지 pre-flight 검사 |
|
|
161
|
-
| `task-outcome-recorder` | 스킬 | 도구 호출 결과를 구조화된 형식으로 기록 |
|
|
162
161
|
| rule-deletion-guard.sh | 훅 스크립트 | 규칙 파일 삭제 시도 하드 블록 (exit 2) |
|
|
163
|
-
| `git-delegation-guard.sh` | 훅 스크립트 | git
|
|
162
|
+
| `git-delegation-guard.sh` | 훅 스크립트 | mgr-gitnerd 외 에이전트로 보내는 위임 프롬프트의 git 쓰기 키워드(commit, branch, checkout 등) 감지 및 어드바이저리 전달 (additionalContext, 차단 없음) |
|
|
164
163
|
|
|
165
164
|
### after_model 단계
|
|
166
165
|
|
|
@@ -178,6 +177,7 @@ oh-my-customcode에서 이 패턴은 R004의 재시도 정책으로 명문화되
|
|
|
178
177
|
|---|---|---|
|
|
179
178
|
| `Stop` hook | 훅 이벤트 | 세션 종료 시 메모리 저장 권고 |
|
|
180
179
|
| `SubagentStop` hook | 훅 이벤트 | 서브에이전트 완료 후 결과 기록·어드바이저리 출력 (오케스트레이터 워크플로 계속 프롬프트 주입 없음) |
|
|
180
|
+
| `task-outcome-recorder.sh` | 훅 스크립트 (SubagentStop) | 서브에이전트 완료 기록(항상 outcome=success) |
|
|
181
181
|
| `omcustom-loop` | 스킬 | SubagentStop 훅 어드바이저리 안내 (오케스트레이터 워크플로 계속 프롬프트 주입 없음) |
|
|
182
182
|
| sys-memory-keeper | 에이전트 | 세션 종료 시 MEMORY.md 업데이트 |
|
|
183
183
|
| `SubagentStart` hook | 훅 이벤트 | 서브에이전트 시작 시 HUD 이벤트 발행 |
|
|
@@ -117,13 +117,7 @@ When a task fails at a lower model tier, escalate. Escalation is executed by re-
|
|
|
117
117
|
haiku → sonnet → opus
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
-
|
|
121
|
-
```yaml
|
|
122
|
-
escalation:
|
|
123
|
-
enabled: true
|
|
124
|
-
path: haiku → sonnet → opus
|
|
125
|
-
threshold: 2 # failures before escalation advisory
|
|
126
|
-
```
|
|
120
|
+
There is no agent-frontmatter switch for escalation: `model-escalation-advisor.sh` reads no frontmatter, its thresholds are hard-coded constants in the script, and it evaluates every haiku/sonnet spawn. It is currently unreachable because the outcome recorder never writes failures. See the `model-escalation` skill.
|
|
127
121
|
|
|
128
122
|
## Fast Mode Interaction
|
|
129
123
|
|
package/templates/manifest.json
CHANGED