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.
@@ -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 dispatches to each hook initializer.
11
+ # this script manages hook registration via findsert utility.
11
12
  #
12
- # .how = runs each init.claude.hooks.*.sh script in sequence
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
- # Dispatch to each hook initializer
24
- "$SKILLS_DIR/init.claude.hooks.sessionstart.sh"
25
- "$SKILLS_DIR/init.claude.hooks.sessionstart.notify-permissions.sh"
26
- "$SKILLS_DIR/init.claude.hooks.pretooluse.forbid-stderr-redirect.sh"
27
- "$SKILLS_DIR/init.claude.hooks.pretooluse.check-permissions.sh"
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.0",
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"
@@ -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"
@@ -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"