rhachet-roles-ehmpathy 1.13.13 → 1.15.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.
@@ -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
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  ######################################################################
3
- # .what = bind mechanic PreToolUse hook to Claude settings
3
+ # .what = bind pretooluse.check-permissions hook to Claude settings
4
4
  #
5
5
  # .why = when Claude attempts a Bash command not covered by existing
6
6
  # permissions, this hook provides feedback asking it to
@@ -24,7 +24,7 @@ set -euo pipefail
24
24
  PROJECT_ROOT="$PWD"
25
25
  SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
26
26
  SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
- HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/check.pretooluse.permissions.sh"
27
+ HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/pretooluse.check-permissions.sh"
28
28
 
29
29
  # Verify hook script exists
30
30
  if [[ ! -f "$HOOK_SCRIPT" ]]; then
@@ -104,7 +104,7 @@ jq --argjson hook "$HOOK_CONFIG" '
104
104
  # Check if any changes were made
105
105
  if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
106
106
  rm "$SETTINGS_FILE.tmp"
107
- echo "👌 mechanic PreToolUse hook already bound"
107
+ echo "👌 pretooluse.check-permissions hook already bound"
108
108
  echo " $SETTINGS_FILE"
109
109
  exit 0
110
110
  fi
@@ -112,7 +112,7 @@ fi
112
112
  # Atomic replace
113
113
  mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
114
114
 
115
- echo "🔗 mechanic PreToolUse hook bound successfully!"
115
+ echo "🔗 pretooluse.check-permissions hook bound successfully!"
116
116
  echo " $SETTINGS_FILE"
117
117
  echo ""
118
118
  echo "✨ Claude will now be reminded to check existing permissions before requesting new ones"
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env bash
2
2
  ######################################################################
3
- # .what = bind forbid.stderr.redirect hook to Claude settings
3
+ # .what = bind pretooluse.forbid-stderr-redirect hook to Claude settings
4
4
  #
5
5
  # .why = when Claude uses 2>&1, error messages are hidden and
6
6
  # debugging becomes harder. this hook blocks such commands.
@@ -21,7 +21,7 @@ set -euo pipefail
21
21
  PROJECT_ROOT="$PWD"
22
22
  SETTINGS_FILE="$PROJECT_ROOT/.claude/settings.local.json"
23
23
  SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
24
- HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/forbid.stderr.redirect.sh"
24
+ HOOK_SCRIPT="$SKILLS_DIR/claude.hooks/pretooluse.forbid-stderr-redirect.sh"
25
25
 
26
26
  # Verify hook script exists
27
27
  if [[ ! -f "$HOOK_SCRIPT" ]]; then
@@ -102,7 +102,7 @@ jq --argjson hook "$HOOK_CONFIG" '
102
102
  # Check if any changes were made
103
103
  if diff -q "$SETTINGS_FILE" "$SETTINGS_FILE.tmp" >/dev/null 2>&1; then
104
104
  rm "$SETTINGS_FILE.tmp"
105
- echo "👌 forbid.stderr.redirect hook already bound"
105
+ echo "👌 pretooluse.forbid-stderr-redirect hook already bound"
106
106
  echo " $SETTINGS_FILE"
107
107
  exit 0
108
108
  fi
@@ -110,7 +110,7 @@ fi
110
110
  # Atomic replace
111
111
  mv "$SETTINGS_FILE.tmp" "$SETTINGS_FILE"
112
112
 
113
- echo "🔗 forbid.stderr.redirect hook bound successfully!"
113
+ echo "🔗 pretooluse.forbid-stderr-redirect hook bound successfully!"
114
114
  echo " $SETTINGS_FILE"
115
115
  echo ""
116
116
  echo "✨ Claude will now be blocked from using 2>&1 in commands"
@@ -0,0 +1,121 @@
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"
@@ -4,6 +4,7 @@
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
7
8
  # • PreToolUse: check existing permissions before new requests
8
9
  #
9
10
  # this script dispatches to each hook initializer.
@@ -21,5 +22,6 @@ SKILLS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
22
 
22
23
  # Dispatch to each hook initializer
23
24
  "$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"
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"
@@ -41,7 +41,59 @@
41
41
  // test runners - should use npm run test:* scripts instead
42
42
  // direct invocation bypasses project test configuration
43
43
  "Bash(npx biome:*)",
44
- "Bash(npx jest:*)"
44
+ "Bash(npx jest:*)",
45
+
46
+ // github cli write operations - require explicit user approval
47
+ // pr mutations
48
+ "Bash(gh pr create:*)",
49
+ "Bash(gh pr merge:*)",
50
+ "Bash(gh pr close:*)",
51
+ "Bash(gh pr edit:*)",
52
+ "Bash(gh pr review:*)",
53
+ "Bash(gh pr comment:*)",
54
+ "Bash(gh pr reopen:*)",
55
+ // issue mutations
56
+ "Bash(gh issue create:*)",
57
+ "Bash(gh issue close:*)",
58
+ "Bash(gh issue edit:*)",
59
+ "Bash(gh issue comment:*)",
60
+ "Bash(gh issue reopen:*)",
61
+ // repo mutations
62
+ "Bash(gh repo create:*)",
63
+ "Bash(gh repo delete:*)",
64
+ "Bash(gh repo fork:*)",
65
+ "Bash(gh repo edit:*)",
66
+ // release mutations
67
+ "Bash(gh release create:*)",
68
+ "Bash(gh release delete:*)",
69
+ "Bash(gh release edit:*)",
70
+ // workflow/run mutations
71
+ "Bash(gh run cancel:*)",
72
+ "Bash(gh run rerun:*)",
73
+ "Bash(gh workflow disable:*)",
74
+ "Bash(gh workflow enable:*)",
75
+ "Bash(gh workflow run:*)",
76
+ // gist mutations
77
+ "Bash(gh gist create:*)",
78
+ "Bash(gh gist delete:*)",
79
+ "Bash(gh gist edit:*)",
80
+ // label mutations
81
+ "Bash(gh label create:*)",
82
+ "Bash(gh label delete:*)",
83
+ "Bash(gh label edit:*)",
84
+ // project mutations
85
+ "Bash(gh project create:*)",
86
+ "Bash(gh project delete:*)",
87
+ "Bash(gh project edit:*)",
88
+ // api write methods - can mutate anything
89
+ "Bash(gh api -X POST:*)",
90
+ "Bash(gh api -X PUT:*)",
91
+ "Bash(gh api -X PATCH:*)",
92
+ "Bash(gh api -X DELETE:*)",
93
+ "Bash(gh api --method POST:*)",
94
+ "Bash(gh api --method PUT:*)",
95
+ "Bash(gh api --method PATCH:*)",
96
+ "Bash(gh api --method DELETE:*)"
45
97
  ],
46
98
 
47
99
  // commands that require explicit user approval each time
@@ -83,8 +135,9 @@
83
135
  "Bash(mkdir:*)",
84
136
  "Bash(pwd)",
85
137
 
86
- // safe custom tools
87
- "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:*)",
88
141
 
89
142
  // npm read operations
90
143
  "Bash(npm view:*)",
@@ -126,8 +179,37 @@
126
179
  "Bash(npm run fix:lint:*)",
127
180
 
128
181
  // github cli read operations
129
- "Bash(gh pr checks:*)",
182
+ // pr reads
183
+ "Bash(gh pr list:*)",
184
+ "Bash(gh pr view:*)",
130
185
  "Bash(gh pr status:*)",
186
+ "Bash(gh pr checks:*)",
187
+ "Bash(gh pr diff:*)",
188
+ // issue reads
189
+ "Bash(gh issue list:*)",
190
+ "Bash(gh issue view:*)",
191
+ "Bash(gh issue status:*)",
192
+ // repo reads
193
+ "Bash(gh repo list:*)",
194
+ "Bash(gh repo view:*)",
195
+ // run/workflow reads
196
+ "Bash(gh run list:*)",
197
+ "Bash(gh run view:*)",
198
+ "Bash(gh run watch:*)",
199
+ "Bash(gh workflow list:*)",
200
+ "Bash(gh workflow view:*)",
201
+ // release reads
202
+ "Bash(gh release list:*)",
203
+ "Bash(gh release view:*)",
204
+ // search (all read-only)
205
+ "Bash(gh search code:*)",
206
+ "Bash(gh search repos:*)",
207
+ "Bash(gh search issues:*)",
208
+ "Bash(gh search prs:*)",
209
+ "Bash(gh search commits:*)",
210
+ // api read operations (explicit GET for safe suffix matching)
211
+ "Bash(gh api -X GET:*)",
212
+ "Bash(gh api --method GET:*)",
131
213
 
132
214
  // skill sourcing
133
215
  "Bash(source .agent/repo=.this/skills/*)"
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.13.13",
5
+ "version": "1.15.0",
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 "$@"