rhachet-roles-ehmpathy 1.12.1 → 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/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 +32 -11
- package/dist/logic/roles/mechanic/.skills/init.claude.sh +12 -0
- package/package.json +1 -1
|
@@ -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:*)",
|
|
61
73
|
|
|
62
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:*)",
|
|
83
|
+
|
|
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": [
|