rhachet-roles-ehmpathy 1.12.0 → 1.13.0

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.
@@ -112,8 +112,27 @@ match_pattern() {
112
112
  local cmd="$1"
113
113
  local pattern="$2"
114
114
 
115
- # Convert glob * to regex .*
116
- local regex="^${pattern//\*/.*}$"
115
+ # Handle Claude Code's :* suffix matcher
116
+ # :* means "optionally match colon and anything after"
117
+ # e.g., "npm run test:*" matches "npm run test", "npm run test:", "npm run test:unit"
118
+
119
+ # First, escape regex special chars except * and :
120
+ local escaped_pattern
121
+ escaped_pattern=$(printf '%s' "$pattern" | sed 's/[.^$+?{}()[\]|\\]/\\&/g')
122
+
123
+ # Convert :* to placeholder first (to avoid * -> .* conversion interfering)
124
+ # Using a unique placeholder that won't appear in commands
125
+ escaped_pattern="${escaped_pattern//:\*/__COLON_STAR_PLACEHOLDER__}"
126
+
127
+ # Convert remaining * to .* (glob-style wildcard)
128
+ escaped_pattern="${escaped_pattern//\*/.*}"
129
+
130
+ # Now replace placeholder with the actual regex for :*
131
+ # (:.*)? matches: nothing, ":", ":foo", ":foo:bar"
132
+ escaped_pattern="${escaped_pattern//__COLON_STAR_PLACEHOLDER__/(:.*)?}"
133
+
134
+ # Build final regex
135
+ local regex="^${escaped_pattern}$"
117
136
 
118
137
  if [[ "$cmd" =~ $regex ]]; then
119
138
  return 0
@@ -121,6 +140,21 @@ match_pattern() {
121
140
  return 1
122
141
  }
123
142
 
143
+ # Transform raw permission pattern to compact bracket notation for display
144
+ format_pattern() {
145
+ local pattern="$1"
146
+
147
+ # Check if pattern ends with :*
148
+ if [[ "$pattern" == *":*" ]]; then
149
+ # Remove :* suffix and format with [p]: label (prefix match)
150
+ local prefix="${pattern%:*}"
151
+ echo "[p]: $prefix"
152
+ else
153
+ # Exact match - format with [e]: label
154
+ echo "[e]: $pattern"
155
+ fi
156
+ }
157
+
124
158
  for pattern in "${ALLOWED_PATTERNS[@]}"; do
125
159
  if match_pattern "$COMMAND" "$pattern"; then
126
160
  exit 0 # Command matches an allowed pattern
@@ -137,10 +171,14 @@ if [[ "$MODE" == "SOFTNUDGE" ]]; then
137
171
  echo ""
138
172
  echo "Before requesting user approval, check if you can accomplish this task using one of these pre-approved patterns:"
139
173
  echo ""
174
+ echo "([e] = exact match, [p] = prefix match)"
175
+ echo ""
140
176
  for pattern in "${ALLOWED_PATTERNS[@]}"; do
141
- echo " • $pattern"
177
+ echo " • $(format_pattern "$pattern")"
142
178
  done
143
179
  echo ""
180
+ echo "([e] = exact match, [p] = prefix match)"
181
+ echo ""
144
182
  echo "If an existing permission pattern can solve your task, use that instead."
145
183
  echo "If not, proceed with requesting approval."
146
184
  echo ""
@@ -182,10 +220,14 @@ jq --arg cmd "$COMMAND" --argjson ts "$now" '. + {($cmd): $ts}' "$ATTEMPTED_FILE
182
220
  echo ""
183
221
  echo "Before requesting user approval, check if you can accomplish this task using one of these pre-approved patterns:"
184
222
  echo ""
223
+ echo "([e] = exact match, [p] = prefix match)"
224
+ echo ""
185
225
  for pattern in "${ALLOWED_PATTERNS[@]}"; do
186
- echo " • $pattern"
226
+ echo " • $(format_pattern "$pattern")"
187
227
  done
188
228
  echo ""
229
+ echo "([e] = exact match, [p] = prefix match)"
230
+ echo ""
189
231
  echo "If an existing permission pattern can solve your task, use that instead."
190
232
  echo "If you've considered the alternatives and still need this specific command, retry it."
191
233
  echo ""
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env bash
2
+ ######################################################################
3
+ # .what = PreToolUse hook to forbid 2>&1 (stderr redirection)
4
+ #
5
+ # .why = redirecting stderr to stdout (2>&1) hides error messages
6
+ # and makes debugging harder. Claude should see stderr
7
+ # separately to understand when commands fail.
8
+ #
9
+ # .how = reads JSON from stdin, extracts tool_input.command,
10
+ # checks if it contains 2>&1 and blocks if found.
11
+ #
12
+ # usage:
13
+ # configure in .claude/settings.local.json under hooks.PreToolUse
14
+ #
15
+ # guarantee:
16
+ # ✔ blocks commands containing 2>&1
17
+ # ✔ fast: simple string matching
18
+ # ✔ helpful: explains why it's blocked
19
+ ######################################################################
20
+
21
+ set -euo pipefail
22
+
23
+ # Read JSON from stdin (Claude Code passes input via stdin)
24
+ STDIN_INPUT=$(cat)
25
+
26
+ # failfast: if no input received, something is wrong
27
+ if [[ -z "$STDIN_INPUT" ]]; then
28
+ echo "ERROR: PreToolUse hook received no input via stdin" >&2
29
+ exit 2
30
+ fi
31
+
32
+ # Extract command from stdin JSON
33
+ # Claude passes: {"tool_name": "Bash", "tool_input": {"command": "..."}}
34
+ COMMAND=$(echo "$STDIN_INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null || echo "")
35
+
36
+ # Skip if not a Bash command or empty
37
+ if [[ -z "$COMMAND" ]]; then
38
+ exit 0
39
+ fi
40
+
41
+ # Check if command contains 2>&1
42
+ if [[ "$COMMAND" == *"2>&1"* ]]; then
43
+ {
44
+ echo ""
45
+ echo "🛑 BLOCKED: Command contains '2>&1' (stderr redirect to stdout)."
46
+ echo ""
47
+ echo "Redirecting stderr to stdout hides error messages and makes debugging harder."
48
+ echo "Claude should see stderr separately to understand when commands fail."
49
+ echo ""
50
+ echo "Please remove '2>&1' from your command and try again."
51
+ echo ""
52
+ } >&2
53
+ exit 2
54
+ fi
55
+
56
+ # Command is allowed
57
+ exit 0
@@ -0,0 +1,116 @@
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"
@@ -21,4 +21,5 @@ SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
21
 
22
22
  # Dispatch to each hook initializer
23
23
  "$SKILLS_DIR/init.claude.hooks.sessionstart.sh"
24
+ "$SKILLS_DIR/init.claude.hooks.forbid.stderr.redirect.sh"
24
25
  "$SKILLS_DIR/init.claude.hooks.pretooluse.sh"
@@ -34,11 +34,16 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
34
34
  {
35
35
  "permissions": {
36
36
  "deny": [
37
- "Bash(git commit:*)"
37
+ "Bash(git commit:*)",
38
+ "Bash(sed:*)",
39
+ "Bash(tee:*)",
40
+ "Bash(find:*)",
41
+ "Bash(echo:*)"
38
42
  ],
39
43
  "ask": [
40
44
  "Bash(bash:*)",
41
45
  "Bash(chmod:*)",
46
+ "Bash(npm install:*)",
42
47
  "Bash(pnpm install:*)",
43
48
  "Bash(pnpm add:*)"
44
49
  ],
@@ -48,20 +53,39 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
48
53
  "WebFetch(domain:www.npmjs.com)",
49
54
  "WebFetch(domain:hub.docker.com)",
50
55
  "WebFetch(domain:raw.githubusercontent.com)",
56
+ "WebFetch(domain:biomejs.dev)",
51
57
 
52
- "Bash(npm run build:*)",
53
- "Bash(npm run start:testdb:*)",
54
- "Bash(AWS_PROFILE=ahbode.dev npm run deploy:dev:*)",
55
58
 
59
+ "Bash(ls:*)",
60
+ "Bash(tree:*)",
56
61
  "Bash(cat:*)",
57
- "Bash(unzip:*)",
62
+ "Bash(head:*)",
63
+ "Bash(tail:*)",
64
+ "Bash(grep:*)",
65
+ "Bash(wc:*)",
66
+ "Bash(diff:*)",
67
+ "Bash(which:*)",
68
+ "Bash(file:*)",
69
+ "Bash(pwd)",
58
70
  "Bash(npm view:*)",
59
71
  "Bash(npm list:*)",
60
- "Bash(pnpm list:*)",
72
+ "Bash(npm remove:*)",
73
+
74
+ "Bash(npx rhachet roles boot --repo ehmpathy --role mechanic)",
75
+ "Bash(npx tsx ./bin/run:*)",
76
+ "Bash(npx tsc:*)",
77
+ "Bash(npx biome:*)",
78
+ "Bash(npx jest:*)",
79
+
80
+ "Bash(npm run build:*)",
81
+ "Bash(npm run build:compile)",
82
+ "Bash(npm run start:testdb:*)",
61
83
 
62
- "Bash(npx rhachet:*)",
63
84
 
64
85
  "Bash(npm run test:*)",
86
+ "Bash(npm run test:types:*)",
87
+ "Bash(npm run test:format:*)",
88
+ "Bash(npm run test:lint:*)",
65
89
  "Bash(npm run test:unit:*)",
66
90
  "Bash(npm run test:integration:*)",
67
91
  "Bash(npm run test:acceptance:*)",
@@ -71,15 +95,12 @@ PERMISSIONS_CONFIG=$(cat <<'EOF'
71
95
  "Bash(THOROUGH=true npm run test:integration:*)",
72
96
  "Bash(THOROUGH=true npm run test:acceptance:*)",
73
97
 
74
- "Bash(AWS_PROFILE=ahbode.dev npm run test:integration:*)",
75
- "Bash(AWS_PROFILE=ahbode.dev STAGE=dev npm run test:acceptance:*)",
76
-
77
98
  "Bash(npm run fix:*)",
78
99
  "Bash(npm run fix:format:*)",
79
100
  "Bash(npm run fix:lint:*)",
80
- "Bash(npm run fix:types:*)",
81
101
 
82
- "Bash(find:*)",
102
+ "Bash(gh pr checks:*)",
103
+ "Bash(gh pr status:*)",
83
104
 
84
105
  "Bash(source .agent/repo=.this/skills/*)"
85
106
  ]
@@ -12,6 +12,7 @@
12
12
  # .how = runs both init scripts in sequence from the same directory.
13
13
  #
14
14
  # guarantee:
15
+ # ✔ backs up settings.local.json before changes (if exists)
15
16
  # ✔ runs both hooks and permissions initialization
16
17
  # ✔ fail-fast on any error
17
18
  # ✔ idempotent: safe to rerun
@@ -20,10 +21,21 @@
20
21
  set -euo pipefail
21
22
 
22
23
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
+ GITROOT="$(git rev-parse --show-toplevel)"
25
+ SETTINGS_FILE="$GITROOT/.claude/settings.local.json"
23
26
 
24
27
  echo "🔧 init claude config for mechanic role..."
25
28
  echo ""
26
29
 
30
+ # backup existing settings before changes
31
+ if [[ -f "$SETTINGS_FILE" ]]; then
32
+ ISODATETIME="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
33
+ BACKUP_FILE="$GITROOT/.claude/settings.$ISODATETIME.bak.local.json"
34
+ cp "$SETTINGS_FILE" "$BACKUP_FILE"
35
+ echo "📦 backed up settings to: ${BACKUP_FILE#$GITROOT/}"
36
+ echo ""
37
+ fi
38
+
27
39
  # initialize hooks
28
40
  "$SCRIPT_DIR/init.claude.hooks.sh"
29
41
  echo ""
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.12.0",
5
+ "version": "1.13.0",
6
6
  "repository": "ehmpathy/rhachet-roles-ehmpathy",
7
7
  "homepage": "https://github.com/ehmpathy/rhachet-roles-ehmpathy",
8
8
  "keywords": [