@codyswann/lisa 2.291.0 → 2.293.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/all/copy-overwrite/scripts/lisa-work-item.mjs +64 -3
- package/all/merge/.claude/settings.json +1 -1
- package/cdk/merge/.claude/settings.json +1 -1
- package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
- package/dist/core/upstream-evidence-manifest.js +16 -13
- package/dist/core/upstream-evidence-manifest.js.map +1 -1
- package/dist/opencode/plugin-templates/parity-safety-net.sh +340 -35
- package/expo/merge/.claude/settings.json +1 -1
- package/harper-fabric/merge/.claude/settings.json +1 -1
- package/nestjs/merge/.claude/settings.json +1 -1
- package/package.json +1 -1
- package/phaser/merge/.claude/settings.json +1 -1
- package/plugins/lisa/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/plugin.json +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/.codex-plugin/skills/lisa-parity-safety-net-rules/SKILL.md +46 -5
- package/plugins/lisa/hooks/parity-safety-net.sh +340 -35
- package/plugins/lisa/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa/skills/lisa-parity-safety-net-rules/SKILL.md +46 -5
- package/plugins/lisa-agy/hooks/parity-safety-net.sh +340 -35
- package/plugins/lisa-agy/plugin.json +1 -1
- package/plugins/lisa-agy/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-agy/skills/lisa-parity-safety-net-rules/SKILL.md +46 -5
- 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 +340 -35
- package/plugins/lisa-copilot/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-copilot/skills/lisa-parity-safety-net-rules/SKILL.md +46 -5
- package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
- package/plugins/lisa-cursor/hooks/parity-safety-net.sh +340 -35
- package/plugins/lisa-cursor/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/lisa-cursor/skills/lisa-parity-safety-net-rules/SKILL.md +46 -5
- 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 +340 -35
- package/plugins/src/base/skills/lisa-implement/SKILL.md +1 -1
- package/plugins/src/base/skills/lisa-parity-safety-net-rules/SKILL.md +46 -5
- package/rails/merge/.claude/settings.json +1 -1
- package/scripts/install-claude-plugins.sh +9 -6
- package/typescript/merge/.claude/settings.json +1 -1
|
@@ -1,17 +1,55 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
2
|
# PreToolUse hook for Bash: a safety net that blocks destructive shell commands
|
|
3
3
|
# before they run. Lisa-native reimplementation of the upstream
|
|
4
|
-
# `safety-net@cc-marketplace` plugin's PreToolUse Bash-guard (parity work,
|
|
5
|
-
# #1059). It does NOT port upstream code — it re-expresses the
|
|
6
|
-
# hook conventions, modeled on block-no-verify.sh.
|
|
4
|
+
# `safety-net@cc-marketplace` plugin's PreToolUse Bash-guard (parity work,
|
|
5
|
+
# issues #1059 and #1960). It does NOT port upstream code — it re-expresses the
|
|
6
|
+
# behavior in Lisa's hook conventions, modeled on block-no-verify.sh.
|
|
7
7
|
#
|
|
8
8
|
# It reads the hook stdin JSON, inspects the proposed Bash command, and EXITS
|
|
9
9
|
# NON-ZERO (2) to BLOCK when a known-destructive pattern matches:
|
|
10
|
-
# - `rm -rf
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
# -
|
|
14
|
-
#
|
|
10
|
+
# - `rm -rf` of a root / home / wildcard path (quote-aware boundaries, so
|
|
11
|
+
# `bash -c "rm -rf /"` and interpreter one-liners are caught too; the rm
|
|
12
|
+
# guards also match path-prefixed invocations like `/bin/rm` and `./rm`)
|
|
13
|
+
# - `rm -rf` of the cwd (`.`), a `..`-traversal path, a `~/`-anchored path,
|
|
14
|
+
# an absolute path outside the project (with a /tmp, /var/tmp, $TMPDIR
|
|
15
|
+
# allowance), a `$VAR` target other than $TMPDIR, or ANY recursive forced
|
|
16
|
+
# delete while cwd is $HOME
|
|
17
|
+
# - force-pushing a protected branch (main/master/production/prod/release) —
|
|
18
|
+
# feature-branch force-push stays allowed (sanctioned rebase workflow;
|
|
19
|
+
# deliberate divergence from upstream's any-branch block). Every git guard
|
|
20
|
+
# sees through leading git GLOBAL options (`-C <path>`, `-c <k>=<v>`,
|
|
21
|
+
# `--git-dir[=…]`, `--no-pager`, …), so `git -C /path <destructive>` cannot
|
|
22
|
+
# dodge the subcommand anchor
|
|
23
|
+
# - `git reset --hard` / `--merge` while the working tree is dirty. Deliberate
|
|
24
|
+
# divergence: upstream blocks unconditionally; Lisa allows clean-tree resets.
|
|
25
|
+
# Residual risk (documented, accepted): the dirty check runs in the hook's
|
|
26
|
+
# cwd at hook time, so a `cd elsewhere && git reset --hard` evades it.
|
|
27
|
+
# - `git rebase --abort`/`--quit` while the in-progress rebase holds
|
|
28
|
+
# human-made conflict resolutions (AUTO_MERGE discriminator; issue #1956).
|
|
29
|
+
# Clean or untouched rebase state stays abortable; the apply backend and a
|
|
30
|
+
# missing AUTO_MERGE ref fail closed. Deliberate divergence: upstream
|
|
31
|
+
# blocks `rebase --abort` unconditionally, which strands agents mid-rebase.
|
|
32
|
+
# - `git checkout` discards (`--`, `-f/--force`, `--pathspec-from-file`,
|
|
33
|
+
# bare `.` — the bare-`.` block exceeds upstream)
|
|
34
|
+
# - `git switch --discard-changes` / `-f/--force`
|
|
35
|
+
# - `git restore` touching the worktree (allowed only with `--staged` and
|
|
36
|
+
# without `--worktree`)
|
|
37
|
+
# - `git stash drop` / `git stash clear`
|
|
38
|
+
# - `git clean` with force and no dry-run (`-n`/`--dry-run` anywhere wins)
|
|
39
|
+
# - `git branch -D` (or `-d` + `-f` in any spelling)
|
|
40
|
+
# - `git tag -d`, `git reflog delete`, `git worktree remove --force`
|
|
41
|
+
# - `find ... -delete`, `find ... -exec rm -rf`, `xargs ... rm -rf`
|
|
42
|
+
# - disk destroyers: `dd of=/dev/...`, `mkfs ... /dev/...`, `shred`
|
|
43
|
+
# - dropping or truncating a database/schema/table (Lisa-only guard)
|
|
44
|
+
# Otherwise it exits 0 and the command proceeds. Malformed hook input fails
|
|
45
|
+
# CLOSED: any parse error exits 2 (a non-2 exit would be a non-blocking hook
|
|
46
|
+
# error in Claude Code, silently failing open).
|
|
47
|
+
#
|
|
48
|
+
# Known accepted false-positive class: this is a text scan, not a shell engine,
|
|
49
|
+
# so display commands quoting a destructive string (`echo "docs about rm -rf /"`)
|
|
50
|
+
# can match. Upstream exempts those via an engine-only DISPLAY_COMMANDS list a
|
|
51
|
+
# grep hook cannot replicate. Workaround: quote-break the string or use the
|
|
52
|
+
# gh-writer heredoc form, whose payload is stripped before the guards run.
|
|
15
53
|
#
|
|
16
54
|
# Operators extend the built-in rules with a project-local rule file — one
|
|
17
55
|
# extended-regex (ERE) per line, blank lines and `#` comments ignored — managed
|
|
@@ -20,6 +58,13 @@
|
|
|
20
58
|
# ${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt
|
|
21
59
|
set -euo pipefail
|
|
22
60
|
|
|
61
|
+
# Fail CLOSED on any unexpected error (malformed JSON, missing jq, …): exit 2 so
|
|
62
|
+
# the tool call is denied instead of surfacing a non-blocking hook warning.
|
|
63
|
+
# Deliberately NOT `set -E`: errtrace would propagate this trap into command
|
|
64
|
+
# substitutions (e.g. the heredoc parser call, whose non-zero exit codes are
|
|
65
|
+
# meaningful protocol) and rewrite their status to 2 before it can be read.
|
|
66
|
+
trap 'printf "%s\n" "Blocked by safety-net: hook failed while parsing its input; denying fail-closed." >&2; exit 2' ERR
|
|
67
|
+
|
|
23
68
|
input="$(cat)"
|
|
24
69
|
|
|
25
70
|
tool_name="$(printf '%s' "$input" | jq -r '.tool_name // empty')"
|
|
@@ -90,22 +135,154 @@ case "$command_str" in
|
|
|
90
135
|
;;
|
|
91
136
|
esac
|
|
92
137
|
|
|
138
|
+
# matches / matches_cs run an ERE against the guarded command text. matches is
|
|
139
|
+
# case-insensitive (the default for these guards); matches_cs is case-SENSITIVE
|
|
140
|
+
# for guards where flag case is meaningful (`git branch -d` vs `-D`).
|
|
141
|
+
matches() {
|
|
142
|
+
printf '%s' "$command_for_guards" | grep -Eiq -- "$1"
|
|
143
|
+
}
|
|
144
|
+
matches_cs() {
|
|
145
|
+
printf '%s' "$command_for_guards" | grep -Eq -- "$1"
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
# Normalize bash line-continuations (a trailing backslash + newline → space)
|
|
149
|
+
# before segmenting the command. Without this, "git push --force origin
|
|
150
|
+
# \<newline>main" splits into a segment matching --force but not `main`, letting a
|
|
151
|
+
# protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
|
|
152
|
+
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
153
|
+
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
154
|
+
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
155
|
+
|
|
156
|
+
# Shared ERE fragments. GIT_TOKENS walks over intermediate argv tokens without
|
|
157
|
+
# crossing a statement separator, so a flag in a LATER statement can never be
|
|
158
|
+
# attributed to an earlier git subcommand.
|
|
159
|
+
readonly GIT_TOKENS='([[:space:]]+[^;&|[:space:]]+)*[[:space:]]+'
|
|
160
|
+
# Git GLOBAL options (`-C <path>`, `-c <k>=<v>`, `--git-dir[=…]`, `--no-pager`,
|
|
161
|
+
# …) legally sit between `git` and its subcommand, so every subcommand guard
|
|
162
|
+
# consumes them — otherwise `git -C /path checkout -- f` dodges the anchor
|
|
163
|
+
# (issue #1960 security review F1). Shape: zero or more dash-led tokens, each
|
|
164
|
+
# optionally followed by ONE non-dash value token, which covers both the
|
|
165
|
+
# `--git-dir=/x` and `--git-dir /x` spellings without naming every option.
|
|
166
|
+
readonly GIT_GLOBAL_OPTS='(-[^;&|[:space:]]+([[:space:]]+[^-;&|[:space:]][^;&|[:space:]]*)?[[:space:]]+)*'
|
|
167
|
+
# Anchor for every git subcommand guard: word-bounded `git`, whitespace, then
|
|
168
|
+
# any run of global options. Callers append the subcommand name directly.
|
|
169
|
+
readonly GIT_CMD='(^|[^[:alnum:]_-])git[[:space:]]+'"$GIT_GLOBAL_OPTS"
|
|
170
|
+
# An rm invocation: bare `rm` or a path whose basename is exactly `rm`
|
|
171
|
+
# (`/bin/rm`, `./rm`) — issue #1960 security review F2. The optional prefix must
|
|
172
|
+
# END IN `/` immediately before `rm`, so charm/confirm/informant/rmdir never
|
|
173
|
+
# match. The preceding-char class still excludes `.`, `/`, and `-` to keep
|
|
174
|
+
# `--rm`-style flags and `foo.rm` names out.
|
|
175
|
+
readonly RM_CMD='(^|[^[:alnum:]_./-])([[:alnum:]_./-]*/)?rm'
|
|
176
|
+
# rm invoked with BOTH a recursive and a force flag — clustered (-rf/-fr, any
|
|
177
|
+
# extra letters) or split, in either order. The split gate pairs ANY recursive
|
|
178
|
+
# form (short cluster containing r, or --recursive) with ANY force form (short
|
|
179
|
+
# cluster containing f, or --force), so mixed spellings like `rm -r --force /`
|
|
180
|
+
# and `rm --recursive -f /` cannot slip between the short/short and long/long
|
|
181
|
+
# alternations (PR #1976 review).
|
|
182
|
+
readonly RM_RF_CLUSTER="$RM_CMD"'([[:space:]]+-[[:alnum:]-]+)*[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)'
|
|
183
|
+
readonly RM_RF_SPLIT="$RM_CMD"'(([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*r[[:alnum:]]*|--recursive)([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|$)|([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]][^;&|]*)?[[:space:]](-[[:alnum:]]*r[[:alnum:]]*|--recursive)([[:space:]]|$))'
|
|
184
|
+
|
|
93
185
|
# 1. Recursive forced delete (`rm -rf`) of a filesystem root, home, or top-level
|
|
94
|
-
# wildcard. Two gates ANDed: the
|
|
186
|
+
# wildcard. Two gates ANDed: the statement must invoke `rm` with BOTH a
|
|
95
187
|
# recursive and a force flag, AND name a catastrophic target. Splitting the
|
|
96
188
|
# flag check from the target check keeps each regex legible and testable.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
189
|
+
# The target boundary classes include quote characters (issue #1960): without
|
|
190
|
+
# them, `bash -c "rm -rf /"` and interpreter one-liners like
|
|
191
|
+
# `python -c "… os.system('rm -rf /')"` slip through because the target is
|
|
192
|
+
# quote-adjacent instead of space-bounded.
|
|
193
|
+
# Both gates run PER STATEMENT inside the shared rm loop below (quality
|
|
194
|
+
# review S1): a harmless `/` in a LATER statement (`rm -rf build && cd /`)
|
|
195
|
+
# is never attributed to the rm in an earlier one.
|
|
196
|
+
qc="'\""
|
|
197
|
+
readonly RM_CATASTROPHIC_TARGET='([[:space:]='"$qc"'])(/|/\*|/\.\*?|~|~/\*?|\$HOME\b|\$\{HOME\}|\*)([[:space:]'"$qc"']|/?\*?['"$qc"']?$)'
|
|
198
|
+
|
|
199
|
+
# 1b. rm target hardening (issue #1960). For every statement that invokes rm
|
|
200
|
+
# with recursive+force flags, additionally block when:
|
|
201
|
+
# - cwd is $HOME (any target is one argument away from wiping home), or
|
|
202
|
+
# - the target is the cwd itself (`.` / `./`), or
|
|
203
|
+
# - the target traverses out via `..`, or
|
|
204
|
+
# - the target is home-anchored (`~/…` — the shell expands it to $HOME at
|
|
205
|
+
# execution time, so it is always outside the project), or
|
|
206
|
+
# - the target is an absolute path outside the project — with an allowance
|
|
207
|
+
# for /tmp, /var/tmp, and $TMPDIR — or
|
|
208
|
+
# - the target is a `$VAR` expansion other than the sanctioned $TMPDIR.
|
|
209
|
+
# Globbing is disabled around the token walk so a literal `*` in the command
|
|
210
|
+
# is never expanded against the hook's own cwd.
|
|
211
|
+
project_dir="${CLAUDE_PROJECT_DIR:-$PWD}"
|
|
212
|
+
tmp_dir_allow="${TMPDIR:-/tmp}"
|
|
213
|
+
tmp_dir_allow="${tmp_dir_allow%/}"
|
|
214
|
+
[ -n "$tmp_dir_allow" ] || tmp_dir_allow="/tmp"
|
|
215
|
+
while IFS= read -r rm_stmt; do
|
|
216
|
+
if ! printf '%s' "$rm_stmt" | grep -Eiq -- "$RM_RF_CLUSTER" \
|
|
217
|
+
&& ! printf '%s' "$rm_stmt" | grep -Eiq -- "$RM_RF_SPLIT"; then
|
|
218
|
+
continue
|
|
219
|
+
fi
|
|
220
|
+
# Guard 1: catastrophic target named in the SAME statement as the rm -rf.
|
|
221
|
+
if printf '%s' "$rm_stmt" | grep -Eq -- "$RM_CATASTROPHIC_TARGET"; then
|
|
103
222
|
block "recursive forced delete of a root, home, or wildcard path (rm -rf)"
|
|
104
223
|
fi
|
|
105
|
-
|
|
224
|
+
# Physical-path comparison (pwd -P): on macOS $HOME or the cwd may arrive via
|
|
225
|
+
# a symlink (/var → /private/var), and a string compare would miss the match.
|
|
226
|
+
if [ -n "${HOME:-}" ] \
|
|
227
|
+
&& [ "$(pwd -P)" = "$(cd -- "$HOME" 2>/dev/null && pwd -P)" ]; then
|
|
228
|
+
block "recursive forced delete while the working directory is \$HOME (cd into a project first)"
|
|
229
|
+
fi
|
|
230
|
+
set -f
|
|
231
|
+
seen_rm=0
|
|
232
|
+
for raw_token in $rm_stmt; do
|
|
233
|
+
token="${raw_token#\"}"
|
|
234
|
+
token="${token#\'}"
|
|
235
|
+
token="${token%\"}"
|
|
236
|
+
token="${token%\'}"
|
|
237
|
+
if [ "$seen_rm" -eq 0 ]; then
|
|
238
|
+
# Path-prefixed spellings (`/bin/rm`, `./rm`) are still rm (F2).
|
|
239
|
+
case "$token" in
|
|
240
|
+
rm | */rm) seen_rm=1 ;;
|
|
241
|
+
esac
|
|
242
|
+
continue
|
|
243
|
+
fi
|
|
244
|
+
case "$token" in
|
|
245
|
+
-*) continue ;;
|
|
246
|
+
. | ./)
|
|
247
|
+
set +f
|
|
248
|
+
block "recursive forced delete of the current directory (rm -rf .)"
|
|
249
|
+
;;
|
|
250
|
+
.. | ../* | */.. | */../*)
|
|
251
|
+
set +f
|
|
252
|
+
block "recursive forced delete of a path outside the project (.. traversal)"
|
|
253
|
+
;;
|
|
254
|
+
'~' | '~'/*)
|
|
255
|
+
set +f
|
|
256
|
+
block "recursive forced delete of a home-anchored path (~/…) outside the project"
|
|
257
|
+
;;
|
|
258
|
+
/*)
|
|
259
|
+
case "$token" in
|
|
260
|
+
"$project_dir" | "$project_dir"/* | /tmp | /tmp/* | /var/tmp | /var/tmp/* | "$tmp_dir_allow" | "$tmp_dir_allow"/*) : ;;
|
|
261
|
+
*)
|
|
262
|
+
set +f
|
|
263
|
+
block "recursive forced delete of an absolute path outside the project (only the project, /tmp, /var/tmp, and \$TMPDIR are allowed)"
|
|
264
|
+
;;
|
|
265
|
+
esac
|
|
266
|
+
;;
|
|
267
|
+
*'$'*)
|
|
268
|
+
case "$token" in
|
|
269
|
+
'$TMPDIR' | '$TMPDIR'/* | '${TMPDIR}' | '${TMPDIR}'/*) : ;;
|
|
270
|
+
*)
|
|
271
|
+
set +f
|
|
272
|
+
block "recursive forced delete of a variable-expanded target (unset or mistyped variables can point anywhere; \$TMPDIR is the only sanctioned dynamic target)"
|
|
273
|
+
;;
|
|
274
|
+
esac
|
|
275
|
+
;;
|
|
276
|
+
esac
|
|
277
|
+
done
|
|
278
|
+
set +f
|
|
279
|
+
done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
|
|
280
|
+
| grep -Ei -- "$RM_CMD"'([[:space:]]|$)' || true)
|
|
106
281
|
|
|
107
282
|
# 2. Force-pushing a protected branch. `--force-with-lease` is the safe,
|
|
108
|
-
# non-clobbering alternative and is intentionally NOT blocked.
|
|
283
|
+
# non-clobbering alternative and is intentionally NOT blocked. Deliberate
|
|
284
|
+
# divergence from upstream (which blocks force-push on ANY branch):
|
|
285
|
+
# feature-branch force-push is a sanctioned rebase-and-push agent workflow.
|
|
109
286
|
#
|
|
110
287
|
# The force flag AND the protected-branch name must appear in the SAME
|
|
111
288
|
# `git push` statement. Checking them independently over the whole command is
|
|
@@ -116,14 +293,6 @@ fi
|
|
|
116
293
|
# (`;`, `&&`, `||`, `|`, newlines), keep only the `git push` segments, and
|
|
117
294
|
# inspect each in isolation — a real `git push --force origin main` still
|
|
118
295
|
# matches, while a feature-branch push next to `[ -f ]`/`--base main` passes.
|
|
119
|
-
# Normalize bash line-continuations (a trailing backslash + newline → space)
|
|
120
|
-
# before segmenting the command. Without this, "git push --force origin
|
|
121
|
-
# \<newline>main" splits into a segment matching --force but not `main`, letting a
|
|
122
|
-
# protected force-push slip past. Uses awk (POSIX) instead of a GNU-only
|
|
123
|
-
# `sed ':a;N;$!ba;…'`, which errors on BSD sed (macOS) and there silently no-ops.
|
|
124
|
-
normalized_command_str="$(printf '%s' "$command_for_guards" \
|
|
125
|
-
| awk '{ if (sub(/\\$/, "")) printf "%s ", $0; else print }')"
|
|
126
|
-
|
|
127
296
|
while IFS= read -r push_stmt; do
|
|
128
297
|
if printf '%s' "$push_stmt" \
|
|
129
298
|
| grep -Eiq '(--force([[:space:]]|=|$)|[[:space:]]-f([[:space:]]|$))' \
|
|
@@ -133,25 +302,161 @@ while IFS= read -r push_stmt; do
|
|
|
133
302
|
block "force-pushing a protected branch (use --force-with-lease, or push a feature branch)"
|
|
134
303
|
fi
|
|
135
304
|
done < <(printf '%s' "$normalized_command_str" | tr '&|;' '\n' \
|
|
136
|
-
| grep -Ei
|
|
305
|
+
| grep -Ei -- "${GIT_CMD}push\b" || true)
|
|
137
306
|
|
|
138
|
-
# 3. `git reset --hard` while the working tree has
|
|
139
|
-
# silently
|
|
140
|
-
# clean-tree reset (a legitimate workflow)
|
|
141
|
-
|
|
307
|
+
# 3. `git reset --hard` / `git reset --merge` while the working tree has
|
|
308
|
+
# uncommitted changes — both silently discard them. Only blocks when the tree
|
|
309
|
+
# is actually dirty, so a clean-tree reset (a legitimate workflow) passes.
|
|
310
|
+
# Deliberate divergence from upstream's unconditional block; the accepted
|
|
311
|
+
# residual risk (dirty check runs at hook time in the hook's cwd) is
|
|
312
|
+
# documented in the header.
|
|
313
|
+
if matches "${GIT_CMD}"'reset\b.*--(hard|merge)\b'; then
|
|
142
314
|
if git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
|
143
315
|
&& [ -n "$(git status --porcelain 2>/dev/null)" ]; then
|
|
144
|
-
block "git reset --hard on a dirty working tree would discard uncommitted changes (stash or commit first)"
|
|
316
|
+
block "git reset --hard/--merge on a dirty working tree would discard uncommitted changes (stash or commit first)"
|
|
145
317
|
fi
|
|
146
318
|
fi
|
|
147
319
|
|
|
148
|
-
#
|
|
149
|
-
|
|
150
|
-
|
|
320
|
+
# 3b. `git rebase --abort` / `--quit` ONLY while human/agent conflict
|
|
321
|
+
# resolutions exist (issue #1956). `--abort` restores the pre-rebase branch
|
|
322
|
+
# and discards every resolution; `--quit` deletes the rebase bookkeeping
|
|
323
|
+
# (head-name, todo) while stranding a detached HEAD, making recovery
|
|
324
|
+
# ambiguous — both are treated the same, conditionally. A rebase state with
|
|
325
|
+
# nothing human-made in it (a clean-pick wedge, or a conflict stop nobody
|
|
326
|
+
# has touched) is agent-recoverable, so aborting it stays ALLOWED.
|
|
327
|
+
# Discriminator (empirical, git 2.53): diff the worktree/index against the
|
|
328
|
+
# AUTO_MERGE ref (the merge-ort recorded conflicted tree). Abort-safe ⇔
|
|
329
|
+
# worktree diff is quiet AND (the cached diff is quiet OR unmerged index
|
|
330
|
+
# entries exist — an untouched conflict stop has unmerged entries that make
|
|
331
|
+
# the cached diff non-quiet without any human edit). Fail CLOSED on the
|
|
332
|
+
# rebase-apply backend (no AUTO_MERGE contract) and on a missing AUTO_MERGE
|
|
333
|
+
# ref while rebase-merge state exists. Same accepted residual risk as
|
|
334
|
+
# guard 3: the probes run in the hook's cwd at hook time.
|
|
335
|
+
if matches "${GIT_CMD}"'rebase'"${GIT_TOKENS}"'--(abort|quit)([[:space:]]|$)'; then
|
|
336
|
+
rebase_apply_dir="$(git rev-parse --git-path rebase-apply 2>/dev/null || true)"
|
|
337
|
+
rebase_merge_dir="$(git rev-parse --git-path rebase-merge 2>/dev/null || true)"
|
|
338
|
+
if [ -n "$rebase_apply_dir" ] && [ -d "$rebase_apply_dir" ]; then
|
|
339
|
+
block "git rebase --abort/--quit on an apply-backend rebase cannot prove no conflict resolutions would be lost (fail closed; finish or continue the rebase instead)"
|
|
340
|
+
fi
|
|
341
|
+
if [ -n "$rebase_merge_dir" ] && [ -d "$rebase_merge_dir" ]; then
|
|
342
|
+
if ! git rev-parse -q --verify AUTO_MERGE >/dev/null 2>&1; then
|
|
343
|
+
block "git rebase --abort/--quit with an unresolvable AUTO_MERGE ref cannot prove no conflict resolutions would be lost (fail closed)"
|
|
344
|
+
fi
|
|
345
|
+
if ! git diff --quiet AUTO_MERGE 2>/dev/null; then
|
|
346
|
+
block "git rebase --abort/--quit would discard conflict resolutions in the working tree (finish resolving and run git rebase --continue instead)"
|
|
347
|
+
fi
|
|
348
|
+
unmerged_entries="$(git ls-files -u 2>/dev/null || true)"
|
|
349
|
+
if [ -z "$unmerged_entries" ] \
|
|
350
|
+
&& ! git diff --cached --quiet AUTO_MERGE 2>/dev/null; then
|
|
351
|
+
block "git rebase --abort/--quit would discard staged conflict resolutions (finish resolving and run git rebase --continue instead)"
|
|
352
|
+
fi
|
|
353
|
+
fi
|
|
354
|
+
fi
|
|
355
|
+
|
|
356
|
+
# 4. `git checkout` worktree discards: the `--` pathspec form (with or without a
|
|
357
|
+
# ref), `-f/--force`, `--pathspec-from-file`, and bare `git checkout .`.
|
|
358
|
+
# Blocking bare `.` exceeds upstream (which allows any single positional) —
|
|
359
|
+
# issue #1960 names it explicitly: it discards the entire tree. Branch
|
|
360
|
+
# switches and creation (`-b`/`-B`) stay allowed.
|
|
361
|
+
readonly GIT_CHECKOUT="${GIT_CMD}checkout"
|
|
362
|
+
if matches "${GIT_CHECKOUT}${GIT_TOKENS}"'--([[:space:]]|$)' \
|
|
363
|
+
|| matches "${GIT_CHECKOUT}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
364
|
+
|| matches "${GIT_CHECKOUT}"'[[:space:]][^;&|]*--pathspec-from-file' \
|
|
365
|
+
|| matches "${GIT_CHECKOUT}"'[[:space:]]+\.(/)?([[:space:]]|$)'; then
|
|
366
|
+
block "git checkout discarding local changes (--, -f/--force, --pathspec-from-file, or bare .) — use git stash to preserve work first"
|
|
367
|
+
fi
|
|
368
|
+
|
|
369
|
+
# 5. `git switch` discards: `--discard-changes` and its `-f/--force` aliases.
|
|
370
|
+
if matches "${GIT_CMD}switch${GIT_TOKENS}"'(--discard-changes|--force|-f)([[:space:]]|=|$)'; then
|
|
371
|
+
block "git switch discarding local changes (--discard-changes/-f/--force)"
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
# 6. `git restore` defaults to overwriting the WORKTREE — a silent discard. Only
|
|
375
|
+
# the pure staging-area form is safe: `--staged` present and `--worktree`
|
|
376
|
+
# absent. Two conditions, because an ERE has no lookahead: `--staged
|
|
377
|
+
# --worktree` still discards, so `--worktree` blocks regardless of `--staged`.
|
|
378
|
+
if matches "${GIT_CMD}"'restore([[:space:]]|$)'; then
|
|
379
|
+
if matches "${GIT_CMD}"'restore[^;&|]*--worktree' \
|
|
380
|
+
|| ! matches "${GIT_CMD}"'restore[^;&|]*--staged'; then
|
|
381
|
+
block "git restore overwriting worktree files (only 'git restore --staged <path>' without --worktree is allowed)"
|
|
382
|
+
fi
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
# 7. `git stash drop` / `git stash clear` destroy stashed work. push/pop/list/
|
|
386
|
+
# apply — the safe alternatives the reset guard recommends — stay allowed.
|
|
387
|
+
if matches "${GIT_CMD}"'stash[[:space:]]+(drop|clear)([[:space:]]|$)'; then
|
|
388
|
+
block "git stash drop/clear destroys stashed work"
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
# 8. `git clean` with a force flag wipes untracked files. A dry-run flag
|
|
392
|
+
# (`-n`/`--dry-run`) ANYWHERE wins — git itself performs no deletion under
|
|
393
|
+
# dry-run, so `git clean -fdn` is a safe preview.
|
|
394
|
+
readonly GIT_CLEAN="${GIT_CMD}clean"
|
|
395
|
+
if matches "${GIT_CLEAN}${GIT_TOKENS}"'(-[[:alnum:]]*f[[:alnum:]]*|--force)([[:space:]]|=|$)' \
|
|
396
|
+
&& ! matches "${GIT_CLEAN}${GIT_TOKENS}"'(-[[:alnum:]]*n[[:alnum:]]*|--dry-run)([[:space:]]|=|$)'; then
|
|
397
|
+
block "git clean --force deletes untracked files (preview with git clean -n first)"
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
# 9. `git branch` force-delete loses unmerged commits: `-D`, `-d` combined with
|
|
401
|
+
# `-f` (clustered or split), or `--delete` + `--force`. Case-SENSITIVE so the
|
|
402
|
+
# safe `-d` (which refuses unmerged work) and rename `-m` stay allowed.
|
|
403
|
+
readonly GIT_BRANCH="${GIT_CMD}branch"
|
|
404
|
+
if matches_cs "${GIT_BRANCH}${GIT_TOKENS}"'-[[:alnum:]]*D[[:alnum:]]*([[:space:]]|$)' \
|
|
405
|
+
|| { matches_cs "${GIT_BRANCH}${GIT_TOKENS}"'-[[:alnum:]]*d[[:alnum:]]*([[:space:]]|$)' \
|
|
406
|
+
&& matches_cs "${GIT_BRANCH}${GIT_TOKENS}"'-[[:alnum:]]*f[[:alnum:]]*([[:space:]]|$)'; } \
|
|
407
|
+
|| { matches "${GIT_BRANCH}"'[^;&|]*--delete' && matches "${GIT_BRANCH}"'[^;&|]*--force'; }; then
|
|
408
|
+
block "git branch force-delete (-D) loses unmerged commits (use -d, which refuses unmerged work)"
|
|
409
|
+
fi
|
|
410
|
+
|
|
411
|
+
# 10. Cheap destructive-ref guards: `git tag -d` (tags are shared refs),
|
|
412
|
+
# `git reflog delete` (erases recovery history), and
|
|
413
|
+
# `git worktree remove --force` (discards a dirty worktree). Tag deletion is
|
|
414
|
+
# case-SENSITIVE so annotated-tag `-a` and message `-m` flags never match.
|
|
415
|
+
if matches_cs "${GIT_CMD}tag${GIT_TOKENS}"'(-[[:alnum:]]*d[[:alnum:]]*([[:space:]]|$)|--delete([[:space:]]|=|$))'; then
|
|
416
|
+
block "git tag -d deletes a shared ref"
|
|
417
|
+
fi
|
|
418
|
+
if matches "${GIT_CMD}"'reflog[[:space:]]+delete([[:space:]]|$)'; then
|
|
419
|
+
block "git reflog delete erases recovery history"
|
|
420
|
+
fi
|
|
421
|
+
if matches "${GIT_CMD}"'worktree[[:space:]]+remove[^;&|]*(--force([[:space:]]|=|$)|[[:space:]]-[[:alnum:]]*f[[:alnum:]]*([[:space:]]|$))'; then
|
|
422
|
+
block "git worktree remove --force discards a dirty worktree (remove without --force, which refuses dirty trees)"
|
|
423
|
+
fi
|
|
424
|
+
|
|
425
|
+
# 11. Deletion through find/xargs, where the target never appears as a literal
|
|
426
|
+
# argument: `find … -delete`, `find … -exec rm -rf`, and `xargs … rm -rf`
|
|
427
|
+
# (targets arrive from dynamic stdin — unauditable). Plain `rm` (no
|
|
428
|
+
# recursive+force) on find/xargs output stays allowed for normal cleanups.
|
|
429
|
+
if matches '(^|[^[:alnum:]_./-])find[[:space:]][^;&|]*[[:space:]]-delete([[:space:]]|$)'; then
|
|
430
|
+
block "find -delete removes files tree-wide (use -print to preview, or an explicit rm on reviewed paths)"
|
|
431
|
+
fi
|
|
432
|
+
if matches '(^|[^[:alnum:]_./-])find[[:space:]][^;&|]*-exec[[:space:]]+rm[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)'; then
|
|
433
|
+
block "find -exec rm -rf performs a recursive forced delete on unreviewed paths"
|
|
434
|
+
fi
|
|
435
|
+
if matches '(^|[^[:alnum:]_./-])xargs[[:space:]]([^;&|]*[[:space:]])?rm[[:space:]]+(-[[:alnum:]]*r[[:alnum:]]*f|-[[:alnum:]]*f[[:alnum:]]*r)([[:space:]]|$)'; then
|
|
436
|
+
block "xargs rm -rf performs a recursive forced delete on dynamic stdin input"
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
# 12. Disk destroyers, always on (exceeds upstream, which only scans these
|
|
440
|
+
# inside interpreter one-liners): writing to a raw /dev node via dd,
|
|
441
|
+
# formatting a device with mkfs, and shred's unrecoverable overwrite.
|
|
442
|
+
# dd/mkfs against regular files (imaging) stays allowed.
|
|
443
|
+
if matches '(^|[^[:alnum:]_./-])dd[[:space:]]+([^;&|]*[[:space:]])?of=/dev/'; then
|
|
444
|
+
block "dd writing to a raw device (of=/dev/…) destroys it"
|
|
445
|
+
fi
|
|
446
|
+
if matches '(^|[^[:alnum:]_./-])mkfs(\.[[:alnum:]]+)?[[:space:]][^;&|]*/dev/'; then
|
|
447
|
+
block "mkfs formatting a device erases it"
|
|
448
|
+
fi
|
|
449
|
+
if matches '(^|[^[:alnum:]_./-])shred([[:space:]]|$)'; then
|
|
450
|
+
block "shred overwrites files unrecoverably"
|
|
451
|
+
fi
|
|
452
|
+
|
|
453
|
+
# 13. Dropping or truncating a database / schema / table (Lisa-only guard —
|
|
454
|
+
# upstream has no SQL protection; keep).
|
|
455
|
+
if matches '\b(drop[[:space:]]+(database|schema|table)|truncate[[:space:]]+(table[[:space:]]+)?[[:alnum:]_."`]+)\b'; then
|
|
151
456
|
block "destructive SQL (DROP/TRUNCATE) detected"
|
|
152
457
|
fi
|
|
153
458
|
|
|
154
|
-
#
|
|
459
|
+
# 14. Project-local custom rules. Each non-comment line is an ERE; a match blocks.
|
|
155
460
|
rules_file="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-net-rules.txt}"
|
|
156
461
|
if [ -f "$rules_file" ]; then
|
|
157
462
|
while IFS= read -r rule || [ -n "$rule" ]; do
|
|
@@ -95,7 +95,7 @@ Using the general-purpose agent in Team Lead session, **determine the base branc
|
|
|
95
95
|
- Already on a feature branch with an **open PR** → reuse it. If the PR's base ≠ the resolved base branch, surface the mismatch and re-target only with confirmation — the ticket's environment is the source of truth.
|
|
96
96
|
- Already on a feature branch with **no open PR** → reuse it; its PR base will be the resolved base branch (do not ask the human — the environment determines it).
|
|
97
97
|
- On an **environment / default branch** → check out a feature branch named for this plan (with the work-item ref prefix, per the linkage rules below) **from `origin/<base>`**.
|
|
98
|
-
- **
|
|
98
|
+
- **Sync the feature branch onto the latest `origin/<base>` and resolve any merge conflicts BEFORE starting work.** Both sync lanes are sanctioned in a bound worktree: **rebase** (`git rebase origin/<base>` — the commit hooks validate mid-rebase picks against the rebase head-name, so a work-item binding never wedges a rebase) and **merge** (`git merge origin/<base>` — push validation exempts commits already reachable from the remote default branch, while branch-authored commits stay strictly validated; the exemption needs the local `origin/HEAD` symref, so if a hand-added remote fails push validation on foreign commits, run `git remote set-head origin -a` first). Prefer rebase for a linear history; use merge when rewriting pushed history is undesirable. If a rebase goes wrong before anyone has resolved conflicts, `git rebase --abort` is a safe, allowed recovery; once conflict resolutions exist, the safety net blocks abort to protect them — finish resolving and `git rebase --continue` instead. If the conflicts cannot be resolved cleanly and safely, create a fix task for the agent team (with the conflicting file list and current merge state) and resolve it before implementation begins — never start work on stale or conflicted code.
|
|
99
99
|
4. **The PR targets the resolved base branch** — carry it as `target_branch=<base>` into `lisa-git-submit-pr` (Verify flow). `git-submit-pr` already chooses a closing keyword when the base is the production/default branch and a non-closing reference for a non-terminal environment branch. For a bug fixed on a non-integration environment branch, the current flow is not done until the fix is merged and verified there, then forward cherry-picked down to the integration branch via a linked follow-up.
|
|
100
100
|
|
|
101
101
|
Every Implement run now has a tracker work item. Preserve its canonical identifier for development linkage unconditionally:
|
|
@@ -53,11 +53,52 @@ RULES_FILE="${SAFETY_NET_RULES_FILE:-${CLAUDE_PROJECT_DIR:-$PWD}/.claude/safety-
|
|
|
53
53
|
|
|
54
54
|
### Built-in guards (always on)
|
|
55
55
|
|
|
56
|
-
1. `rm -rf` of a filesystem root, `$HOME`/`~`, or a top-level wildcard
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
56
|
+
1. `rm -rf` of a filesystem root, `$HOME`/`~`, or a top-level wildcard — with
|
|
57
|
+
quote-aware boundaries, so wrapper/interpreter forms like
|
|
58
|
+
`bash -c "rm -rf /"` and `python -c "… os.system('rm -rf /')"` match too.
|
|
59
|
+
Path-prefixed spellings (`/bin/rm`, `./rm`) count as `rm` for every rm
|
|
60
|
+
guard.
|
|
61
|
+
2. `rm -rf` target hardening: the cwd itself (`.`/`./`), `..`-traversal paths,
|
|
62
|
+
home-anchored `~/…` paths, absolute paths outside the project (`/tmp`,
|
|
63
|
+
`/var/tmp`, and `$TMPDIR` are allowed), `$VAR` targets other than `$TMPDIR`,
|
|
64
|
+
and **any** recursive forced delete while the working directory is `$HOME`.
|
|
65
|
+
3. Force-pushing a protected branch
|
|
66
|
+
(`main`/`master`/`production`/`prod`/`release`). `--force-with-lease` is
|
|
67
|
+
intentionally allowed, and so is force-pushing a feature branch (sanctioned
|
|
68
|
+
rebase workflow). Acceptable parity divergence: a refspec force-push
|
|
69
|
+
(`git push origin +main`) is not blocked — upstream 1.0.6 allows it too.
|
|
70
|
+
4. `git reset --hard` / `git reset --merge` while the working tree is **dirty**
|
|
71
|
+
(would discard work). Clean-tree resets are intentionally allowed.
|
|
72
|
+
5. `git rebase --abort` / `git rebase --quit` while the in-progress rebase
|
|
73
|
+
holds human-made conflict resolutions (they would be discarded). A clean or
|
|
74
|
+
untouched rebase state stays abortable — an agent-recoverable wedge; the
|
|
75
|
+
apply backend and a missing `AUTO_MERGE` ref fail closed. Deliberate
|
|
76
|
+
divergence: upstream blocks `rebase --abort` unconditionally.
|
|
77
|
+
6. `git checkout` discards: the `--` pathspec form (with or without a ref),
|
|
78
|
+
`-f`/`--force`, `--pathspec-from-file`, and bare `git checkout .`.
|
|
79
|
+
Branch switching and `-b`/`-B` creation stay allowed.
|
|
80
|
+
7. `git switch --discard-changes` / `-f`/`--force`.
|
|
81
|
+
8. `git restore` touching the worktree — only `git restore --staged <path>`
|
|
82
|
+
without `--worktree` is allowed (unstaging is safe).
|
|
83
|
+
9. `git stash drop` / `git stash clear` (push/pop/list/apply stay allowed).
|
|
84
|
+
10. `git clean` with a force flag and no dry-run — `-n`/`--dry-run` anywhere
|
|
85
|
+
makes it a safe preview.
|
|
86
|
+
11. `git branch -D` (or `-d` combined with `-f`, clustered or split);
|
|
87
|
+
safe `-d` and rename `-m` stay allowed.
|
|
88
|
+
12. `git tag -d`, `git reflog delete`, `git worktree remove --force`.
|
|
89
|
+
13. Deletion via `find … -delete`, `find … -exec rm -rf`, and `xargs … rm -rf`
|
|
90
|
+
(plain non-recursive `rm` on find/xargs output stays allowed).
|
|
91
|
+
14. Disk destroyers: `dd of=/dev/…`, `mkfs … /dev/…`, `shred`.
|
|
92
|
+
15. Destructive SQL — `DROP DATABASE/SCHEMA/TABLE`, `TRUNCATE TABLE`.
|
|
93
|
+
|
|
94
|
+
Every git guard sees through leading git **global options** (`-C <path>`,
|
|
95
|
+
`-c <k>=<v>`, `--git-dir[=…]`, `--no-pager`, …), so `git -C /path <destructive>`
|
|
96
|
+
is screened the same as `git <destructive>`.
|
|
97
|
+
|
|
98
|
+
Malformed hook input fails **closed** (exit 2 denies the command). A text-scan
|
|
99
|
+
hook cannot exempt display commands, so prose like
|
|
100
|
+
`echo "docs about rm -rf /"` can false-positive — quote-break the string or use
|
|
101
|
+
the gh-writer heredoc form (payload is stripped before the guards run).
|
|
61
102
|
|
|
62
103
|
## View the current rules
|
|
63
104
|
|