rhachet-roles-ehmpathy 1.14.0 → 1.15.1
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/logic/roles/mechanic/.skills/claude.hooks/sessionstart.notify-permissions.sh +106 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.cleanup.sh +104 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.findsert.sh +216 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sh +45 -6
- package/dist/logic/roles/mechanic/.skills/init.claude.permissions.jsonc +3 -2
- package/package.json +1 -1
- package/dist/logic/roles/mechanic/.skills/claude.tools/mvsafe.sh +0 -75
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.forbid.stderr.redirect.sh +0 -116
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.pretooluse.sh +0 -118
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sessionstart.sh +0 -113
- /package/dist/logic/roles/mechanic/.skills/claude.hooks/{check.pretooluse.permissions.sh → pretooluse.check-permissions.sh} +0 -0
- /package/dist/logic/roles/mechanic/.skills/claude.hooks/{forbid.stderr.redirect.sh → pretooluse.forbid-stderr-redirect.sh} +0 -0
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
######################################################################
|
|
3
|
+
# .what = SessionStart hook to notify Claude of allowed permissions
|
|
4
|
+
#
|
|
5
|
+
# .why = proactively informing Claude of pre-approved Bash commands
|
|
6
|
+
# at session start reduces interruptions from permission
|
|
7
|
+
# prompts by guiding it to use allowed patterns upfront.
|
|
8
|
+
#
|
|
9
|
+
# this complements the PreToolUse hook which blocks/nudges
|
|
10
|
+
# when Claude attempts unapproved commands, by providing
|
|
11
|
+
# the information before any attempts are made.
|
|
12
|
+
#
|
|
13
|
+
# .how = reads .claude/settings.local.json, extracts Bash permissions,
|
|
14
|
+
# outputs a formatted list of allowed commands for Claude
|
|
15
|
+
# to reference throughout the session.
|
|
16
|
+
#
|
|
17
|
+
# usage:
|
|
18
|
+
# configure in .claude/settings.local.json under hooks.SessionStart
|
|
19
|
+
#
|
|
20
|
+
# guarantee:
|
|
21
|
+
# ✔ non-blocking: always exits 0
|
|
22
|
+
# ✔ informational only: no side effects
|
|
23
|
+
# ✔ graceful fallback: exits silently if no settings found
|
|
24
|
+
######################################################################
|
|
25
|
+
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
# Find the .claude directory (search upward from current directory)
|
|
29
|
+
find_claude_dir() {
|
|
30
|
+
local dir="$PWD"
|
|
31
|
+
while [[ "$dir" != "/" ]]; do
|
|
32
|
+
if [[ -d "$dir/.claude" ]]; then
|
|
33
|
+
echo "$dir/.claude"
|
|
34
|
+
return 0
|
|
35
|
+
fi
|
|
36
|
+
dir="$(dirname "$dir")"
|
|
37
|
+
done
|
|
38
|
+
return 1
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
# Find the settings file
|
|
42
|
+
find_settings_file() {
|
|
43
|
+
local claude_dir
|
|
44
|
+
claude_dir=$(find_claude_dir) || return 1
|
|
45
|
+
local settings_file="$claude_dir/settings.local.json"
|
|
46
|
+
if [[ -f "$settings_file" ]]; then
|
|
47
|
+
echo "$settings_file"
|
|
48
|
+
return 0
|
|
49
|
+
fi
|
|
50
|
+
return 1
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
SETTINGS_FILE=$(find_settings_file) || {
|
|
54
|
+
# No settings file found, exit silently
|
|
55
|
+
exit 0
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
# Extract Bash permissions from settings file
|
|
59
|
+
# Patterns look like: "Bash(npm run test:*)" -> extract "npm run test:*"
|
|
60
|
+
mapfile -t ALLOWED_PATTERNS < <(
|
|
61
|
+
jq -r '.permissions.allow // [] | .[] | select(startswith("Bash(")) | sub("^Bash\\("; "") | sub("\\)$"; "")' "$SETTINGS_FILE" 2>/dev/null
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# If no Bash permissions found, exit silently
|
|
65
|
+
if [[ ${#ALLOWED_PATTERNS[@]} -eq 0 ]]; then
|
|
66
|
+
exit 0
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Transform raw permission pattern to compact bracket notation for display
|
|
70
|
+
format_pattern() {
|
|
71
|
+
local pattern="$1"
|
|
72
|
+
|
|
73
|
+
# Check if pattern ends with :*
|
|
74
|
+
if [[ "$pattern" == *":*" ]]; then
|
|
75
|
+
# Remove :* suffix and format with [p]: label (prefix match)
|
|
76
|
+
local prefix="${pattern%:*}"
|
|
77
|
+
echo "[p]: $prefix"
|
|
78
|
+
else
|
|
79
|
+
# Exact match - format with [e]: label
|
|
80
|
+
echo "[e]: $pattern"
|
|
81
|
+
fi
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Output the allowed permissions notification
|
|
85
|
+
echo ""
|
|
86
|
+
echo "=================================================="
|
|
87
|
+
echo "PRE-APPROVED BASH PERMISSIONS"
|
|
88
|
+
echo "=================================================="
|
|
89
|
+
echo ""
|
|
90
|
+
echo "The following Bash commands are pre-approved and can be used without"
|
|
91
|
+
echo "requesting permission from the user:"
|
|
92
|
+
echo ""
|
|
93
|
+
echo "([e] = exact match, [p] = prefix match - anything starting with this)"
|
|
94
|
+
echo ""
|
|
95
|
+
for pattern in "${ALLOWED_PATTERNS[@]}"; do
|
|
96
|
+
echo " $(format_pattern "$pattern")"
|
|
97
|
+
done
|
|
98
|
+
echo ""
|
|
99
|
+
echo "IMPORTANT: If you attempt a Bash command NOT on this list, you will be"
|
|
100
|
+
echo "blocked and asked to reconsider. Please check this list first before"
|
|
101
|
+
echo "using Bash commands to minimize interruptions to the user."
|
|
102
|
+
echo ""
|
|
103
|
+
echo "=================================================="
|
|
104
|
+
echo ""
|
|
105
|
+
|
|
106
|
+
exit 0
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
######################################################################
|
|
3
|
+
# .what = cleanup stale hooks from Claude settings
|
|
4
|
+
#
|
|
5
|
+
# .why = when hook scripts are removed from claude.hooks/, the
|
|
6
|
+
# corresponding entries in .claude/settings.local.json
|
|
7
|
+
# become stale and should be cleaned up.
|
|
8
|
+
#
|
|
9
|
+
# .how = reads settings.local.json, finds hooks referencing files
|
|
10
|
+
# in claude.hooks/, checks if those files exist, and removes
|
|
11
|
+
# any hooks whose scripts no longer exist.
|
|
12
|
+
#
|
|
13
|
+
# usage:
|
|
14
|
+
# init.claude.hooks.cleanup.sh
|
|
15
|
+
#
|
|
16
|
+
# guarantee:
|
|
17
|
+
# ✔ only removes hooks referencing missing claude.hooks/ files
|
|
18
|
+
# ✔ preserves all other hooks and settings
|
|
19
|
+
# ✔ idempotent: safe to rerun
|
|
20
|
+
# ✔ no-op if no stale hooks found
|
|
21
|
+
######################################################################
|
|
22
|
+
|
|
23
|
+
set -euo pipefail
|
|
24
|
+
|
|
25
|
+
SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
26
|
+
HOOKS_DIR="$SKILLS_DIR/claude.hooks"
|
|
27
|
+
|
|
28
|
+
PROJECT_ROOT="$PWD"
|
|
29
|
+
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
30
|
+
|
|
31
|
+
# Exit if no settings file
|
|
32
|
+
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
33
|
+
exit 0
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Extract all hook commands that reference claude.hooks/
|
|
37
|
+
# and check which ones point to missing files
|
|
38
|
+
STALE_COMMANDS=$(jq -r '
|
|
39
|
+
.hooks // {} | to_entries[] |
|
|
40
|
+
.value[] | .hooks[] | .command // empty
|
|
41
|
+
' "$SETTINGS_FILE" 2>/dev/null | grep -E "claude\.hooks/" | while read -r cmd; do
|
|
42
|
+
# Extract the path - it might be absolute or relative
|
|
43
|
+
# Look for the claude.hooks/ part and check if the file exists
|
|
44
|
+
if [[ "$cmd" == /* ]]; then
|
|
45
|
+
# Absolute path
|
|
46
|
+
if [[ ! -f "$cmd" ]]; then
|
|
47
|
+
echo "$cmd"
|
|
48
|
+
fi
|
|
49
|
+
else
|
|
50
|
+
# Relative path or command - check if it contains claude.hooks/
|
|
51
|
+
# and if the file exists relative to PWD
|
|
52
|
+
if [[ ! -f "$PROJECT_ROOT/$cmd" ]]; then
|
|
53
|
+
echo "$cmd"
|
|
54
|
+
fi
|
|
55
|
+
fi
|
|
56
|
+
done)
|
|
57
|
+
|
|
58
|
+
# Exit if no stale commands found
|
|
59
|
+
if [[ -z "$STALE_COMMANDS" ]]; then
|
|
60
|
+
echo "👌 no stale hooks found"
|
|
61
|
+
exit 0
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# Build jq filter to remove stale hooks
|
|
65
|
+
# Convert stale commands to JSON array for jq
|
|
66
|
+
STALE_JSON=$(echo "$STALE_COMMANDS" | jq -R -s 'split("\n") | map(select(length > 0))')
|
|
67
|
+
|
|
68
|
+
jq --argjson stale "$STALE_JSON" '
|
|
69
|
+
# Remove hooks whose command is in the stale list
|
|
70
|
+
.hooks |= (
|
|
71
|
+
if . then
|
|
72
|
+
to_entries | map(
|
|
73
|
+
.value |= map(
|
|
74
|
+
.hooks |= map(select(.command as $cmd | ($stale | index($cmd)) == null))
|
|
75
|
+
)
|
|
76
|
+
# Remove matchers with empty hooks arrays
|
|
77
|
+
| .value |= map(select(.hooks | length > 0))
|
|
78
|
+
)
|
|
79
|
+
# Remove hook types with empty arrays
|
|
80
|
+
| map(select(.value | length > 0))
|
|
81
|
+
| from_entries
|
|
82
|
+
else
|
|
83
|
+
.
|
|
84
|
+
end
|
|
85
|
+
)
|
|
86
|
+
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
87
|
+
|
|
88
|
+
# Check if any changes were made
|
|
89
|
+
if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
|
|
90
|
+
rm "$SETTINGS_FILE.tmp"
|
|
91
|
+
echo "👌 no stale hooks found"
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# Report what's being removed
|
|
96
|
+
echo "🧹 removing stale hooks:"
|
|
97
|
+
echo "$STALE_COMMANDS" | while read -r cmd; do
|
|
98
|
+
echo " - $cmd"
|
|
99
|
+
done
|
|
100
|
+
|
|
101
|
+
# Atomic replace
|
|
102
|
+
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
103
|
+
|
|
104
|
+
echo "✨ cleanup complete"
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
######################################################################
|
|
3
|
+
# .what = generic findsert utility for Claude hooks
|
|
4
|
+
#
|
|
5
|
+
# .why = centralizes the "find-or-insert" logic for binding hooks
|
|
6
|
+
# to .claude/settings.local.json, avoiding duplication across
|
|
7
|
+
# individual hook initializers.
|
|
8
|
+
#
|
|
9
|
+
# .how = takes hook configuration as arguments and uses jq to merge
|
|
10
|
+
# the hook into the settings file, creating structure if absent.
|
|
11
|
+
#
|
|
12
|
+
# usage:
|
|
13
|
+
# init.claude.hooks.findsert.sh \
|
|
14
|
+
# --hook-type SessionStart|PreToolUse \
|
|
15
|
+
# --matcher "*"|"Bash"|... \
|
|
16
|
+
# --command "command to run" \
|
|
17
|
+
# --name "hookname" \
|
|
18
|
+
# [--timeout 5] \
|
|
19
|
+
# [--position append|prepend]
|
|
20
|
+
#
|
|
21
|
+
# guarantee:
|
|
22
|
+
# ✔ creates .claude/settings.local.json if missing
|
|
23
|
+
# ✔ preserves existing settings (permissions, other hooks)
|
|
24
|
+
# ✔ idempotent: no-op if hook already present (at correct position)
|
|
25
|
+
# ✔ fail-fast on errors
|
|
26
|
+
######################################################################
|
|
27
|
+
|
|
28
|
+
set -euo pipefail
|
|
29
|
+
|
|
30
|
+
# Defaults
|
|
31
|
+
HOOK_TYPE=""
|
|
32
|
+
MATCHER=""
|
|
33
|
+
HOOK_COMMAND=""
|
|
34
|
+
HOOK_NAME=""
|
|
35
|
+
TIMEOUT=5
|
|
36
|
+
POSITION="append"
|
|
37
|
+
|
|
38
|
+
# Parse arguments
|
|
39
|
+
while [[ $# -gt 0 ]]; do
|
|
40
|
+
case "$1" in
|
|
41
|
+
--hook-type)
|
|
42
|
+
HOOK_TYPE="$2"
|
|
43
|
+
shift 2
|
|
44
|
+
;;
|
|
45
|
+
--matcher)
|
|
46
|
+
MATCHER="$2"
|
|
47
|
+
shift 2
|
|
48
|
+
;;
|
|
49
|
+
--command)
|
|
50
|
+
HOOK_COMMAND="$2"
|
|
51
|
+
shift 2
|
|
52
|
+
;;
|
|
53
|
+
--name)
|
|
54
|
+
HOOK_NAME="$2"
|
|
55
|
+
shift 2
|
|
56
|
+
;;
|
|
57
|
+
--timeout)
|
|
58
|
+
TIMEOUT="$2"
|
|
59
|
+
shift 2
|
|
60
|
+
;;
|
|
61
|
+
--position)
|
|
62
|
+
POSITION="$2"
|
|
63
|
+
shift 2
|
|
64
|
+
;;
|
|
65
|
+
*)
|
|
66
|
+
echo "Unknown argument: $1" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
;;
|
|
69
|
+
esac
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
# Validate required arguments
|
|
73
|
+
if [[ -z "$HOOK_TYPE" || -z "$MATCHER" || -z "$HOOK_COMMAND" || -z "$HOOK_NAME" ]]; then
|
|
74
|
+
echo "Usage: $0 --hook-type TYPE --matcher MATCHER --command CMD --name NAME [--timeout SECS] [--position append|prepend]" >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
PROJECT_ROOT="$PWD"
|
|
79
|
+
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
80
|
+
|
|
81
|
+
# Ensure .claude directory exists
|
|
82
|
+
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
83
|
+
|
|
84
|
+
# Initialize settings file if it doesn't exist
|
|
85
|
+
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
86
|
+
echo "{}" > "$SETTINGS_FILE"
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# Build the hook configuration JSON
|
|
90
|
+
HOOK_CONFIG=$(jq -n \
|
|
91
|
+
--arg hookType "$HOOK_TYPE" \
|
|
92
|
+
--arg matcher "$MATCHER" \
|
|
93
|
+
--arg command "$HOOK_COMMAND" \
|
|
94
|
+
--argjson timeout "$TIMEOUT" \
|
|
95
|
+
'{
|
|
96
|
+
hooks: {
|
|
97
|
+
($hookType): [
|
|
98
|
+
{
|
|
99
|
+
matcher: $matcher,
|
|
100
|
+
hooks: [
|
|
101
|
+
{
|
|
102
|
+
type: "command",
|
|
103
|
+
command: $command,
|
|
104
|
+
timeout: $timeout
|
|
105
|
+
}
|
|
106
|
+
]
|
|
107
|
+
}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}'
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Generate jq script based on position (append vs prepend)
|
|
114
|
+
if [[ "$POSITION" == "prepend" ]]; then
|
|
115
|
+
# Prepend: ensure hook is first in the array
|
|
116
|
+
JQ_SCRIPT=$(cat <<'JQEOF'
|
|
117
|
+
def hookType: $hookType;
|
|
118
|
+
def matcher: $matcher;
|
|
119
|
+
def targetCmd: $hook.hooks[hookType][0].hooks[0].command;
|
|
120
|
+
|
|
121
|
+
# Check if hook is already first in the matcher
|
|
122
|
+
def hookIsFirst:
|
|
123
|
+
(.hooks[hookType] // [])
|
|
124
|
+
| map(select(.matcher == matcher) | .hooks // [])
|
|
125
|
+
| flatten
|
|
126
|
+
| first
|
|
127
|
+
| .command == targetCmd;
|
|
128
|
+
|
|
129
|
+
# If hook is already first, return unchanged
|
|
130
|
+
if hookIsFirst then
|
|
131
|
+
.
|
|
132
|
+
else
|
|
133
|
+
# Ensure .hooks exists
|
|
134
|
+
.hooks //= {} |
|
|
135
|
+
|
|
136
|
+
# Ensure .hooks[hookType] exists
|
|
137
|
+
.hooks[hookType] //= [] |
|
|
138
|
+
|
|
139
|
+
# Check if our matcher already exists
|
|
140
|
+
if (.hooks[hookType] | map(.matcher) | index(matcher)) then
|
|
141
|
+
# Matcher exists - remove our hook if present, then prepend it
|
|
142
|
+
.hooks[hookType] |= map(
|
|
143
|
+
if .matcher == matcher then
|
|
144
|
+
.hooks = $hook.hooks[hookType][0].hooks + (.hooks | map(select(.command != targetCmd)))
|
|
145
|
+
else
|
|
146
|
+
.
|
|
147
|
+
end
|
|
148
|
+
)
|
|
149
|
+
else
|
|
150
|
+
# Matcher does not exist, add the entire entry
|
|
151
|
+
.hooks[hookType] += $hook.hooks[hookType]
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
JQEOF
|
|
155
|
+
)
|
|
156
|
+
else
|
|
157
|
+
# Append: add hook to end of array (default)
|
|
158
|
+
JQ_SCRIPT=$(cat <<'JQEOF'
|
|
159
|
+
def hookType: $hookType;
|
|
160
|
+
def matcher: $matcher;
|
|
161
|
+
def targetCmd: $hook.hooks[hookType][0].hooks[0].command;
|
|
162
|
+
|
|
163
|
+
# Check if hook already exists anywhere
|
|
164
|
+
def hookExists:
|
|
165
|
+
(.hooks[hookType] // [])
|
|
166
|
+
| map(select(.matcher == matcher) | .hooks // [])
|
|
167
|
+
| flatten
|
|
168
|
+
| map(.command)
|
|
169
|
+
| any(. == targetCmd);
|
|
170
|
+
|
|
171
|
+
# If hook already exists, return unchanged
|
|
172
|
+
if hookExists then
|
|
173
|
+
.
|
|
174
|
+
else
|
|
175
|
+
# Ensure .hooks exists
|
|
176
|
+
.hooks //= {} |
|
|
177
|
+
|
|
178
|
+
# Ensure .hooks[hookType] exists
|
|
179
|
+
.hooks[hookType] //= [] |
|
|
180
|
+
|
|
181
|
+
# Check if our matcher already exists
|
|
182
|
+
if (.hooks[hookType] | map(.matcher) | index(matcher)) then
|
|
183
|
+
# Matcher exists, add our hook to its hooks array
|
|
184
|
+
.hooks[hookType] |= map(
|
|
185
|
+
if .matcher == matcher then
|
|
186
|
+
.hooks += $hook.hooks[hookType][0].hooks
|
|
187
|
+
else
|
|
188
|
+
.
|
|
189
|
+
end
|
|
190
|
+
)
|
|
191
|
+
else
|
|
192
|
+
# Matcher does not exist, add the entire entry
|
|
193
|
+
.hooks[hookType] += $hook.hooks[hookType]
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
JQEOF
|
|
197
|
+
)
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
# Run jq with the appropriate script
|
|
201
|
+
jq --argjson hook "$HOOK_CONFIG" \
|
|
202
|
+
--arg hookType "$HOOK_TYPE" \
|
|
203
|
+
--arg matcher "$MATCHER" \
|
|
204
|
+
"$JQ_SCRIPT" "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
205
|
+
|
|
206
|
+
# Check if any changes were made
|
|
207
|
+
if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
|
|
208
|
+
rm "$SETTINGS_FILE.tmp"
|
|
209
|
+
echo "👌 $HOOK_NAME hook already bound"
|
|
210
|
+
exit 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
# Atomic replace
|
|
214
|
+
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
215
|
+
|
|
216
|
+
echo "🔗 $HOOK_NAME hook bound successfully!"
|
|
@@ -4,11 +4,14 @@
|
|
|
4
4
|
#
|
|
5
5
|
# .why = the mechanic role uses multiple hooks:
|
|
6
6
|
# • SessionStart: boot mechanic on every session
|
|
7
|
+
# • SessionStart: notify Claude of allowed permissions upfront
|
|
8
|
+
# • PreToolUse: forbid stderr redirects (2>&1)
|
|
7
9
|
# • PreToolUse: check existing permissions before new requests
|
|
8
10
|
#
|
|
9
|
-
# this script
|
|
11
|
+
# this script manages hook registration via findsert utility.
|
|
10
12
|
#
|
|
11
|
-
# .how = runs
|
|
13
|
+
# .how = 1. runs cleanup to remove stale hooks (deleted scripts)
|
|
14
|
+
# 2. calls findsert for each hook in desired order
|
|
12
15
|
#
|
|
13
16
|
# guarantee:
|
|
14
17
|
# ✔ idempotent: safe to rerun
|
|
@@ -18,8 +21,44 @@
|
|
|
18
21
|
set -euo pipefail
|
|
19
22
|
|
|
20
23
|
SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
+
FINDSERT="$SKILLS_DIR/init.claude.hooks.findsert.sh"
|
|
25
|
+
CLEANUP="$SKILLS_DIR/init.claude.hooks.cleanup.sh"
|
|
21
26
|
|
|
22
|
-
#
|
|
23
|
-
"$SKILLS_DIR/
|
|
24
|
-
|
|
25
|
-
|
|
27
|
+
# Path to hook scripts (relative to this script)
|
|
28
|
+
HOOKS_DIR="$SKILLS_DIR/claude.hooks"
|
|
29
|
+
|
|
30
|
+
# First, cleanup any stale hooks (referencing removed scripts)
|
|
31
|
+
"$CLEANUP"
|
|
32
|
+
|
|
33
|
+
# SessionStart hooks (order matters - boot first, then notify permissions)
|
|
34
|
+
|
|
35
|
+
"$FINDSERT" \
|
|
36
|
+
--hook-type SessionStart \
|
|
37
|
+
--matcher "*" \
|
|
38
|
+
--command "npx rhachet roles boot --repo ehmpathy --role mechanic" \
|
|
39
|
+
--name "sessionstart.boot" \
|
|
40
|
+
--timeout 60
|
|
41
|
+
|
|
42
|
+
"$FINDSERT" \
|
|
43
|
+
--hook-type SessionStart \
|
|
44
|
+
--matcher "*" \
|
|
45
|
+
--command "$HOOKS_DIR/sessionstart.notify-permissions.sh" \
|
|
46
|
+
--name "sessionstart.notify-permissions" \
|
|
47
|
+
--timeout 5
|
|
48
|
+
|
|
49
|
+
# PreToolUse hooks (order matters - forbid-stderr-redirect first via prepend)
|
|
50
|
+
|
|
51
|
+
"$FINDSERT" \
|
|
52
|
+
--hook-type PreToolUse \
|
|
53
|
+
--matcher "Bash" \
|
|
54
|
+
--command "$HOOKS_DIR/pretooluse.forbid-stderr-redirect.sh" \
|
|
55
|
+
--name "pretooluse.forbid-stderr-redirect" \
|
|
56
|
+
--timeout 5 \
|
|
57
|
+
--position prepend
|
|
58
|
+
|
|
59
|
+
"$FINDSERT" \
|
|
60
|
+
--hook-type PreToolUse \
|
|
61
|
+
--matcher "Bash" \
|
|
62
|
+
--command "$HOOKS_DIR/pretooluse.check-permissions.sh" \
|
|
63
|
+
--name "pretooluse.check-permissions" \
|
|
64
|
+
--timeout 5
|
|
@@ -135,8 +135,9 @@
|
|
|
135
135
|
"Bash(mkdir:*)",
|
|
136
136
|
"Bash(pwd)",
|
|
137
137
|
|
|
138
|
-
// safe
|
|
139
|
-
"Bash(
|
|
138
|
+
// git mv/rm are safe - constrained to repo, all changes revertable
|
|
139
|
+
"Bash(git mv:*)",
|
|
140
|
+
"Bash(git rm:*)",
|
|
140
141
|
|
|
141
142
|
// npm read operations
|
|
142
143
|
"Bash(npm view:*)",
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "rhachet-roles-ehmpathy",
|
|
3
3
|
"author": "ehmpathy",
|
|
4
4
|
"description": "empathetic software construction roles and skills, via rhachet",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.15.1",
|
|
6
6
|
"repository": "ehmpathy/rhachet-roles-ehmpathy",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
|
|
8
8
|
"keywords": [
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = safe mv wrapper that constrains moves to within the repo
|
|
4
|
-
#
|
|
5
|
-
# .why = mv can move/overwrite files anywhere on the filesystem.
|
|
6
|
-
# this wrapper ensures both source and destination resolve
|
|
7
|
-
# to paths within the current working directory (repo root).
|
|
8
|
-
#
|
|
9
|
-
# .how = uses realpath to resolve absolute paths, then validates
|
|
10
|
-
# both are prefixed by $PWD before executing mv.
|
|
11
|
-
#
|
|
12
|
-
# usage:
|
|
13
|
-
# bash mvsafe.sh <source> <destination>
|
|
14
|
-
#
|
|
15
|
-
# guarantee:
|
|
16
|
-
# ✔ fails if source is outside repo
|
|
17
|
-
# ✔ fails if destination is outside repo
|
|
18
|
-
# ✔ fails if source doesn't exist
|
|
19
|
-
# ✔ passes all arguments to mv if validation passes
|
|
20
|
-
######################################################################
|
|
21
|
-
|
|
22
|
-
set -euo pipefail
|
|
23
|
-
|
|
24
|
-
if [[ $# -lt 2 ]]; then
|
|
25
|
-
echo "error: mvsafe requires at least 2 arguments" >&2
|
|
26
|
-
echo "usage: mvsafe.sh <source> <destination>" >&2
|
|
27
|
-
exit 1
|
|
28
|
-
fi
|
|
29
|
-
|
|
30
|
-
REPO_ROOT="$PWD"
|
|
31
|
-
|
|
32
|
-
# get the last argument (destination)
|
|
33
|
-
DEST="${*: -1}"
|
|
34
|
-
|
|
35
|
-
# get all arguments except the last (sources, could be multiple)
|
|
36
|
-
SOURCES=("${@:1:$#-1}")
|
|
37
|
-
|
|
38
|
-
# resolve destination path
|
|
39
|
-
# if dest doesn't exist yet, resolve its parent directory
|
|
40
|
-
if [[ -e "$DEST" ]]; then
|
|
41
|
-
DEST_RESOLVED="$(realpath "$DEST")"
|
|
42
|
-
else
|
|
43
|
-
DEST_PARENT="$(dirname "$DEST")"
|
|
44
|
-
if [[ ! -d "$DEST_PARENT" ]]; then
|
|
45
|
-
echo "error: destination parent directory does not exist: $DEST_PARENT" >&2
|
|
46
|
-
exit 1
|
|
47
|
-
fi
|
|
48
|
-
DEST_RESOLVED="$(realpath "$DEST_PARENT")/$(basename "$DEST")"
|
|
49
|
-
fi
|
|
50
|
-
|
|
51
|
-
# validate destination is within repo
|
|
52
|
-
if [[ "$DEST_RESOLVED" != "$REPO_ROOT"* ]]; then
|
|
53
|
-
echo "error: destination is outside repo: $DEST_RESOLVED" >&2
|
|
54
|
-
echo " repo root: $REPO_ROOT" >&2
|
|
55
|
-
exit 1
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
# validate each source is within repo
|
|
59
|
-
for SRC in "${SOURCES[@]}"; do
|
|
60
|
-
if [[ ! -e "$SRC" ]]; then
|
|
61
|
-
echo "error: source does not exist: $SRC" >&2
|
|
62
|
-
exit 1
|
|
63
|
-
fi
|
|
64
|
-
|
|
65
|
-
SRC_RESOLVED="$(realpath "$SRC")"
|
|
66
|
-
|
|
67
|
-
if [[ "$SRC_RESOLVED" != "$REPO_ROOT"* ]]; then
|
|
68
|
-
echo "error: source is outside repo: $SRC_RESOLVED" >&2
|
|
69
|
-
echo " repo root: $REPO_ROOT" >&2
|
|
70
|
-
exit 1
|
|
71
|
-
fi
|
|
72
|
-
done
|
|
73
|
-
|
|
74
|
-
# all validations passed, execute mv
|
|
75
|
-
exec mv "$@"
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = bind forbid.stderr.redirect hook to Claude settings
|
|
4
|
-
#
|
|
5
|
-
# .why = when Claude uses 2>&1, error messages are hidden and
|
|
6
|
-
# debugging becomes harder. this hook blocks such commands.
|
|
7
|
-
#
|
|
8
|
-
# .how = uses jq to findsert the PreToolUse hook configuration
|
|
9
|
-
# into .claude/settings.local.json
|
|
10
|
-
#
|
|
11
|
-
# guarantee:
|
|
12
|
-
# ✔ creates .claude/settings.local.json if missing
|
|
13
|
-
# ✔ preserves existing settings (permissions, other hooks)
|
|
14
|
-
# ✔ idempotent: no-op if hook already present
|
|
15
|
-
# ✔ prepends to hooks array (runs before other Bash hooks)
|
|
16
|
-
# ✔ fail-fast on errors
|
|
17
|
-
######################################################################
|
|
18
|
-
|
|
19
|
-
set -euo pipefail
|
|
20
|
-
|
|
21
|
-
PROJECT_ROOT="$PWD"
|
|
22
|
-
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
23
|
-
SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
24
|
-
HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/forbid.stderr.redirect.sh"
|
|
25
|
-
|
|
26
|
-
# Verify hook script exists
|
|
27
|
-
if [[ ! -f "$HOOK_SCRIPT" ]]; then
|
|
28
|
-
echo "❌ hook script not found: $HOOK_SCRIPT" >&2
|
|
29
|
-
exit 1
|
|
30
|
-
fi
|
|
31
|
-
|
|
32
|
-
# Define the hook configuration to findsert
|
|
33
|
-
HOOK_CONFIG=$(cat <<EOF
|
|
34
|
-
{
|
|
35
|
-
"hooks": {
|
|
36
|
-
"PreToolUse": [
|
|
37
|
-
{
|
|
38
|
-
"matcher": "Bash",
|
|
39
|
-
"hooks": [
|
|
40
|
-
{
|
|
41
|
-
"type": "command",
|
|
42
|
-
"command": "$HOOK_SCRIPT",
|
|
43
|
-
"timeout": 5
|
|
44
|
-
}
|
|
45
|
-
]
|
|
46
|
-
}
|
|
47
|
-
]
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
EOF
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
# Ensure .claude directory exists
|
|
54
|
-
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
55
|
-
|
|
56
|
-
# Initialize settings file if it doesn't exist
|
|
57
|
-
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
58
|
-
echo "{}" > "$SETTINGS_FILE"
|
|
59
|
-
fi
|
|
60
|
-
|
|
61
|
-
# Findsert: ensure hook is at the front of the hooks array
|
|
62
|
-
jq --argjson hook "$HOOK_CONFIG" '
|
|
63
|
-
# Define the target command for comparison
|
|
64
|
-
def targetCmd: $hook.hooks.PreToolUse[0].hooks[0].command;
|
|
65
|
-
|
|
66
|
-
# Check if hook is already first in the Bash matcher
|
|
67
|
-
def hookIsFirst:
|
|
68
|
-
(.hooks.PreToolUse // [])
|
|
69
|
-
| map(select(.matcher == "Bash") | .hooks // [])
|
|
70
|
-
| flatten
|
|
71
|
-
| first
|
|
72
|
-
| .command == targetCmd;
|
|
73
|
-
|
|
74
|
-
# If hook is already first, return unchanged
|
|
75
|
-
if hookIsFirst then
|
|
76
|
-
.
|
|
77
|
-
else
|
|
78
|
-
# Ensure .hooks exists
|
|
79
|
-
.hooks //= {} |
|
|
80
|
-
|
|
81
|
-
# Ensure .hooks.PreToolUse exists
|
|
82
|
-
.hooks.PreToolUse //= [] |
|
|
83
|
-
|
|
84
|
-
# Check if our matcher already exists
|
|
85
|
-
if (.hooks.PreToolUse | map(.matcher) | index("Bash")) then
|
|
86
|
-
# Matcher exists - remove our hook if present, then prepend it
|
|
87
|
-
.hooks.PreToolUse |= map(
|
|
88
|
-
if .matcher == "Bash" then
|
|
89
|
-
# Remove existing instance of our hook, then prepend
|
|
90
|
-
.hooks = $hook.hooks.PreToolUse[0].hooks + (.hooks | map(select(.command != targetCmd)))
|
|
91
|
-
else
|
|
92
|
-
.
|
|
93
|
-
end
|
|
94
|
-
)
|
|
95
|
-
else
|
|
96
|
-
# Matcher does not exist, add the entire entry
|
|
97
|
-
.hooks.PreToolUse += $hook.hooks.PreToolUse
|
|
98
|
-
end
|
|
99
|
-
end
|
|
100
|
-
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
101
|
-
|
|
102
|
-
# Check if any changes were made
|
|
103
|
-
if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
|
|
104
|
-
rm "$SETTINGS_FILE.tmp"
|
|
105
|
-
echo "👌 forbid.stderr.redirect hook already bound"
|
|
106
|
-
echo " $SETTINGS_FILE"
|
|
107
|
-
exit 0
|
|
108
|
-
fi
|
|
109
|
-
|
|
110
|
-
# Atomic replace
|
|
111
|
-
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
112
|
-
|
|
113
|
-
echo "🔗 forbid.stderr.redirect hook bound successfully!"
|
|
114
|
-
echo " $SETTINGS_FILE"
|
|
115
|
-
echo ""
|
|
116
|
-
echo "✨ Claude will now be blocked from using 2>&1 in commands"
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = bind mechanic PreToolUse hook to Claude settings
|
|
4
|
-
#
|
|
5
|
-
# .why = when Claude attempts a Bash command not covered by existing
|
|
6
|
-
# permissions, this hook provides feedback asking it to
|
|
7
|
-
# reconsider whether a pre-approved command could work instead.
|
|
8
|
-
#
|
|
9
|
-
# this reduces unnecessary permission prompts and encourages
|
|
10
|
-
# consistent command patterns across the project.
|
|
11
|
-
#
|
|
12
|
-
# .how = uses jq to findsert the PreToolUse hook configuration
|
|
13
|
-
# into .claude/settings.local.json
|
|
14
|
-
#
|
|
15
|
-
# guarantee:
|
|
16
|
-
# ✔ creates .claude/settings.local.json if missing
|
|
17
|
-
# ✔ preserves existing settings (permissions, other hooks)
|
|
18
|
-
# ✔ idempotent: no-op if hook already present
|
|
19
|
-
# ✔ fail-fast on errors
|
|
20
|
-
######################################################################
|
|
21
|
-
|
|
22
|
-
set -euo pipefail
|
|
23
|
-
|
|
24
|
-
PROJECT_ROOT="$PWD"
|
|
25
|
-
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
26
|
-
SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
27
|
-
HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/check.pretooluse.permissions.sh"
|
|
28
|
-
|
|
29
|
-
# Verify hook script exists
|
|
30
|
-
if [[ ! -f "$HOOK_SCRIPT" ]]; then
|
|
31
|
-
echo "❌ hook script not found: $HOOK_SCRIPT" >&2
|
|
32
|
-
exit 1
|
|
33
|
-
fi
|
|
34
|
-
|
|
35
|
-
# Define the hook configuration to findsert
|
|
36
|
-
HOOK_CONFIG=$(cat <<EOF
|
|
37
|
-
{
|
|
38
|
-
"hooks": {
|
|
39
|
-
"PreToolUse": [
|
|
40
|
-
{
|
|
41
|
-
"matcher": "Bash",
|
|
42
|
-
"hooks": [
|
|
43
|
-
{
|
|
44
|
-
"type": "command",
|
|
45
|
-
"command": "$HOOK_SCRIPT",
|
|
46
|
-
"timeout": 5
|
|
47
|
-
}
|
|
48
|
-
]
|
|
49
|
-
}
|
|
50
|
-
]
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
EOF
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Ensure .claude directory exists
|
|
57
|
-
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
58
|
-
|
|
59
|
-
# Initialize settings file if it doesn't exist
|
|
60
|
-
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
61
|
-
echo "{}" > "$SETTINGS_FILE"
|
|
62
|
-
fi
|
|
63
|
-
|
|
64
|
-
# Findsert: merge the hook configuration if not already present
|
|
65
|
-
jq --argjson hook "$HOOK_CONFIG" '
|
|
66
|
-
# Define the target command for comparison
|
|
67
|
-
def targetCmd: $hook.hooks.PreToolUse[0].hooks[0].command;
|
|
68
|
-
|
|
69
|
-
# Check if hook already exists
|
|
70
|
-
def hookExists:
|
|
71
|
-
(.hooks.PreToolUse // [])
|
|
72
|
-
| map(select(.matcher == "Bash") | .hooks // [])
|
|
73
|
-
| flatten
|
|
74
|
-
| map(.command)
|
|
75
|
-
| any(. == targetCmd);
|
|
76
|
-
|
|
77
|
-
# If hook already exists, return unchanged
|
|
78
|
-
if hookExists then
|
|
79
|
-
.
|
|
80
|
-
else
|
|
81
|
-
# Ensure .hooks exists
|
|
82
|
-
.hooks //= {} |
|
|
83
|
-
|
|
84
|
-
# Ensure .hooks.PreToolUse exists
|
|
85
|
-
.hooks.PreToolUse //= [] |
|
|
86
|
-
|
|
87
|
-
# Check if our matcher already exists
|
|
88
|
-
if (.hooks.PreToolUse | map(.matcher) | index("Bash")) then
|
|
89
|
-
# Matcher exists, add our hook to its hooks array
|
|
90
|
-
.hooks.PreToolUse |= map(
|
|
91
|
-
if .matcher == "Bash" then
|
|
92
|
-
.hooks += $hook.hooks.PreToolUse[0].hooks
|
|
93
|
-
else
|
|
94
|
-
.
|
|
95
|
-
end
|
|
96
|
-
)
|
|
97
|
-
else
|
|
98
|
-
# Matcher does not exist, add the entire entry
|
|
99
|
-
.hooks.PreToolUse += $hook.hooks.PreToolUse
|
|
100
|
-
end
|
|
101
|
-
end
|
|
102
|
-
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
103
|
-
|
|
104
|
-
# Check if any changes were made
|
|
105
|
-
if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
|
|
106
|
-
rm "$SETTINGS_FILE.tmp"
|
|
107
|
-
echo "👌 mechanic PreToolUse hook already bound"
|
|
108
|
-
echo " $SETTINGS_FILE"
|
|
109
|
-
exit 0
|
|
110
|
-
fi
|
|
111
|
-
|
|
112
|
-
# Atomic replace
|
|
113
|
-
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
114
|
-
|
|
115
|
-
echo "🔗 mechanic PreToolUse hook bound successfully!"
|
|
116
|
-
echo " $SETTINGS_FILE"
|
|
117
|
-
echo ""
|
|
118
|
-
echo "✨ Claude will now be reminded to check existing permissions before requesting new ones"
|
|
@@ -1,113 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = bind mechanic SessionStart hook to Claude settings
|
|
4
|
-
#
|
|
5
|
-
# .why = the mechanic role needs to boot on every Claude session
|
|
6
|
-
# to ensure project context and briefs are loaded.
|
|
7
|
-
#
|
|
8
|
-
# this script "findserts" (find-or-insert) the SessionStart
|
|
9
|
-
# hook into .claude/settings.local.json, ensuring:
|
|
10
|
-
# • the hook is present after running this skill
|
|
11
|
-
# • no duplication if already present
|
|
12
|
-
# • idempotent: safe to rerun
|
|
13
|
-
#
|
|
14
|
-
# .how = uses jq to merge the SessionStart hook configuration
|
|
15
|
-
# into the existing hooks structure, creating it if absent.
|
|
16
|
-
#
|
|
17
|
-
# guarantee:
|
|
18
|
-
# ✔ creates .claude/settings.local.json if missing
|
|
19
|
-
# ✔ preserves existing settings (permissions, other hooks)
|
|
20
|
-
# ✔ idempotent: no-op if hook already present
|
|
21
|
-
# ✔ fail-fast on errors
|
|
22
|
-
######################################################################
|
|
23
|
-
|
|
24
|
-
set -euo pipefail
|
|
25
|
-
|
|
26
|
-
PROJECT_ROOT="$PWD"
|
|
27
|
-
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
28
|
-
|
|
29
|
-
# Define the hook configuration to findsert
|
|
30
|
-
HOOK_CONFIG=$(cat <<'EOF'
|
|
31
|
-
{
|
|
32
|
-
"hooks": {
|
|
33
|
-
"SessionStart": [
|
|
34
|
-
{
|
|
35
|
-
"matcher": "*",
|
|
36
|
-
"hooks": [
|
|
37
|
-
{
|
|
38
|
-
"type": "command",
|
|
39
|
-
"command": "npx rhachet roles boot --repo ehmpathy --role mechanic",
|
|
40
|
-
"timeout": 60
|
|
41
|
-
}
|
|
42
|
-
]
|
|
43
|
-
}
|
|
44
|
-
]
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
EOF
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
# Ensure .claude directory exists
|
|
51
|
-
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
52
|
-
|
|
53
|
-
# Initialize settings file if it doesn't exist
|
|
54
|
-
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
55
|
-
echo "{}" > "$SETTINGS_FILE"
|
|
56
|
-
fi
|
|
57
|
-
|
|
58
|
-
# Findsert: merge the hook configuration if not already present
|
|
59
|
-
# Strategy: deep merge with existing hooks, creating structure if needed
|
|
60
|
-
jq --argjson hook "$HOOK_CONFIG" '
|
|
61
|
-
# Define the target command for comparison
|
|
62
|
-
def targetCmd: "npx rhachet roles boot --repo ehmpathy --role mechanic";
|
|
63
|
-
|
|
64
|
-
# Check if hook already exists
|
|
65
|
-
def hookExists:
|
|
66
|
-
(.hooks.SessionStart // [])
|
|
67
|
-
| map(select(.matcher == "*") | .hooks // [])
|
|
68
|
-
| flatten
|
|
69
|
-
| map(.command)
|
|
70
|
-
| index(targetCmd) != null;
|
|
71
|
-
|
|
72
|
-
# If hook already exists, return unchanged
|
|
73
|
-
if hookExists then
|
|
74
|
-
.
|
|
75
|
-
else
|
|
76
|
-
# Ensure .hooks exists
|
|
77
|
-
.hooks //= {} |
|
|
78
|
-
|
|
79
|
-
# Ensure .hooks.SessionStart exists
|
|
80
|
-
.hooks.SessionStart //= [] |
|
|
81
|
-
|
|
82
|
-
# Check if our matcher already exists
|
|
83
|
-
if (.hooks.SessionStart | map(.matcher) | index("*")) then
|
|
84
|
-
# Matcher exists, add our hook to its hooks array
|
|
85
|
-
.hooks.SessionStart |= map(
|
|
86
|
-
if .matcher == "*" then
|
|
87
|
-
.hooks += $hook.hooks.SessionStart[0].hooks
|
|
88
|
-
else
|
|
89
|
-
.
|
|
90
|
-
end
|
|
91
|
-
)
|
|
92
|
-
else
|
|
93
|
-
# Matcher does not exist, add the entire entry
|
|
94
|
-
.hooks.SessionStart += $hook.hooks.SessionStart
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
98
|
-
|
|
99
|
-
# Check if any changes were made
|
|
100
|
-
if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
|
|
101
|
-
rm "$SETTINGS_FILE.tmp"
|
|
102
|
-
echo "👌 mechanic SessionStart hook already bound"
|
|
103
|
-
echo " $SETTINGS_FILE"
|
|
104
|
-
exit 0
|
|
105
|
-
fi
|
|
106
|
-
|
|
107
|
-
# Atomic replace
|
|
108
|
-
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
109
|
-
|
|
110
|
-
echo "🔗 mechanic SessionStart hook bound successfully!"
|
|
111
|
-
echo " $SETTINGS_FILE"
|
|
112
|
-
echo ""
|
|
113
|
-
echo "✨ next time you start a Claude session, the mechanic will boot automatically"
|
|
File without changes
|