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.
@@ -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 dispatches to each hook initializer.
11
+ # this script manages hook registration via findsert utility.
10
12
  #
11
- # .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
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
- # Dispatch to each hook initializer
23
- "$SKILLS_DIR/init.claude.hooks.sessionstart.sh"
24
- "$SKILLS_DIR/init.claude.hooks.forbid.stderr.redirect.sh"
25
- "$SKILLS_DIR/init.claude.hooks.pretooluse.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
@@ -135,8 +135,9 @@
135
135
  "Bash(mkdir:*)",
136
136
  "Bash(pwd)",
137
137
 
138
- // safe custom tools
139
- "Bash(bash src/logic/roles/mechanic/.skills/claude.tools/mvsafe.sh:*)",
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.14.0",
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"