@codyswann/lisa 2.222.1 → 2.222.2
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/package.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/hooks/parity-safety-net.sh +58 -7
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-agy/plugin.json +1 -1
- package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-copilot/hooks/parity-safety-net.sh +58 -7
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +58 -7
- package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-agy/plugin.json +1 -1
- package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-agy/plugin.json +1 -1
- package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-agy/plugin.json +1 -1
- package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-agy/plugin.json +1 -1
- package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-agy/plugin.json +1 -1
- package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-agy/plugin.json +1 -1
- package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-agy/plugin.json +1 -1
- package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/src/base/hooks/parity-safety-net.sh +58 -7
package/package.json
CHANGED
|
@@ -102,7 +102,7 @@
|
|
|
102
102
|
"form-data": ">=4.0.6"
|
|
103
103
|
},
|
|
104
104
|
"name": "@codyswann/lisa",
|
|
105
|
-
"version": "2.222.
|
|
105
|
+
"version": "2.222.2",
|
|
106
106
|
"description": "Claude Code governance framework that applies guardrails, guidance, and automated enforcement to projects",
|
|
107
107
|
"main": "dist/index.js",
|
|
108
108
|
"exports": {
|
|
@@ -32,6 +32,57 @@ if [ -z "$command_str" ]; then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
command_for_guards="$command_str"
|
|
36
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
37
|
+
command_for_guards="$(SAFETY_NET_COMMAND="$command_str" python3 - <<'PY'
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
command = os.environ.get("SAFETY_NET_COMMAND", "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def strip_heredocs(text: str) -> str:
|
|
45
|
+
lines = text.splitlines()
|
|
46
|
+
output = []
|
|
47
|
+
pending = []
|
|
48
|
+
# Quoted strings are matched (and thereby skipped over) as plain,
|
|
49
|
+
# non-capturing alternatives BEFORE the heredoc-marker alternative gets a
|
|
50
|
+
# chance to run, so a "<<MARKER"-looking sequence inside a string literal
|
|
51
|
+
# (e.g. `echo "hello <<MARKER"`) is never mistaken for a real heredoc
|
|
52
|
+
# start. Only the heredoc-marker alternative captures a group.
|
|
53
|
+
marker_pattern = re.compile(
|
|
54
|
+
r'"(?:\\.|[^"])*"|\'[^\']*\''
|
|
55
|
+
r"|<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
|
|
56
|
+
)
|
|
57
|
+
index = 0
|
|
58
|
+
while index < len(lines):
|
|
59
|
+
line = lines[index]
|
|
60
|
+
output.append(line)
|
|
61
|
+
pending.extend(
|
|
62
|
+
next(group for group in match.groups() if group)
|
|
63
|
+
for match in marker_pattern.finditer(line)
|
|
64
|
+
if any(match.groups())
|
|
65
|
+
)
|
|
66
|
+
index += 1
|
|
67
|
+
# Consume every pending heredoc body in order (chained same-line
|
|
68
|
+
# heredocs, e.g. `cat <<A <<B`, push more than one marker at once).
|
|
69
|
+
# Do NOT stop after the first terminator — dropping the `break` lets
|
|
70
|
+
# this loop keep dropping body lines until each pending marker is
|
|
71
|
+
# matched, instead of leaking the second body back into `output`
|
|
72
|
+
# where the destructive-pattern guards would see it.
|
|
73
|
+
while pending and index < len(lines):
|
|
74
|
+
if lines[index].strip() == pending[0]:
|
|
75
|
+
output.append(lines[index])
|
|
76
|
+
pending.pop(0)
|
|
77
|
+
index += 1
|
|
78
|
+
return "\n".join(output)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
print(strip_heredocs(command), end="")
|
|
82
|
+
PY
|
|
83
|
+
)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
35
86
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
36
87
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
37
88
|
block() {
|
|
@@ -49,11 +100,11 @@ EOF
|
|
|
49
100
|
# wildcard. Two gates ANDed: the command must invoke `rm` with BOTH a
|
|
50
101
|
# recursive and a force flag, AND name a catastrophic target. Splitting the
|
|
51
102
|
# flag check from the target check keeps each regex legible and testable.
|
|
52
|
-
if printf '%s' "$
|
|
103
|
+
if printf '%s' "$command_for_guards" \
|
|
53
104
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)' \
|
|
54
|
-
|| printf '%s' "$
|
|
105
|
+
|| printf '%s' "$command_for_guards" \
|
|
55
106
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm[[:space:]].*(-r\b.*[[:space:]]-f\b|-f\b.*[[:space:]]-r\b|--recursive\b.*--force\b|--force\b.*--recursive\b)'; then
|
|
56
|
-
if printf '%s' "$
|
|
107
|
+
if printf '%s' "$command_for_guards" \
|
|
57
108
|
| grep -Eq '([[:space:]]|=)(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]]|/?\*?$)'; then
|
|
58
109
|
block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
|
|
59
110
|
fi
|
|
@@ -76,7 +127,7 @@ fi
|
|
|
76
127
|
# \<newline>main" splits into a segment matching --force but not `main`, letting a
|
|
77
128
|
# protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
|
|
78
129
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
79
|
-
normalized_command_str="$(printf '%s' "$
|
|
130
|
+
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
80
131
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
81
132
|
|
|
82
133
|
while IFS= read -r push_stmt; do
|
|
@@ -93,7 +144,7 @@ done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
|
|
|
93
144
|
# 3. `git reset --hard` while the working tree has uncommitted changes — this
|
|
94
145
|
# silently discards them. Only blocks when the tree is actually dirty, so a
|
|
95
146
|
# clean-tree reset (a legitimate workflow) still passes.
|
|
96
|
-
if printf '%s' "$
|
|
147
|
+
if printf '%s' "$command_for_guards" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
|
|
97
148
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
98
149
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
99
150
|
block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
|
|
@@ -101,7 +152,7 @@ if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+rese
|
|
|
101
152
|
fi
|
|
102
153
|
|
|
103
154
|
# 4. Dropping or truncating a database / schema / table.
|
|
104
|
-
if printf '%s' "$
|
|
155
|
+
if printf '%s' "$command_for_guards" \
|
|
105
156
|
| grep -Eiq '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
|
|
106
157
|
block "destructive SQL (DROP/TRUNCATE) detected"
|
|
107
158
|
fi
|
|
@@ -113,7 +164,7 @@ if [ -f "$rules_file" ]; then
|
|
|
113
164
|
case "$rule" in
|
|
114
165
|
'' | '#'*) continue ;;
|
|
115
166
|
esac
|
|
116
|
-
if printf '%s' "$
|
|
167
|
+
if printf '%s' "$command_for_guards" | grep -Eiq -- "$rule"; then
|
|
117
168
|
block "matched a project custom safety rule (${rules_file##*/}): $rule"
|
|
118
169
|
fi
|
|
119
170
|
done <"$rules_file"
|
|
@@ -32,6 +32,57 @@ if [ -z "$command_str" ]; then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
command_for_guards="$command_str"
|
|
36
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
37
|
+
command_for_guards="$(SAFETY_NET_COMMAND="$command_str" python3 - <<'PY'
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
command = os.environ.get("SAFETY_NET_COMMAND", "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def strip_heredocs(text: str) -> str:
|
|
45
|
+
lines = text.splitlines()
|
|
46
|
+
output = []
|
|
47
|
+
pending = []
|
|
48
|
+
# Quoted strings are matched (and thereby skipped over) as plain,
|
|
49
|
+
# non-capturing alternatives BEFORE the heredoc-marker alternative gets a
|
|
50
|
+
# chance to run, so a "<<MARKER"-looking sequence inside a string literal
|
|
51
|
+
# (e.g. `echo "hello <<MARKER"`) is never mistaken for a real heredoc
|
|
52
|
+
# start. Only the heredoc-marker alternative captures a group.
|
|
53
|
+
marker_pattern = re.compile(
|
|
54
|
+
r'"(?:\\.|[^"])*"|\'[^\']*\''
|
|
55
|
+
r"|<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
|
|
56
|
+
)
|
|
57
|
+
index = 0
|
|
58
|
+
while index < len(lines):
|
|
59
|
+
line = lines[index]
|
|
60
|
+
output.append(line)
|
|
61
|
+
pending.extend(
|
|
62
|
+
next(group for group in match.groups() if group)
|
|
63
|
+
for match in marker_pattern.finditer(line)
|
|
64
|
+
if any(match.groups())
|
|
65
|
+
)
|
|
66
|
+
index += 1
|
|
67
|
+
# Consume every pending heredoc body in order (chained same-line
|
|
68
|
+
# heredocs, e.g. `cat <<A <<B`, push more than one marker at once).
|
|
69
|
+
# Do NOT stop after the first terminator — dropping the `break` lets
|
|
70
|
+
# this loop keep dropping body lines until each pending marker is
|
|
71
|
+
# matched, instead of leaking the second body back into `output`
|
|
72
|
+
# where the destructive-pattern guards would see it.
|
|
73
|
+
while pending and index < len(lines):
|
|
74
|
+
if lines[index].strip() == pending[0]:
|
|
75
|
+
output.append(lines[index])
|
|
76
|
+
pending.pop(0)
|
|
77
|
+
index += 1
|
|
78
|
+
return "\n".join(output)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
print(strip_heredocs(command), end="")
|
|
82
|
+
PY
|
|
83
|
+
)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
35
86
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
36
87
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
37
88
|
block() {
|
|
@@ -49,11 +100,11 @@ EOF
|
|
|
49
100
|
# wildcard. Two gates ANDed: the command must invoke `rm` with BOTH a
|
|
50
101
|
# recursive and a force flag, AND name a catastrophic target. Splitting the
|
|
51
102
|
# flag check from the target check keeps each regex legible and testable.
|
|
52
|
-
if printf '%s' "$
|
|
103
|
+
if printf '%s' "$command_for_guards" \
|
|
53
104
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)' \
|
|
54
|
-
|| printf '%s' "$
|
|
105
|
+
|| printf '%s' "$command_for_guards" \
|
|
55
106
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm[[:space:]].*(-r\b.*[[:space:]]-f\b|-f\b.*[[:space:]]-r\b|--recursive\b.*--force\b|--force\b.*--recursive\b)'; then
|
|
56
|
-
if printf '%s' "$
|
|
107
|
+
if printf '%s' "$command_for_guards" \
|
|
57
108
|
| grep -Eq '([[:space:]]|=)(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]]|/?\*?$)'; then
|
|
58
109
|
block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
|
|
59
110
|
fi
|
|
@@ -76,7 +127,7 @@ fi
|
|
|
76
127
|
# \<newline>main" splits into a segment matching --force but not `main`, letting a
|
|
77
128
|
# protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
|
|
78
129
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
79
|
-
normalized_command_str="$(printf '%s' "$
|
|
130
|
+
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
80
131
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
81
132
|
|
|
82
133
|
while IFS= read -r push_stmt; do
|
|
@@ -93,7 +144,7 @@ done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
|
|
|
93
144
|
# 3. `git reset --hard` while the working tree has uncommitted changes — this
|
|
94
145
|
# silently discards them. Only blocks when the tree is actually dirty, so a
|
|
95
146
|
# clean-tree reset (a legitimate workflow) still passes.
|
|
96
|
-
if printf '%s' "$
|
|
147
|
+
if printf '%s' "$command_for_guards" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
|
|
97
148
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
98
149
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
99
150
|
block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
|
|
@@ -101,7 +152,7 @@ if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+rese
|
|
|
101
152
|
fi
|
|
102
153
|
|
|
103
154
|
# 4. Dropping or truncating a database / schema / table.
|
|
104
|
-
if printf '%s' "$
|
|
155
|
+
if printf '%s' "$command_for_guards" \
|
|
105
156
|
| grep -Eiq '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
|
|
106
157
|
block "destructive SQL (DROP/TRUNCATE) detected"
|
|
107
158
|
fi
|
|
@@ -113,7 +164,7 @@ if [ -f "$rules_file" ]; then
|
|
|
113
164
|
case "$rule" in
|
|
114
165
|
'' | '#'*) continue ;;
|
|
115
166
|
esac
|
|
116
|
-
if printf '%s' "$
|
|
167
|
+
if printf '%s' "$command_for_guards" | grep -Eiq -- "$rule"; then
|
|
117
168
|
block "matched a project custom safety rule (${rules_file##*/}): $rule"
|
|
118
169
|
fi
|
|
119
170
|
done <"$rules_file"
|
|
@@ -32,6 +32,57 @@ if [ -z "$command_str" ]; then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
command_for_guards="$command_str"
|
|
36
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
37
|
+
command_for_guards="$(SAFETY_NET_COMMAND="$command_str" python3 - <<'PY'
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
command = os.environ.get("SAFETY_NET_COMMAND", "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def strip_heredocs(text: str) -> str:
|
|
45
|
+
lines = text.splitlines()
|
|
46
|
+
output = []
|
|
47
|
+
pending = []
|
|
48
|
+
# Quoted strings are matched (and thereby skipped over) as plain,
|
|
49
|
+
# non-capturing alternatives BEFORE the heredoc-marker alternative gets a
|
|
50
|
+
# chance to run, so a "<<MARKER"-looking sequence inside a string literal
|
|
51
|
+
# (e.g. `echo "hello <<MARKER"`) is never mistaken for a real heredoc
|
|
52
|
+
# start. Only the heredoc-marker alternative captures a group.
|
|
53
|
+
marker_pattern = re.compile(
|
|
54
|
+
r'"(?:\\.|[^"])*"|\'[^\']*\''
|
|
55
|
+
r"|<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
|
|
56
|
+
)
|
|
57
|
+
index = 0
|
|
58
|
+
while index < len(lines):
|
|
59
|
+
line = lines[index]
|
|
60
|
+
output.append(line)
|
|
61
|
+
pending.extend(
|
|
62
|
+
next(group for group in match.groups() if group)
|
|
63
|
+
for match in marker_pattern.finditer(line)
|
|
64
|
+
if any(match.groups())
|
|
65
|
+
)
|
|
66
|
+
index += 1
|
|
67
|
+
# Consume every pending heredoc body in order (chained same-line
|
|
68
|
+
# heredocs, e.g. `cat <<A <<B`, push more than one marker at once).
|
|
69
|
+
# Do NOT stop after the first terminator — dropping the `break` lets
|
|
70
|
+
# this loop keep dropping body lines until each pending marker is
|
|
71
|
+
# matched, instead of leaking the second body back into `output`
|
|
72
|
+
# where the destructive-pattern guards would see it.
|
|
73
|
+
while pending and index < len(lines):
|
|
74
|
+
if lines[index].strip() == pending[0]:
|
|
75
|
+
output.append(lines[index])
|
|
76
|
+
pending.pop(0)
|
|
77
|
+
index += 1
|
|
78
|
+
return "\n".join(output)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
print(strip_heredocs(command), end="")
|
|
82
|
+
PY
|
|
83
|
+
)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
35
86
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
36
87
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
37
88
|
block() {
|
|
@@ -49,11 +100,11 @@ EOF
|
|
|
49
100
|
# wildcard. Two gates ANDed: the command must invoke `rm` with BOTH a
|
|
50
101
|
# recursive and a force flag, AND name a catastrophic target. Splitting the
|
|
51
102
|
# flag check from the target check keeps each regex legible and testable.
|
|
52
|
-
if printf '%s' "$
|
|
103
|
+
if printf '%s' "$command_for_guards" \
|
|
53
104
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)' \
|
|
54
|
-
|| printf '%s' "$
|
|
105
|
+
|| printf '%s' "$command_for_guards" \
|
|
55
106
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm[[:space:]].*(-r\b.*[[:space:]]-f\b|-f\b.*[[:space:]]-r\b|--recursive\b.*--force\b|--force\b.*--recursive\b)'; then
|
|
56
|
-
if printf '%s' "$
|
|
107
|
+
if printf '%s' "$command_for_guards" \
|
|
57
108
|
| grep -Eq '([[:space:]]|=)(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]]|/?\*?$)'; then
|
|
58
109
|
block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
|
|
59
110
|
fi
|
|
@@ -76,7 +127,7 @@ fi
|
|
|
76
127
|
# \<newline>main" splits into a segment matching --force but not `main`, letting a
|
|
77
128
|
# protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
|
|
78
129
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
79
|
-
normalized_command_str="$(printf '%s' "$
|
|
130
|
+
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
80
131
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
81
132
|
|
|
82
133
|
while IFS= read -r push_stmt; do
|
|
@@ -93,7 +144,7 @@ done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
|
|
|
93
144
|
# 3. `git reset --hard` while the working tree has uncommitted changes — this
|
|
94
145
|
# silently discards them. Only blocks when the tree is actually dirty, so a
|
|
95
146
|
# clean-tree reset (a legitimate workflow) still passes.
|
|
96
|
-
if printf '%s' "$
|
|
147
|
+
if printf '%s' "$command_for_guards" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
|
|
97
148
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
98
149
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
99
150
|
block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
|
|
@@ -101,7 +152,7 @@ if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+rese
|
|
|
101
152
|
fi
|
|
102
153
|
|
|
103
154
|
# 4. Dropping or truncating a database / schema / table.
|
|
104
|
-
if printf '%s' "$
|
|
155
|
+
if printf '%s' "$command_for_guards" \
|
|
105
156
|
| grep -Eiq '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
|
|
106
157
|
block "destructive SQL (DROP/TRUNCATE) detected"
|
|
107
158
|
fi
|
|
@@ -113,7 +164,7 @@ if [ -f "$rules_file" ]; then
|
|
|
113
164
|
case "$rule" in
|
|
114
165
|
'' | '#'*) continue ;;
|
|
115
166
|
esac
|
|
116
|
-
if printf '%s' "$
|
|
167
|
+
if printf '%s' "$command_for_guards" | grep -Eiq -- "$rule"; then
|
|
117
168
|
block "matched a project custom safety rule (${rules_file##*/}): $rule"
|
|
118
169
|
fi
|
|
119
170
|
done <"$rules_file"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.222.
|
|
3
|
+
"version": "2.222.2",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.222.
|
|
3
|
+
"version": "2.222.2",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, across Claude and Codex.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.222.
|
|
3
|
+
"version": "2.222.2",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.222.
|
|
3
|
+
"version": "2.222.2",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lisa-openclaw",
|
|
3
|
-
"version": "2.222.
|
|
3
|
+
"version": "2.222.2",
|
|
4
4
|
"description": "Connect staff roles to Telegram or Slack via OpenClaw — facilitator/specialist hub-and-spoke routing and repo-coding topics, for Claude Code and Codex",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Cody Swann"
|
|
@@ -32,6 +32,57 @@ if [ -z "$command_str" ]; then
|
|
|
32
32
|
exit 0
|
|
33
33
|
fi
|
|
34
34
|
|
|
35
|
+
command_for_guards="$command_str"
|
|
36
|
+
if command -v python3 >/dev/null 2>&1; then
|
|
37
|
+
command_for_guards="$(SAFETY_NET_COMMAND="$command_str" python3 - <<'PY'
|
|
38
|
+
import os
|
|
39
|
+
import re
|
|
40
|
+
|
|
41
|
+
command = os.environ.get("SAFETY_NET_COMMAND", "")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def strip_heredocs(text: str) -> str:
|
|
45
|
+
lines = text.splitlines()
|
|
46
|
+
output = []
|
|
47
|
+
pending = []
|
|
48
|
+
# Quoted strings are matched (and thereby skipped over) as plain,
|
|
49
|
+
# non-capturing alternatives BEFORE the heredoc-marker alternative gets a
|
|
50
|
+
# chance to run, so a "<<MARKER"-looking sequence inside a string literal
|
|
51
|
+
# (e.g. `echo "hello <<MARKER"`) is never mistaken for a real heredoc
|
|
52
|
+
# start. Only the heredoc-marker alternative captures a group.
|
|
53
|
+
marker_pattern = re.compile(
|
|
54
|
+
r'"(?:\\.|[^"])*"|\'[^\']*\''
|
|
55
|
+
r"|<<-?\s*(?:'([^']+)'|\"([^\"]+)\"|([A-Za-z_][A-Za-z0-9_]*))"
|
|
56
|
+
)
|
|
57
|
+
index = 0
|
|
58
|
+
while index < len(lines):
|
|
59
|
+
line = lines[index]
|
|
60
|
+
output.append(line)
|
|
61
|
+
pending.extend(
|
|
62
|
+
next(group for group in match.groups() if group)
|
|
63
|
+
for match in marker_pattern.finditer(line)
|
|
64
|
+
if any(match.groups())
|
|
65
|
+
)
|
|
66
|
+
index += 1
|
|
67
|
+
# Consume every pending heredoc body in order (chained same-line
|
|
68
|
+
# heredocs, e.g. `cat <<A <<B`, push more than one marker at once).
|
|
69
|
+
# Do NOT stop after the first terminator — dropping the `break` lets
|
|
70
|
+
# this loop keep dropping body lines until each pending marker is
|
|
71
|
+
# matched, instead of leaking the second body back into `output`
|
|
72
|
+
# where the destructive-pattern guards would see it.
|
|
73
|
+
while pending and index < len(lines):
|
|
74
|
+
if lines[index].strip() == pending[0]:
|
|
75
|
+
output.append(lines[index])
|
|
76
|
+
pending.pop(0)
|
|
77
|
+
index += 1
|
|
78
|
+
return "\n".join(output)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
print(strip_heredocs(command), end="")
|
|
82
|
+
PY
|
|
83
|
+
)"
|
|
84
|
+
fi
|
|
85
|
+
|
|
35
86
|
# block() prints the reason to stderr (surfaced to the model) and exits 2 so the
|
|
36
87
|
# Bash tool call is denied. $1 = human-readable reason for the block.
|
|
37
88
|
block() {
|
|
@@ -49,11 +100,11 @@ EOF
|
|
|
49
100
|
# wildcard. Two gates ANDed: the command must invoke `rm` with BOTH a
|
|
50
101
|
# recursive and a force flag, AND name a catastrophic target. Splitting the
|
|
51
102
|
# flag check from the target check keeps each regex legible and testable.
|
|
52
|
-
if printf '%s' "$
|
|
103
|
+
if printf '%s' "$command_for_guards" \
|
|
53
104
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)' \
|
|
54
|
-
|| printf '%s' "$
|
|
105
|
+
|| printf '%s' "$command_for_guards" \
|
|
55
106
|
| grep -Eiq '(^|[^[:alnum:]_./-])rm[[:space:]].*(-r\b.*[[:space:]]-f\b|-f\b.*[[:space:]]-r\b|--recursive\b.*--force\b|--force\b.*--recursive\b)'; then
|
|
56
|
-
if printf '%s' "$
|
|
107
|
+
if printf '%s' "$command_for_guards" \
|
|
57
108
|
| grep -Eq '([[:space:]]|=)(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]]|/?\*?$)'; then
|
|
58
109
|
block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
|
|
59
110
|
fi
|
|
@@ -76,7 +127,7 @@ fi
|
|
|
76
127
|
# \<newline>main" splits into a segment matching --force but not `main`, letting a
|
|
77
128
|
# protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
|
|
78
129
|
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
79
|
-
normalized_command_str="$(printf '%s' "$
|
|
130
|
+
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
80
131
|
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
81
132
|
|
|
82
133
|
while IFS= read -r push_stmt; do
|
|
@@ -93,7 +144,7 @@ done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
|
|
|
93
144
|
# 3. `git reset --hard` while the working tree has uncommitted changes — this
|
|
94
145
|
# silently discards them. Only blocks when the tree is actually dirty, so a
|
|
95
146
|
# clean-tree reset (a legitimate workflow) still passes.
|
|
96
|
-
if printf '%s' "$
|
|
147
|
+
if printf '%s' "$command_for_guards" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+reset\b.*--hard\b'; then
|
|
97
148
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
98
149
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
99
150
|
block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
|
|
@@ -101,7 +152,7 @@ if printf '%s' "$command_str" | grep -Eiq '(^|[^[:alnum:]_-])git[[:space:]]+rese
|
|
|
101
152
|
fi
|
|
102
153
|
|
|
103
154
|
# 4. Dropping or truncating a database / schema / table.
|
|
104
|
-
if printf '%s' "$
|
|
155
|
+
if printf '%s' "$command_for_guards" \
|
|
105
156
|
| grep -Eiq '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
|
|
106
157
|
block "destructive SQL (DROP/TRUNCATE) detected"
|
|
107
158
|
fi
|
|
@@ -113,7 +164,7 @@ if [ -f "$rules_file" ]; then
|
|
|
113
164
|
case "$rule" in
|
|
114
165
|
'' | '#'*) continue ;;
|
|
115
166
|
esac
|
|
116
|
-
if printf '%s' "$
|
|
167
|
+
if printf '%s' "$command_for_guards" | grep -Eiq -- "$rule"; then
|
|
117
168
|
block "matched a project custom safety rule (${rules_file##*/}): $rule"
|
|
118
169
|
fi
|
|
119
170
|
done <"$rules_file"
|