@hasna/hooks 0.4.1 → 0.5.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.
@@ -6,8 +6,78 @@ This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail
6
6
 
7
7
  It also blocks scoped destructive shell operations such as recursive `rm`,
8
8
  `rsync --delete`, destructive `find`, and destructive `git clean` / `git reset
9
- --hard` forms only when the resolved target threatens `~/.hasna`, configured
10
- workspace roots, Hasna division/scope roots, or active repo/worktree roots.
9
+ --hard` forms when the resolved target threatens a protected root.
10
+
11
+ ## Protected roots
12
+
13
+ - `/` and the system directories (`/usr`, `/etc`, `/bin`, `/lib`, `/var`, `/boot`,
14
+ `/home`, `/Users`, and the other FHS and macOS equivalents). Add machine-specific
15
+ entries with `HASNA_PROTECTED_SYSTEM_ROOTS` (colon-separated). `/tmp` is not
16
+ protected — scratch cleanup there is routine.
17
+ - `~/.hasna`, configured workspace roots, Hasna division/scope roots, and active
18
+ repo/worktree roots.
19
+
20
+ These match in *root* mode: wiping a root or its contents (`rm -rf /usr`,
21
+ `rm -rf /usr/*`) blocks, while a targeted delete beneath one
22
+ (`rm -rf /usr/local/lib/my-build`) is allowed.
23
+
24
+ ## Expansions that can collapse to empty
25
+
26
+ A destructive target containing a command substitution, backtick substitution or
27
+ variable expansion is checked twice: as written, and as the shell would render it
28
+ if the expansion returned empty. `rm -rf "$(anything)"/*`, `` rm -rf `cmd`/* ``,
29
+ `rm -rf "$VAR"/*` and `rm -rf "${VAR}"/*` are blocked by shape, whatever the
30
+ expansion is.
31
+
32
+ This exists because of a realized incident: `bun pm cache` exits non-zero with an
33
+ empty stdout when no `package.json` is found walking up from cwd, so
34
+ `rm -rf "$(bun pm cache)"/*` ran as `rm -rf /*`. Redirecting stderr does not help
35
+ — it discards the diagnostic, not the path.
36
+
37
+ Two forms are deliberately not blocked:
38
+
39
+ - `${VAR:?}` / `${VAR:?message}`, which POSIX guarantees non-empty. (`${VAR?}`
40
+ without the colon permits an empty value and is *not* exempt.)
41
+ - A bare `rm -rf "$(cmd)"` with no trailing separator, which degrades to
42
+ `rm -rf ""` — rejected by `rm` without deleting anything.
43
+
44
+ The recommended form is to resolve the path first and assert it:
45
+
46
+ ```bash
47
+ dir="$(bun pm cache)" || exit 1
48
+ case "$dir" in /|"") exit 1;; esac
49
+ rm -rf -- "$dir"
50
+ ```
51
+
52
+ ## Globs
53
+
54
+ A glob threatens a protected root when it can match that root or an ancestor of it, or when it
55
+ wipes the root's contents wholesale. Matching is per path component, so a trailing literal
56
+ bounds the delete: `rm -rf */node_modules` at a monorepo root is allowed, while `rm -rf /*/*`
57
+ is not.
58
+
59
+ A glob directly under a protected root is refused only when it is *unanchored* — when no
60
+ literal text survives once the wildcards are removed. `[a-z]*`, `?*`, `.??*` and `*.*` are
61
+ unanchored and blocked; `*.log`, `tmp-*`, `.turbo*` and `snapshot-[0-9]*` keep their literal
62
+ anchor and are allowed.
63
+
64
+ Bracket expressions that this matcher does not model exactly — POSIX `[:class:]`, `[=equiv=]`,
65
+ `[.collate.]`, backslash escapes, anything unterminated — are treated as **matching**, never as
66
+ not-matching. An under-match would leave a protected root unmatched and allow the delete, so
67
+ ambiguity resolves toward refusing.
68
+
69
+ ## Working directory
70
+
71
+ `cd`, `pushd`, `pushd -n`, `popd` and `cd -` are tracked, per subshell, with a directory stack.
72
+ A `cd` inside `( … )` or a pipeline stage applies within that shell and does not escape it.
73
+
74
+ ## Wrappers
75
+
76
+ Commands are unwrapped before scanning: `bash -c` / `sh -c` / `zsh -c`, `su -c`,
77
+ `runuser -c`, `eval`, and `ssh host '…'`, including nested combinations. `cd` is
78
+ tracked within a command, and a `for VAR in <glob>` binding is followed into
79
+ `rm -rf "$VAR"`. Remote (`ssh`) layers only consider absolute targets, because a
80
+ remote relative path cannot be resolved against the local working directory.
11
81
 
12
82
  ## Install for Codewith
13
83
 
@@ -5,8 +5,15 @@ Codewith-native hook installed as `hooks run worktree-guard`.
5
5
  This hook is OSS-safe: optional Hasna CLIs are best-effort and missing CLIs fail open with concise warnings. Security gates only fail closed when a guarded commit/push scan runs successfully and finds possible secrets.
6
6
 
7
7
  It blocks scoped destructive shell operations and file-tool-like payloads when
8
- the resolved target threatens `~/.hasna`, configured workspace roots, Hasna
9
- division/scope roots, or active repo/worktree roots.
8
+ the resolved target threatens `/` or a system root (`/usr`, `/etc`, `/var`,
9
+ `/home`, …), `~/.hasna`, configured workspace roots, Hasna division/scope roots,
10
+ or active repo/worktree roots.
11
+
12
+ It shares its classifier with `pre-bash`, so it also blocks destructive targets
13
+ whose command substitution or variable expansion could collapse to empty —
14
+ `rm -rf "$(cmd)"/*` and `rm -rf "$VAR"/*`. See
15
+ [`hooks/pre-bash/README.md`](../pre-bash/README.md) for the full rules and the
16
+ recommended safe form.
10
17
 
11
18
  ## Canonical worktree path
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
5
5
  "type": "module",
6
6
  "bin": {