oh-my-customcode 1.1.54 → 1.1.56

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.
Files changed (42) hide show
  1. package/dist/cli/index.js +1 -1
  2. package/dist/index.js +1 -1
  3. package/package.json +1 -1
  4. package/templates/.claude/hooks/hooks.json +19 -19
  5. package/templates/.claude/hooks/scripts/agent-start-recorder.sh +5 -5
  6. package/templates/.claude/hooks/scripts/agent-teams-advisor.sh +4 -4
  7. package/templates/.claude/hooks/scripts/audit-log.sh +6 -6
  8. package/templates/.claude/hooks/scripts/auto-continue-guard.sh +1 -1
  9. package/templates/.claude/hooks/scripts/auto-dev-token-summary.sh +6 -6
  10. package/templates/.claude/hooks/scripts/auto-dev-token-tracker.sh +6 -6
  11. package/templates/.claude/hooks/scripts/content-hash-validator.sh +4 -4
  12. package/templates/.claude/hooks/scripts/context-budget-advisor.sh +3 -3
  13. package/templates/.claude/hooks/scripts/cost-cap-advisor.sh +6 -6
  14. package/templates/.claude/hooks/scripts/cwd-change-detector.sh +3 -3
  15. package/templates/.claude/hooks/scripts/destructive-git-guard.sh +2 -2
  16. package/templates/.claude/hooks/scripts/eval-core-batch-save.sh +2 -2
  17. package/templates/.claude/hooks/scripts/feedback-collector.sh +7 -7
  18. package/templates/.claude/hooks/scripts/file-change-validator.sh +4 -4
  19. package/templates/.claude/hooks/scripts/git-delegation-guard.sh +3 -3
  20. package/templates/.claude/hooks/scripts/model-escalation-advisor.sh +4 -4
  21. package/templates/.claude/hooks/scripts/omcustom-auto-update.sh +6 -6
  22. package/templates/.claude/hooks/scripts/playwright-compress.sh +4 -4
  23. package/templates/.claude/hooks/scripts/plugin-cache-check.sh +2 -2
  24. package/templates/.claude/hooks/scripts/r007-r008-drift-advisor.sh +26 -3
  25. package/templates/.claude/hooks/scripts/rtk-intercept.sh +9 -9
  26. package/templates/.claude/hooks/scripts/rule-deletion-guard.sh +5 -5
  27. package/templates/.claude/hooks/scripts/schema-validator.sh +10 -10
  28. package/templates/.claude/hooks/scripts/secret-filter.sh +4 -4
  29. package/templates/.claude/hooks/scripts/session-autofix-prompt.sh +3 -3
  30. package/templates/.claude/hooks/scripts/session-autofix.sh +1 -1
  31. package/templates/.claude/hooks/scripts/session-env-check.sh +1 -1
  32. package/templates/.claude/hooks/scripts/session-reflection.sh +15 -15
  33. package/templates/.claude/hooks/scripts/skill-extractor-analyzer.sh +2 -2
  34. package/templates/.claude/hooks/scripts/stale-todo-scanner.sh +1 -1
  35. package/templates/.claude/hooks/scripts/stall-detection-advisor.sh +11 -11
  36. package/templates/.claude/hooks/scripts/stop-console-audit.sh +1 -1
  37. package/templates/.claude/hooks/scripts/stuck-detector.sh +81 -25
  38. package/templates/.claude/hooks/scripts/subagent-failure-advisor.sh +36 -0
  39. package/templates/.claude/hooks/scripts/task-outcome-recorder.sh +8 -8
  40. package/templates/.claude/hooks/scripts/task-state-precompact.sh +1 -1
  41. package/templates/.claude/settings.json +19 -19
  42. package/templates/manifest.json +1 -1
@@ -16,29 +16,28 @@ command -v jq >/dev/null 2>&1 || exit 0
16
16
  HARD_BLOCK_THRESHOLD=${CLAUDE_STUCK_THRESHOLD:-3}
17
17
 
18
18
  # Determine if a Bash command is read-only (metadata/query only, no side effects).
19
- # Conservative: any ambiguity (chaining, redirection, substitution, or an
19
+ # Conservative: any ambiguity (redirection, substitution, "||", or an
20
20
  # unrecognized command/subcommand) is treated as NOT read-only (write).
21
- # Used ONLY to exclude read-only Bash polling from Hard Block Checks 1 and 3
22
- # (same-path / same-tool+target consecutive-repeat blocking) below — Signal 1
23
- # (repeated-error advisory) and Hard Block Check 2 (same error hash) are
24
- # intentionally unaffected: repeated errors are a genuine stuck signal
25
- # regardless of read/write (issue #1625).
26
- is_readonly_bash_command() {
21
+ # Compound commands chained with "&&", ";", or a single "|" are split into
22
+ # segments (#1629) — the whole command is read-only ONLY when EVERY segment
23
+ # is read-only; one write segment (or one unparseable/empty segment) makes
24
+ # the whole command a write.
25
+ # Used to exclude read-only Bash polling from Hard Block Checks 1 and 3
26
+ # (same-path / same-tool+target consecutive-repeat blocking) AND the Signal 3
27
+ # tool-spam advisory below — Signal 1 (repeated-error advisory) and Hard
28
+ # Block Check 2 (same error hash) are intentionally unaffected: repeated
29
+ # errors are a genuine stuck signal regardless of read/write (issue #1625).
30
+
31
+ # Classify a SINGLE (non-compound) command segment as read-only. Callers
32
+ # (is_readonly_bash_command) are responsible for stripping ambiguous
33
+ # constructs and splitting compound commands before invoking this directly.
34
+ is_readonly_single_command() {
27
35
  local cmd="$1"
28
- cmd="$(printf '%s' "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
29
36
  if [ -z "$cmd" ]; then
30
37
  echo "false"
31
38
  return 0
32
39
  fi
33
40
 
34
- # Any chaining/redirection/substitution metachar => ambiguous, treat as write
35
- case "$cmd" in
36
- *'>'*|*'|'*|*'&&'*|*';'*|*'$('*|*'`'*|*'<('*)
37
- echo "false"
38
- return 0
39
- ;;
40
- esac
41
-
42
41
  local parts=()
43
42
  read -ra parts <<< "$cmd"
44
43
  local w1="${parts[0]:-}"
@@ -117,11 +116,62 @@ is_readonly_bash_command() {
117
116
  esac
118
117
  }
119
118
 
119
+ is_readonly_bash_command() {
120
+ local cmd="$1"
121
+ cmd="$(printf '%s' "$cmd" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
122
+ if [ -z "$cmd" ]; then
123
+ echo "false"
124
+ return 0
125
+ fi
126
+
127
+ # Truly ambiguous constructs: redirection, command/process substitution, or
128
+ # "||" (conditional-OR — intentionally NOT decomposed into segments, since
129
+ # its branch that actually runs depends on the first segment's exit code)
130
+ # => always treated as write.
131
+ case "$cmd" in
132
+ *'>'*|*'||'*|*'$('*|*'`'*|*'<('*)
133
+ echo "false"
134
+ return 0
135
+ ;;
136
+ esac
137
+
138
+ # Compound command: split on "&&", ";", or a single "|" into segments and
139
+ # require EVERY segment to be read-only (#1629). An empty segment (e.g. a
140
+ # trailing separator) is treated as ambiguous => write, same as any
141
+ # segment that fails single-command classification.
142
+ if [[ "$cmd" == *'&&'* || "$cmd" == *';'* || "$cmd" == *'|'* ]]; then
143
+ local normalized="$cmd"
144
+ normalized="${normalized//&&/$'\n'}"
145
+ normalized="${normalized//;/$'\n'}"
146
+ normalized="${normalized//|/$'\n'}"
147
+ local seg
148
+ while IFS= read -r seg; do
149
+ seg="$(printf '%s' "$seg" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
150
+ if [ -z "$seg" ]; then
151
+ echo "false"
152
+ return 0
153
+ fi
154
+ if [ "$(is_readonly_single_command "$seg")" != "true" ]; then
155
+ echo "false"
156
+ return 0
157
+ fi
158
+ done <<< "$normalized"
159
+ echo "true"
160
+ return 0
161
+ fi
162
+
163
+ is_readonly_single_command "$cmd"
164
+ }
165
+
120
166
  input=$(cat)
121
167
 
122
168
  # Extract tool info
123
169
  tool_name=$(printf '%s' "$input" | jq -r '.tool_name // "unknown"')
124
- file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.command // ""' | head -c 120)
170
+ # 300 (not 120): a 120-char cutoff let two genuinely different long Bash
171
+ # commands collide on their shared 120-char prefix, causing a false-positive
172
+ # same-path/same-tool+target hard-block. 300 chars covers the vast majority
173
+ # of real commands without their differing tail being cut off (#1629).
174
+ file_path=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_input.command // ""' | head -c 300)
125
175
  is_error=$(printf '%s' "$input" | jq -r '.tool_output.is_error // false')
126
176
  output_preview=$(printf '%s' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
127
177
  raw_command=$(printf '%s' "$input" | jq -r '.tool_input.command // ""')
@@ -150,9 +200,10 @@ entry=$(jq -cn \
150
200
  --arg err "$is_error" \
151
201
  --arg hash "$error_hash" \
152
202
  --arg preview "$output_preview" \
153
- '{timestamp: $ts, tool: $tool, path: $path, is_error: $err, error_hash: $hash, preview: $preview}')
203
+ --arg readonly "$is_readonly" \
204
+ '{timestamp: $ts, tool: $tool, path: $path, is_error: $err, error_hash: $hash, preview: $preview, readonly: $readonly}')
154
205
 
155
- echo "$entry" >> "$HISTORY_FILE"
206
+ printf '%s\n' "$entry" >> "$HISTORY_FILE"
156
207
 
157
208
  # Ring buffer: keep last 100 entries
158
209
  if [ -f "$HISTORY_FILE" ]; then
@@ -167,13 +218,13 @@ fi
167
218
 
168
219
  # Only check for patterns if we have enough history
169
220
  if [ ! -f "$HISTORY_FILE" ]; then
170
- echo "$input"
221
+ printf '%s\n' "$input"
171
222
  exit 0
172
223
  fi
173
224
 
174
225
  recent_count=$(wc -l < "$HISTORY_FILE")
175
226
  if [ "$recent_count" -lt 3 ]; then
176
- echo "$input"
227
+ printf '%s\n' "$input"
177
228
  exit 0
178
229
  fi
179
230
 
@@ -214,8 +265,13 @@ if [ "$stuck_detected" = false ] && { [ "$tool_name" = "Edit" ] || [ "$tool_name
214
265
  fi
215
266
 
216
267
  # Signal 3: Tool spam (same tool 5+ times in last 8 entries)
217
- if [ "$stuck_detected" = false ]; then
218
- tool_repeat=$(tail -8 "$HISTORY_FILE" | grep -c "\"tool\":\"${tool_name}\"" 2>/dev/null || echo "0")
268
+ # Read-only Bash polling is excluded from this count (issue #1629): skip
269
+ # entirely when the CURRENT call is read-only, and exclude prior read-only
270
+ # entries from the historical count so a mix of harmless read-only Bash
271
+ # calls (git status / gh view / ls / ...) doesn't inflate the "same tool
272
+ # called N times" advisory.
273
+ if [ "$stuck_detected" = false ] && [ "$is_readonly" != "true" ]; then
274
+ tool_repeat=$(tail -8 "$HISTORY_FILE" | grep -v "\"readonly\":\"true\"" | grep -c "\"tool\":\"${tool_name}\"" 2>/dev/null || echo "0")
219
275
  if [ "$tool_repeat" -ge 5 ]; then
220
276
  stuck_detected=true
221
277
  signal_type="Tool loop"
@@ -291,7 +347,7 @@ if [ "$hard_block" = true ]; then
291
347
  echo " Action: Blocking this tool call to break the stuck loop." >&2
292
348
  echo " Recovery: Step back, re-read the error, and try a fundamentally different approach." >&2
293
349
  echo "=====================================" >&2
294
- echo "$input"
350
+ printf '%s\n' "$input"
295
351
  HOOK_END=$(date +%s%N 2>/dev/null || echo 0)
296
352
  if [ "$HOOK_START" != "0" ] && [ "$HOOK_END" != "0" ]; then
297
353
  HOOK_MS=$(( (HOOK_END - HOOK_START) / 1000000 ))
@@ -301,7 +357,7 @@ if [ "$hard_block" = true ]; then
301
357
  fi
302
358
 
303
359
  # Pass through
304
- echo "$input"
360
+ printf '%s\n' "$input"
305
361
  HOOK_END=$(date +%s%N 2>/dev/null || echo 0)
306
362
  if [ "$HOOK_START" != "0" ] && [ "$HOOK_END" != "0" ]; then
307
363
  HOOK_MS=$(( (HOOK_END - HOOK_START) / 1000000 ))
@@ -0,0 +1,36 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+
4
+ # Dependency check: exit silently if jq not available
5
+ command -v jq >/dev/null 2>&1 || exit 0
6
+
7
+ # Subagent Failure Advisor Hook
8
+ # Trigger: SubagentStop
9
+ # Purpose: Advisory-only replacement for the removed `type: "prompt"` SubagentStop hook
10
+ # (#1631). The prompt hook asked an LLM judge whether the CURRENT session's own
11
+ # background_tasks entry had transitioned to "completed" before allowing Stop —
12
+ # but that transition only happens AFTER the SubagentStop hook chain resolves, so the
13
+ # judgment was structurally always "not yet completed" for a subagent evaluating its
14
+ # OWN stop. That produced a self-referential `Stop hook feedback:` re-injection loop
15
+ # with no dedup, observed to repeat 8-10x per session before manual override.
16
+ # Fix: replace the blocking LLM judge with a plain stderr-only warning that never
17
+ # triggers the Stop-hook-feedback re-injection path. No block, no LLM call, no loop.
18
+ # Protocol: stdin JSON -> read tool_output.is_error -> stderr warning if failed -> pass through -> exit 0 (always)
19
+
20
+ input=$(cat)
21
+
22
+ is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error // false' 2>/dev/null || echo "false")
23
+
24
+ if [ "$is_error" = "true" ]; then
25
+ agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // .agent_type // "unknown"' 2>/dev/null || echo "unknown")
26
+ error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output // ""' 2>/dev/null | head -c 200)
27
+ echo "" >&2
28
+ echo "--- [Subagent Failure Advisor] background subagent (${agent_type}) reported an error ---" >&2
29
+ echo " ${error_summary}" >&2
30
+ echo " If it FAILED, do NOT auto-continue -- report the failure and wait for user input." >&2
31
+ echo "-----------------------------------------------------------------------------------" >&2
32
+ fi
33
+
34
+ # Always pass through -- advisory only, never blocks, never triggers Stop hook feedback re-injection
35
+ printf '%s\n' "$input"
36
+ exit 0
@@ -12,9 +12,9 @@ command -v jq >/dev/null 2>&1 || exit 0
12
12
  input=$(cat)
13
13
 
14
14
  # Extract task info — support both PostToolUse (tool_input.*) and SubagentStop (top-level) shapes
15
- agent_type=$(echo "$input" | jq -r '.tool_input.subagent_type // .agent_type // "unknown"')
16
- model=$(echo "$input" | jq -r '.tool_input.model // .model // "inherit"')
17
- description=$(echo "$input" | jq -r '.tool_input.description // .description // ""' | head -c 80)
15
+ agent_type=$(printf '%s\n' "$input" | jq -r '.tool_input.subagent_type // .agent_type // "unknown"')
16
+ model=$(printf '%s\n' "$input" | jq -r '.tool_input.model // .model // "inherit"')
17
+ description=$(printf '%s\n' "$input" | jq -r '.tool_input.description // .description // ""' | head -c 80)
18
18
 
19
19
  # Extract skill name from description or prompt
20
20
  skill_name=""
@@ -23,16 +23,16 @@ if echo "$description" | grep -qiE '(skill:|routing|→.*skill)'; then
23
23
  fi
24
24
  # Fallback: check prompt field for "Skill: {name}" pattern
25
25
  if [ -z "$skill_name" ]; then
26
- prompt=$(echo "$input" | jq -r '.tool_input.prompt // ""' | head -c 500)
26
+ prompt=$(printf '%s\n' "$input" | jq -r '.tool_input.prompt // ""' | head -c 500)
27
27
  skill_name=$(echo "$prompt" | grep -oiE 'Skill:\s*[a-z]+-[a-z]+(-[a-z]+)*' | sed 's/[Ss]kill:\s*//' | head -1)
28
28
  fi
29
29
 
30
30
  # Determine outcome
31
- is_error=$(echo "$input" | jq -r '.tool_output.is_error // false')
31
+ is_error=$(printf '%s\n' "$input" | jq -r '.tool_output.is_error // false')
32
32
 
33
33
  if [ "$is_error" = "true" ]; then
34
34
  outcome="failure"
35
- error_summary=$(echo "$input" | jq -r '.tool_output.output // ""' | head -c 200)
35
+ error_summary=$(printf '%s\n' "$input" | jq -r '.tool_output.output // ""' | head -c 200)
36
36
  else
37
37
  outcome="success"
38
38
  error_summary=""
@@ -94,7 +94,7 @@ entry=$(jq -n \
94
94
  --arg dur "$duration_seconds" \
95
95
  '{timestamp: $ts, agent_type: $agent, model: $model, outcome: $outcome, pattern_used: $pattern, skill: $skill, description: $desc, error_summary: $err, duration_seconds: ($dur | tonumber)}')
96
96
 
97
- echo "$entry" >> "$OUTCOME_FILE"
97
+ printf '%s\n' "$entry" >> "$OUTCOME_FILE"
98
98
 
99
99
  # Ring buffer: keep last 50 entries
100
100
  if [ -f "$OUTCOME_FILE" ]; then
@@ -115,5 +115,5 @@ if [ "$outcome" = "failure" ]; then
115
115
  fi
116
116
 
117
117
  # Pass through
118
- echo "$input"
118
+ printf '%s\n' "$input"
119
119
  exit 0
@@ -49,7 +49,7 @@ collect_state() {
49
49
  state=$(collect_state 2>/dev/null)
50
50
 
51
51
  if [ -n "$state" ] && [ "$state" != "{}" ]; then
52
- echo "$state" > "$STATE_FILE"
52
+ printf '%s\n' "$state" > "$STATE_FILE"
53
53
  echo "[PreCompact] Task state saved to $STATE_FILE" >&2
54
54
  else
55
55
  echo "[PreCompact] No active task state to save" >&2
@@ -18,7 +18,7 @@
18
18
  "hooks": [
19
19
  {
20
20
  "type": "command",
21
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.command // \"\") | test(\"(npm run dev|pnpm( run)? dev|yarn dev|bun run dev)\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\ncmd=$(echo \"$input\" | jq -r '.tool_input.command // \"\"')\n\n# Block dev servers that aren't run in tmux\necho '[Hook] BLOCKED: Dev server must run in tmux for log access' >&2\necho '[Hook] Use this command instead:' >&2\necho \"[Hook] tmux new-session -d -s dev 'npm run dev'\" >&2\necho '[Hook] Then: tmux attach -t dev' >&2\nexit 1\n )\nelse\n printf '%s' \"$input\"\nfi"
21
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.command // \"\") | test(\"(npm run dev|pnpm( run)? dev|yarn dev|bun run dev)\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\ncmd=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.command // \"\"')\n\n# Block dev servers that aren't run in tmux\necho '[Hook] BLOCKED: Dev server must run in tmux for log access' >&2\necho '[Hook] Use this command instead:' >&2\necho \"[Hook] tmux new-session -d -s dev 'npm run dev'\" >&2\necho '[Hook] Then: tmux attach -t dev' >&2\nexit 1\n )\nelse\n printf '%s' \"$input\"\nfi"
22
22
  }
23
23
  ],
24
24
  "matcher": "Bash",
@@ -28,7 +28,7 @@
28
28
  "hooks": [
29
29
  {
30
30
  "type": "command",
31
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.command // \"\") | test(\"(npm (install|test)|pnpm (install|test)|yarn (install|test)|bun (install|test)|cargo build|make|docker|pytest|vitest|playwright)\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nif [ -z \"$TMUX\" ]; then\n echo '[Hook] Consider running in tmux for session persistence' >&2\n echo '[Hook] tmux new -s dev | tmux attach -t dev' >&2\nfi\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi"
31
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.command // \"\") | test(\"(npm (install|test)|pnpm (install|test)|yarn (install|test)|bun (install|test)|cargo build|make|docker|pytest|vitest|playwright)\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nif [ -z \"$TMUX\" ]; then\n echo '[Hook] Consider running in tmux for session persistence' >&2\n echo '[Hook] tmux new -s dev | tmux attach -t dev' >&2\nfi\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi"
32
32
  }
33
33
  ],
34
34
  "matcher": "Bash",
@@ -48,7 +48,7 @@
48
48
  "hooks": [
49
49
  {
50
50
  "type": "command",
51
- "command": "#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [[ \"$file_path\" =~ \\.(md|txt)$ ]] && [[ ! \"$file_path\" =~ (README|CLAUDE|AGENT|SKILL)\\.md$ ]]; then\n echo \"[Hook] BLOCKED: Unnecessary documentation file creation\" >&2\n echo \"[Hook] File: $file_path\" >&2\n echo \"[Hook] Use README.md or AGENT.md for documentation instead\" >&2\n exit 1\nfi\n\necho \"$input\""
51
+ "command": "#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [[ \"$file_path\" =~ \\.(md|txt)$ ]] && [[ ! \"$file_path\" =~ (README|CLAUDE|AGENT|SKILL)\\.md$ ]]; then\n echo \"[Hook] BLOCKED: Unnecessary documentation file creation\" >&2\n echo \"[Hook] File: $file_path\" >&2\n echo \"[Hook] Use README.md or AGENT.md for documentation instead\" >&2\n exit 1\nfi\n\nprintf '%s\\n' \"$input\""
52
52
  }
53
53
  ],
54
54
  "matcher": "Write",
@@ -118,7 +118,7 @@
118
118
  "hooks": [
119
119
  {
120
120
  "type": "command",
121
- "command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(echo \"$input\" | jq -r '.tool_input.subagent_type // \"unknown\"')\nmodel=$(echo \"$input\" | jq -r '.tool_input.model // \"inherit\"')\ndesc=$(echo \"$input\" | jq -r '.tool_input.description // \"\"' | head -c 40)\nresume=$(echo \"$input\" | jq -r '.tool_input.resume // empty')\nif [ -n \"$resume\" ]; then\n echo \"─── [Resume] ${agent_type}:${model} | ${desc} ───\" >&2\nelse\n echo \"─── [Spawn] ${agent_type}:${model} | ${desc} ───\" >&2\nfi\necho \"$input\""
121
+ "command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.subagent_type // \"unknown\"')\nmodel=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.model // \"inherit\"')\ndesc=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.description // \"\"' | head -c 40)\nresume=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.resume // empty')\nif [ -n \"$resume\" ]; then\n echo \"─── [Resume] ${agent_type}:${model} | ${desc} ───\" >&2\nelse\n echo \"─── [Spawn] ${agent_type}:${model} | ${desc} ───\" >&2\nfi\nprintf '%s\\n' \"$input\""
122
122
  },
123
123
  {
124
124
  "type": "command",
@@ -256,7 +256,7 @@
256
256
  "hooks": [
257
257
  {
258
258
  "type": "command",
259
- "command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(echo \"$input\" | jq -r '.agent_type // \"unknown\"')\nmodel=$(echo \"$input\" | jq -r '.model // \"inherit\"')\ndesc=$(echo \"$input\" | jq -r '.description // \"\"' | head -c 40)\necho \"─── [SubagentStart] ${agent_type}:${model} | ${desc} ───\" >&2\necho \"$input\""
259
+ "command": "#!/bin/bash\ninput=$(cat)\nagent_type=$(printf '%s\\n' \"$input\" | jq -r '.agent_type // \"unknown\"')\nmodel=$(printf '%s\\n' \"$input\" | jq -r '.model // \"inherit\"')\ndesc=$(printf '%s\\n' \"$input\" | jq -r '.description // \"\"' | head -c 40)\necho \"─── [SubagentStart] ${agent_type}:${model} | ${desc} ───\" >&2\nprintf '%s\\n' \"$input\""
260
260
  },
261
261
  {
262
262
  "type": "command",
@@ -287,12 +287,12 @@
287
287
  "command": "bash .claude/hooks/scripts/r007-r008-drift-advisor.sh"
288
288
  },
289
289
  {
290
- "type": "prompt",
291
- "prompt": "A background subagent just completed. If it FAILED, do NOT auto-continue — report the failure and wait for user input."
290
+ "type": "command",
291
+ "command": "bash .claude/hooks/scripts/subagent-failure-advisor.sh"
292
292
  }
293
293
  ],
294
294
  "matcher": "*",
295
- "description": "Record agent outcomes + auto-continue workflow + R007/R008 drift advisory on subagent completion (autonomous-loop re-entry, #1545)"
295
+ "description": "Record agent outcomes + auto-continue workflow + R007/R008 drift advisory + failure advisory on subagent completion (autonomous-loop re-entry, #1545; #1631 replaced the self-referential prompt-type judge hook with an advisory-only command hook)"
296
296
  }
297
297
  ],
298
298
  "CwdChanged": [
@@ -324,7 +324,7 @@
324
324
  "hooks": [
325
325
  {
326
326
  "type": "command",
327
- "command": "#!/bin/bash\ninput=$(cat)\nmsg=$(echo \"$input\" | jq -r '.message // \"\"' 2>/dev/null)\nif [ -n \"$msg\" ]; then\n echo \"[Hook] Notification: $msg\" >&2\nfi\necho \"$input\""
327
+ "command": "#!/bin/bash\ninput=$(cat)\nmsg=$(printf '%s\\n' \"$input\" | jq -r '.message // \"\"' 2>/dev/null)\nif [ -n \"$msg\" ]; then\n echo \"[Hook] Notification: $msg\" >&2\nfi\nprintf '%s\\n' \"$input\""
328
328
  }
329
329
  ],
330
330
  "matcher": "*",
@@ -336,7 +336,7 @@
336
336
  "hooks": [
337
337
  {
338
338
  "type": "command",
339
- "command": "#!/bin/bash\ninput=$(cat)\nteammate=$(echo \"$input\" | jq -r '.teammate_id // \"unknown\"' 2>/dev/null)\necho \"[Hook] TeammateIdle: $teammate — available for reassignment\" >&2\necho \"$input\""
339
+ "command": "#!/bin/bash\ninput=$(cat)\nteammate=$(printf '%s\\n' \"$input\" | jq -r '.teammate_id // \"unknown\"' 2>/dev/null)\necho \"[Hook] TeammateIdle: $teammate — available for reassignment\" >&2\nprintf '%s\\n' \"$input\""
340
340
  }
341
341
  ],
342
342
  "matcher": "*",
@@ -348,7 +348,7 @@
348
348
  "hooks": [
349
349
  {
350
350
  "type": "command",
351
- "command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(echo \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\ndesc=$(echo \"$input\" | jq -r '.description // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCreated: $task_id — $desc\" >&2\necho \"$input\""
351
+ "command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(printf '%s\\n' \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\ndesc=$(printf '%s\\n' \"$input\" | jq -r '.description // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCreated: $task_id — $desc\" >&2\nprintf '%s\\n' \"$input\""
352
352
  }
353
353
  ],
354
354
  "matcher": "*",
@@ -360,7 +360,7 @@
360
360
  "hooks": [
361
361
  {
362
362
  "type": "command",
363
- "command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(echo \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\nresult=$(echo \"$input\" | jq -r '.result // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCompleted: $task_id — $result\" >&2\necho \"$input\""
363
+ "command": "#!/bin/bash\ninput=$(cat)\ntask_id=$(printf '%s\\n' \"$input\" | jq -r '.task_id // \"\"' 2>/dev/null)\nresult=$(printf '%s\\n' \"$input\" | jq -r '.result // \"\"' 2>/dev/null | head -c 60)\necho \"[Hook] TaskCompleted: $task_id — $result\" >&2\nprintf '%s\\n' \"$input\""
364
364
  }
365
365
  ],
366
366
  "matcher": "*",
@@ -396,7 +396,7 @@
396
396
  "hooks": [
397
397
  {
398
398
  "type": "command",
399
- "command": "#!/bin/bash\ninput=$(cat)\ncmd=$(echo \"$input\" | jq -r '.tool_input.command')\n\nif echo \"$cmd\" | grep -qE 'gh pr create'; then\n output=$(echo \"$input\" | jq -r '.tool_output.output // \"\"')\n pr_url=$(echo \"$output\" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+')\n \n if [ -n \"$pr_url\" ]; then\n echo \"[Hook] PR created: $pr_url\" >&2\n echo \"[Hook] Checking GitHub Actions status...\" >&2\n repo=$(echo \"$pr_url\" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\\1|')\n pr_num=$(echo \"$pr_url\" | sed -E 's|.*/pull/([0-9]+)|\\1|')\n echo \"[Hook] To review PR: gh pr review $pr_num --repo $repo\" >&2\n fi\nfi\n\necho \"$input\""
399
+ "command": "#!/bin/bash\ninput=$(cat)\ncmd=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.command')\n\nif echo \"$cmd\" | grep -qE 'gh pr create'; then\n output=$(printf '%s\\n' \"$input\" | jq -r '.tool_output.output // \"\"')\n pr_url=$(echo \"$output\" | grep -oE 'https://github.com/[^/]+/[^/]+/pull/[0-9]+')\n \n if [ -n \"$pr_url\" ]; then\n echo \"[Hook] PR created: $pr_url\" >&2\n echo \"[Hook] Checking GitHub Actions status...\" >&2\n repo=$(echo \"$pr_url\" | sed -E 's|https://github.com/([^/]+/[^/]+)/pull/[0-9]+|\\1|')\n pr_num=$(echo \"$pr_url\" | sed -E 's|.*/pull/([0-9]+)|\\1|')\n echo \"[Hook] To review PR: gh pr review $pr_num --repo $repo\" >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\""
400
400
  }
401
401
  ],
402
402
  "matcher": "Bash",
@@ -406,7 +406,7 @@
406
406
  "hooks": [
407
407
  {
408
408
  "type": "command",
409
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(ts|tsx|js|jsx)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n if command -v prettier >/dev/null 2>&1; then\n prettier --write \"$file_path\" 2>&1 | head -5 >&2\n fi\nfi\n\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi"
409
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(ts|tsx|js|jsx)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n if command -v prettier >/dev/null 2>&1; then\n prettier --write \"$file_path\" 2>&1 | head -5 >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi"
410
410
  }
411
411
  ],
412
412
  "matcher": "Edit",
@@ -416,7 +416,7 @@
416
416
  "hooks": [
417
417
  {
418
418
  "type": "command",
419
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(ts|tsx)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n dir=$(dirname \"$file_path\")\n project_root=\"$dir\"\n while [ \"$project_root\" != \"/\" ] && [ ! -f \"$project_root/package.json\" ]; do\n project_root=$(dirname \"$project_root\")\n done\n \n if [ -f \"$project_root/tsconfig.json\" ]; then\n cd \"$project_root\" && npx tsc --noEmit --pretty false 2>&1 | grep \"$file_path\" | head -10 >&2 || true\n fi\nfi\n\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
419
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(ts|tsx)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n dir=$(dirname \"$file_path\")\n project_root=\"$dir\"\n while [ \"$project_root\" != \"/\" ] && [ ! -f \"$project_root/package.json\" ]; do\n project_root=$(dirname \"$project_root\")\n done\n \n if [ -f \"$project_root/tsconfig.json\" ]; then\n cd \"$project_root\" && npx tsc --noEmit --pretty false 2>&1 | grep \"$file_path\" | head -10 >&2 || true\n fi\nfi\n\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
420
420
  "continueOnBlock": true
421
421
  }
422
422
  ],
@@ -427,7 +427,7 @@
427
427
  "hooks": [
428
428
  {
429
429
  "type": "command",
430
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(ts|tsx|js|jsx)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n console_logs=$(grep -n \"console\\\\.log\" \"$file_path\" 2>/dev/null || true)\n \n if [ -n \"$console_logs\" ]; then\n echo \"[Hook] WARNING: console.log found in $file_path\" >&2\n echo \"$console_logs\" | head -5 >&2\n echo \"[Hook] Remove console.log before committing\" >&2\n fi\nfi\n\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
430
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(ts|tsx|js|jsx)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n console_logs=$(grep -n \"console\\\\.log\" \"$file_path\" 2>/dev/null || true)\n \n if [ -n \"$console_logs\" ]; then\n echo \"[Hook] WARNING: console.log found in $file_path\" >&2\n echo \"$console_logs\" | head -5 >&2\n echo \"[Hook] Remove console.log before committing\" >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
431
431
  "continueOnBlock": true
432
432
  }
433
433
  ],
@@ -438,7 +438,7 @@
438
438
  "hooks": [
439
439
  {
440
440
  "type": "command",
441
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(go)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n if command -v gofmt >/dev/null 2>&1; then\n gofmt -w \"$file_path\" 2>&1 >&2\n fi\nfi\n\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi"
441
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(go)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n if command -v gofmt >/dev/null 2>&1; then\n gofmt -w \"$file_path\" 2>&1 >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi"
442
442
  }
443
443
  ],
444
444
  "matcher": "Edit",
@@ -448,7 +448,7 @@
448
448
  "hooks": [
449
449
  {
450
450
  "type": "command",
451
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(py)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n # Format with ruff\n if command -v ruff >/dev/null 2>&1; then\n ruff format --quiet \"$file_path\" 2>&1 >&2\n ruff check --fix --quiet \"$file_path\" 2>&1 >&2 || true\n echo \"[Hook] Python formatted with ruff\" >&2\n fi\nfi\n\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
451
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(py)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n # Format with ruff\n if command -v ruff >/dev/null 2>&1; then\n ruff format --quiet \"$file_path\" 2>&1 >&2\n ruff check --fix --quiet \"$file_path\" 2>&1 >&2 || true\n echo \"[Hook] Python formatted with ruff\" >&2\n fi\nfi\n\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
452
452
  "continueOnBlock": true
453
453
  }
454
454
  ],
@@ -459,7 +459,7 @@
459
459
  "hooks": [
460
460
  {
461
461
  "type": "command",
462
- "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(py)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(echo \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n # Type check with ty\n if command -v ty >/dev/null 2>&1; then\n dir=$(dirname \"$file_path\")\n project_root=\"$dir\"\n while [ \"$project_root\" != \"/\" ] && [ ! -f \"$project_root/pyproject.toml\" ]; do\n project_root=$(dirname \"$project_root\")\n done\n \n if [ -f \"$project_root/pyproject.toml\" ]; then\n cd \"$project_root\" && ty check \"$file_path\" 2>&1 | head -10 >&2 || true\n else\n ty check \"$file_path\" 2>&1 | head -10 >&2 || true\n fi\n fi\nfi\n\necho \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
462
+ "command": "input=$(cat)\nif printf '%s' \"$input\" | jq -e '((.tool_input.file_path // \"\") | test(\"\\\\.(py)$\"))' >/dev/null 2>&1; then\n printf '%s' \"$input\" | (\n#!/bin/bash\ninput=$(cat)\nfile_path=$(printf '%s\\n' \"$input\" | jq -r '.tool_input.file_path // \"\"')\n\nif [ -n \"$file_path\" ] && [ -f \"$file_path\" ]; then\n # Type check with ty\n if command -v ty >/dev/null 2>&1; then\n dir=$(dirname \"$file_path\")\n project_root=\"$dir\"\n while [ \"$project_root\" != \"/\" ] && [ ! -f \"$project_root/pyproject.toml\" ]; do\n project_root=$(dirname \"$project_root\")\n done\n \n if [ -f \"$project_root/pyproject.toml\" ]; then\n cd \"$project_root\" && ty check \"$file_path\" 2>&1 | head -10 >&2 || true\n else\n ty check \"$file_path\" 2>&1 | head -10 >&2 || true\n fi\n fi\nfi\n\nprintf '%s\\n' \"$input\"\n )\nelse\n printf '%s' \"$input\"\nfi",
463
463
  "continueOnBlock": true
464
464
  }
465
465
  ],
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "1.1.54",
2
+ "version": "1.1.56",
3
3
  "lastUpdated": "2026-07-14T00:00:00.000Z",
4
4
  "omcustomMinClaudeCode": "2.1.121",
5
5
  "omcustomMinClaudeCodeReason": "Sensitive-path direct Write/Edit on .claude/** under bypassPermissions (R010 deprecation, #1101)",