rhachet-roles-ehmpathy 1.15.0 → 1.15.2
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.tools/sedreplace.sh +135 -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 +44 -7
- package/dist/logic/roles/mechanic/.skills/init.claude.permissions.jsonc +3 -0
- package/package.json +1 -1
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.pretooluse.check-permissions.sh +0 -118
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.pretooluse.forbid-stderr-redirect.sh +0 -116
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sessionstart.notify-permissions.sh +0 -121
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sessionstart.sh +0 -113
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
######################################################################
|
|
3
|
+
# .what = safe find-and-replace across git-tracked files only
|
|
4
|
+
#
|
|
5
|
+
# .why = enables bulk text replacement without:
|
|
6
|
+
# - touching files outside the repo
|
|
7
|
+
# - modifying untracked files
|
|
8
|
+
# - accidental command chaining attacks
|
|
9
|
+
#
|
|
10
|
+
# this is a controlled alternative to raw sed, which is
|
|
11
|
+
# denied in permissions due to security risks.
|
|
12
|
+
#
|
|
13
|
+
# usage:
|
|
14
|
+
# sedreplace.sh --old "pattern" --new "replacement" # dry-run
|
|
15
|
+
# sedreplace.sh --old "pattern" --new "replacement" --execute # apply
|
|
16
|
+
# sedreplace.sh --old "pattern" --new "replacement" --glob "*.ts" # filter
|
|
17
|
+
#
|
|
18
|
+
# guarantee:
|
|
19
|
+
# - only operates on git-tracked files (git ls-files)
|
|
20
|
+
# - dry-run by default (shows diff, no changes)
|
|
21
|
+
# - requires --execute to apply changes
|
|
22
|
+
# - fail-fast on errors
|
|
23
|
+
######################################################################
|
|
24
|
+
set -euo pipefail
|
|
25
|
+
|
|
26
|
+
# parse named arguments
|
|
27
|
+
OLD_PATTERN=""
|
|
28
|
+
NEW_PATTERN=""
|
|
29
|
+
GLOB_FILTER=""
|
|
30
|
+
EXECUTE=false
|
|
31
|
+
|
|
32
|
+
while [[ $# -gt 0 ]]; do
|
|
33
|
+
case $1 in
|
|
34
|
+
--old)
|
|
35
|
+
OLD_PATTERN="$2"
|
|
36
|
+
shift 2
|
|
37
|
+
;;
|
|
38
|
+
--new)
|
|
39
|
+
NEW_PATTERN="$2"
|
|
40
|
+
shift 2
|
|
41
|
+
;;
|
|
42
|
+
--glob)
|
|
43
|
+
GLOB_FILTER="$2"
|
|
44
|
+
shift 2
|
|
45
|
+
;;
|
|
46
|
+
--execute)
|
|
47
|
+
EXECUTE=true
|
|
48
|
+
shift
|
|
49
|
+
;;
|
|
50
|
+
*)
|
|
51
|
+
echo "unknown argument: $1"
|
|
52
|
+
echo "usage: sedreplace.sh --old 'pattern' --new 'replacement' [--glob '*.ts'] [--execute]"
|
|
53
|
+
exit 1
|
|
54
|
+
;;
|
|
55
|
+
esac
|
|
56
|
+
done
|
|
57
|
+
|
|
58
|
+
# validate required args
|
|
59
|
+
if [[ -z "$OLD_PATTERN" ]]; then
|
|
60
|
+
echo "error: --old pattern is required"
|
|
61
|
+
exit 1
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
if [[ -z "$NEW_PATTERN" ]]; then
|
|
65
|
+
echo "error: --new replacement is required"
|
|
66
|
+
exit 1
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# ensure we're in a git repo
|
|
70
|
+
if ! git rev-parse --git-dir > /dev/null 2>&1; then
|
|
71
|
+
echo "error: not in a git repository"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
# get git-tracked files, optionally filtered by glob
|
|
76
|
+
if [[ -n "$GLOB_FILTER" ]]; then
|
|
77
|
+
FILES=$(git ls-files "$GLOB_FILTER")
|
|
78
|
+
else
|
|
79
|
+
FILES=$(git ls-files)
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
if [[ -z "$FILES" ]]; then
|
|
83
|
+
echo "no files match the criteria"
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# find files containing the pattern
|
|
88
|
+
MATCHING_FILES=$(echo "$FILES" | xargs grep -l "$OLD_PATTERN" 2>/dev/null || true)
|
|
89
|
+
|
|
90
|
+
if [[ -z "$MATCHING_FILES" ]]; then
|
|
91
|
+
echo "no files contain pattern: $OLD_PATTERN"
|
|
92
|
+
exit 0
|
|
93
|
+
fi
|
|
94
|
+
|
|
95
|
+
# count matches
|
|
96
|
+
MATCH_COUNT=$(echo "$MATCHING_FILES" | wc -l)
|
|
97
|
+
echo "found $MATCH_COUNT file(s) containing pattern"
|
|
98
|
+
echo ""
|
|
99
|
+
|
|
100
|
+
if [[ "$EXECUTE" == "false" ]]; then
|
|
101
|
+
# dry-run: show what would change
|
|
102
|
+
echo "=== DRY RUN (use --execute to apply) ==="
|
|
103
|
+
echo ""
|
|
104
|
+
|
|
105
|
+
for file in $MATCHING_FILES; do
|
|
106
|
+
echo "--- $file ---"
|
|
107
|
+
# show the diff that would result
|
|
108
|
+
sed "s|$OLD_PATTERN|$NEW_PATTERN|g" "$file" | diff -u "$file" - || true
|
|
109
|
+
echo ""
|
|
110
|
+
done
|
|
111
|
+
|
|
112
|
+
echo "=== END DRY RUN ==="
|
|
113
|
+
echo ""
|
|
114
|
+
echo "to apply changes, run with --execute flag"
|
|
115
|
+
else
|
|
116
|
+
# execute: apply changes
|
|
117
|
+
echo "=== APPLYING CHANGES ==="
|
|
118
|
+
echo ""
|
|
119
|
+
|
|
120
|
+
for file in $MATCHING_FILES; do
|
|
121
|
+
echo "updating: $file"
|
|
122
|
+
# use sed -i for in-place editing
|
|
123
|
+
# note: macOS sed requires -i '' but linux sed uses -i
|
|
124
|
+
if [[ "$(uname)" == "Darwin" ]]; then
|
|
125
|
+
sed -i '' "s|$OLD_PATTERN|$NEW_PATTERN|g" "$file"
|
|
126
|
+
else
|
|
127
|
+
sed -i "s|$OLD_PATTERN|$NEW_PATTERN|g" "$file"
|
|
128
|
+
fi
|
|
129
|
+
done
|
|
130
|
+
|
|
131
|
+
echo ""
|
|
132
|
+
echo "=== DONE: updated $MATCH_COUNT file(s) ==="
|
|
133
|
+
echo ""
|
|
134
|
+
echo "to undo: git checkout ."
|
|
135
|
+
fi
|
|
@@ -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!"
|
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
# .why = the mechanic role uses multiple hooks:
|
|
6
6
|
# • SessionStart: boot mechanic on every session
|
|
7
7
|
# • SessionStart: notify Claude of allowed permissions upfront
|
|
8
|
+
# • PreToolUse: forbid stderr redirects (2>&1)
|
|
8
9
|
# • PreToolUse: check existing permissions before new requests
|
|
9
10
|
#
|
|
10
|
-
# this script
|
|
11
|
+
# this script manages hook registration via findsert utility.
|
|
11
12
|
#
|
|
12
|
-
# .how = runs
|
|
13
|
+
# .how = 1. runs cleanup to remove stale hooks (deleted scripts)
|
|
14
|
+
# 2. calls findsert for each hook in desired order
|
|
13
15
|
#
|
|
14
16
|
# guarantee:
|
|
15
17
|
# ✔ idempotent: safe to rerun
|
|
@@ -19,9 +21,44 @@
|
|
|
19
21
|
set -euo pipefail
|
|
20
22
|
|
|
21
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"
|
|
22
26
|
|
|
23
|
-
#
|
|
24
|
-
"$SKILLS_DIR/
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"$
|
|
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
|
|
@@ -139,6 +139,9 @@
|
|
|
139
139
|
"Bash(git mv:*)",
|
|
140
140
|
"Bash(git rm:*)",
|
|
141
141
|
|
|
142
|
+
// sedreplace - safe bulk find-and-replace on git-tracked files only
|
|
143
|
+
"Bash(bash .agent/repo=ehmpathy/role=mechanic/skills/.skills/claude.tools/sedreplace.sh:*)",
|
|
144
|
+
|
|
142
145
|
// npm read operations
|
|
143
146
|
"Bash(npm view:*)",
|
|
144
147
|
"Bash(npm list:*)",
|
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.15.
|
|
5
|
+
"version": "1.15.2",
|
|
6
6
|
"repository": "ehmpathy/rhachet-roles-ehmpathy",
|
|
7
7
|
"homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
|
|
8
8
|
"keywords": [
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = bind pretooluse.check-permissions 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/pretooluse.check-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 "👌 pretooluse.check-permissions 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 "🔗 pretooluse.check-permissions hook bound successfully!"
|
|
116
|
-
echo " $SETTINGS_FILE"
|
|
117
|
-
echo ""
|
|
118
|
-
echo "✨ Claude will now be reminded to check existing permissions before requesting new ones"
|
package/dist/logic/roles/mechanic/.skills/init.claude.hooks.pretooluse.forbid-stderr-redirect.sh
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = bind pretooluse.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/pretooluse.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 "👌 pretooluse.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 "🔗 pretooluse.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"
|
package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sessionstart.notify-permissions.sh
DELETED
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bash
|
|
2
|
-
######################################################################
|
|
3
|
-
# .what = bind sessionstart.notify-permissions hook to Claude settings
|
|
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 script "findserts" (find-or-insert) the SessionStart
|
|
10
|
-
# hook into .claude/settings.local.json, ensuring:
|
|
11
|
-
# - the hook is present after running this skill
|
|
12
|
-
# - no duplication if already present
|
|
13
|
-
# - idempotent: safe to rerun
|
|
14
|
-
#
|
|
15
|
-
# .how = uses jq to merge the SessionStart hook configuration
|
|
16
|
-
# into the existing hooks structure, creating it if absent.
|
|
17
|
-
#
|
|
18
|
-
# guarantee:
|
|
19
|
-
# ✔ creates .claude/settings.local.json if missing
|
|
20
|
-
# ✔ preserves existing settings (permissions, other hooks)
|
|
21
|
-
# ✔ idempotent: no-op if hook already present
|
|
22
|
-
# ✔ fail-fast on errors
|
|
23
|
-
######################################################################
|
|
24
|
-
|
|
25
|
-
set -euo pipefail
|
|
26
|
-
|
|
27
|
-
PROJECT_ROOT="$PWD"
|
|
28
|
-
SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
|
|
29
|
-
SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
30
|
-
HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/sessionstart.notify-permissions.sh"
|
|
31
|
-
|
|
32
|
-
# Verify hook script exists
|
|
33
|
-
if [[ ! -f "$HOOK_SCRIPT" ]]; then
|
|
34
|
-
echo "❌ hook script not found: $HOOK_SCRIPT" >&2
|
|
35
|
-
exit 1
|
|
36
|
-
fi
|
|
37
|
-
|
|
38
|
-
# Define the hook configuration to findsert
|
|
39
|
-
HOOK_CONFIG=$(cat <<EOF
|
|
40
|
-
{
|
|
41
|
-
"hooks": {
|
|
42
|
-
"SessionStart": [
|
|
43
|
-
{
|
|
44
|
-
"matcher": "*",
|
|
45
|
-
"hooks": [
|
|
46
|
-
{
|
|
47
|
-
"type": "command",
|
|
48
|
-
"command": "$HOOK_SCRIPT",
|
|
49
|
-
"timeout": 5
|
|
50
|
-
}
|
|
51
|
-
]
|
|
52
|
-
}
|
|
53
|
-
]
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
EOF
|
|
57
|
-
)
|
|
58
|
-
|
|
59
|
-
# Ensure .claude directory exists
|
|
60
|
-
mkdir -p "$(dirname "$SETTINGS_FILE")"
|
|
61
|
-
|
|
62
|
-
# Initialize settings file if it doesn't exist
|
|
63
|
-
if [[ ! -f "$SETTINGS_FILE" ]]; then
|
|
64
|
-
echo "{}" > "$SETTINGS_FILE"
|
|
65
|
-
fi
|
|
66
|
-
|
|
67
|
-
# Findsert: merge the hook configuration if not already present
|
|
68
|
-
jq --argjson hook "$HOOK_CONFIG" '
|
|
69
|
-
# Define the target command for comparison
|
|
70
|
-
def targetCmd: $hook.hooks.SessionStart[0].hooks[0].command;
|
|
71
|
-
|
|
72
|
-
# Check if hook already exists
|
|
73
|
-
def hookExists:
|
|
74
|
-
(.hooks.SessionStart // [])
|
|
75
|
-
| map(select(.matcher == "*") | .hooks // [])
|
|
76
|
-
| flatten
|
|
77
|
-
| map(.command)
|
|
78
|
-
| any(. == targetCmd);
|
|
79
|
-
|
|
80
|
-
# If hook already exists, return unchanged
|
|
81
|
-
if hookExists then
|
|
82
|
-
.
|
|
83
|
-
else
|
|
84
|
-
# Ensure .hooks exists
|
|
85
|
-
.hooks //= {} |
|
|
86
|
-
|
|
87
|
-
# Ensure .hooks.SessionStart exists
|
|
88
|
-
.hooks.SessionStart //= [] |
|
|
89
|
-
|
|
90
|
-
# Check if our matcher already exists
|
|
91
|
-
if (.hooks.SessionStart | map(.matcher) | index("*")) then
|
|
92
|
-
# Matcher exists, add our hook to its hooks array
|
|
93
|
-
.hooks.SessionStart |= map(
|
|
94
|
-
if .matcher == "*" then
|
|
95
|
-
.hooks += $hook.hooks.SessionStart[0].hooks
|
|
96
|
-
else
|
|
97
|
-
.
|
|
98
|
-
end
|
|
99
|
-
)
|
|
100
|
-
else
|
|
101
|
-
# Matcher does not exist, add the entire entry
|
|
102
|
-
.hooks.SessionStart += $hook.hooks.SessionStart
|
|
103
|
-
end
|
|
104
|
-
end
|
|
105
|
-
' "$SETTINGS_FILE" > "$SETTINGS_FILE.tmp"
|
|
106
|
-
|
|
107
|
-
# Check if any changes were made
|
|
108
|
-
if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
|
|
109
|
-
rm "$SETTINGS_FILE.tmp"
|
|
110
|
-
echo "👌 sessionstart.notify-permissions hook already bound"
|
|
111
|
-
echo " $SETTINGS_FILE"
|
|
112
|
-
exit 0
|
|
113
|
-
fi
|
|
114
|
-
|
|
115
|
-
# Atomic replace
|
|
116
|
-
mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
|
|
117
|
-
|
|
118
|
-
echo "🔗 sessionstart.notify-permissions hook bound successfully!"
|
|
119
|
-
echo " $SETTINGS_FILE"
|
|
120
|
-
echo ""
|
|
121
|
-
echo "✨ Claude will now see allowed permissions at the start of each session"
|
|
@@ -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"
|