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.
- package/dist/logic/roles/mechanic/.skills/claude.hooks/check.pretooluse.permissions.sh +46 -4
- package/dist/logic/roles/mechanic/.skills/claude.hooks/forbid.stderr.redirect.sh +57 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.forbid.stderr.redirect.sh +116 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.hooks.sh +1 -0
- package/dist/logic/roles/mechanic/.skills/init.claude.permissions.sh +33 -12
- package/dist/logic/roles/mechanic/.skills/init.claude.sh +12 -0
- package/package.json +1 -1
|
@@ -112,8 +112,27 @@ match_pattern() {
|
|
|
112
112
|
local cmd="$1"
|
|
113
113
|
local pattern="$2"
|
|
114
114
|
|
|
115
|
-
#
|
|
116
|
-
|
|
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"
|
|
@@ -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(
|
|
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(
|
|
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(
|
|
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.
|
|
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": [
|