@hasna/hooks 0.9.6 → 0.10.1
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.
|
@@ -534,6 +534,16 @@ function resolveLiteralTarget(raw, cwd, home) {
|
|
|
534
534
|
function isUnder(target, root) {
|
|
535
535
|
return target === root || target.startsWith(`${root}${sep}`);
|
|
536
536
|
}
|
|
537
|
+
function isScratchpadContent(target, home) {
|
|
538
|
+
const prefix = `${join(home, ".hasna", "scratchpad", "scratch")}${sep}`;
|
|
539
|
+
if (!target.startsWith(prefix))
|
|
540
|
+
return false;
|
|
541
|
+
const [session, firstChild] = target.slice(prefix.length).split(sep);
|
|
542
|
+
if (!session || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(session) || session.endsWith(".") || !firstChild)
|
|
543
|
+
return false;
|
|
544
|
+
const control = firstChild.normalize("NFKC").toLowerCase();
|
|
545
|
+
return control !== "meta.json" && control !== ".lock";
|
|
546
|
+
}
|
|
537
547
|
function protectedTargetReason(target, home) {
|
|
538
548
|
if (target === home) {
|
|
539
549
|
return `\`${target}\` is the home directory itself, which is never deleted or trashed. Delete a specific file or directory inside it instead.`;
|
|
@@ -545,6 +555,8 @@ function protectedTargetReason(target, home) {
|
|
|
545
555
|
}
|
|
546
556
|
for (const suffix of PROTECTED_HOME_TREES) {
|
|
547
557
|
const root = join(home, suffix);
|
|
558
|
+
if (suffix === ".hasna" && isScratchpadContent(target, home))
|
|
559
|
+
continue;
|
|
548
560
|
if (!isUnder(target, root) && !isUnder(root, target))
|
|
549
561
|
continue;
|
|
550
562
|
return `\`${target}\` is the protected \`~/${suffix}\` path (or contains it), which is never deleted or trashed. Delete a specific file inside it, and only when you are sure.`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# trash-guard
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Native safety hook installed with `hooks safety install trash-guard`.
|
|
4
4
|
|
|
5
5
|
PreToolUse guard for `rm` issued through the Bash tool. It rewrites the verb
|
|
6
6
|
into `@hasna/trash`'s guard subcommand — `<abs>/trash guard <same args>` — so
|
|
@@ -60,14 +60,24 @@ Each refusal carries a reason and a way forward: re-run the delete as
|
|
|
60
60
|
|
|
61
61
|
## The protected class
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
Literal protected paths are refused rather than redirected:
|
|
64
64
|
|
|
65
65
|
- the filesystem root and the system roots `pre-bash`'s protected-path rules
|
|
66
66
|
already name (`/etc`, `/usr`, `/bin`, `/home`, `/var`, …) — the root itself,
|
|
67
67
|
or any ancestor of it;
|
|
68
68
|
- the home directory itself (`~`, `$HOME`, `${HOME}`);
|
|
69
|
-
- `~/.hasna`, `~/.ssh`, `~/.aws` — the state and credential stores,
|
|
70
|
-
|
|
69
|
+
- `~/.hasna`, `~/.ssh`, `~/.aws` — the state and credential stores, with the
|
|
70
|
+
Scratchpad content exception below.
|
|
71
|
+
|
|
72
|
+
An owned content path beneath
|
|
73
|
+
`~/.hasna/scratchpad/scratch/<session-id>/` can be redirected to Trash. The
|
|
74
|
+
session identifier must match `[a-z0-9][a-z0-9._-]{0,63}` and cannot end in a
|
|
75
|
+
dot. The session root and its ancestors remain protected. Session-level
|
|
76
|
+
`meta.json` and `.lock`, their descendants, and their Unicode-normalized,
|
|
77
|
+
case-insensitive equivalents also remain protected; a nested
|
|
78
|
+
`notes/meta.json` is ordinary content. This routing exception does not grant
|
|
79
|
+
permission to remove another session's files. Variable and glob limitations
|
|
80
|
+
below still apply.
|
|
71
81
|
|
|
72
82
|
These are the catastrophic cases where "move it to trash" is not an acceptable
|
|
73
83
|
answer. Everything else this hook owns is rewritten, not refused.
|
|
@@ -91,10 +101,10 @@ therefore **refused**, and `hooks doctor` reports more than one input-rewriting
|
|
|
91
101
|
hook on an overlapping matcher. Other overlaps still install with the usual
|
|
92
102
|
advisory warning.
|
|
93
103
|
|
|
94
|
-
The
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
104
|
+
The native safety installer uses a bounded supervisor and a ten-second native
|
|
105
|
+
registration timeout. The supervisor turns startup, integrity, input, worker
|
|
106
|
+
and deadline failures into a blocking verdict. A harness that never delivers
|
|
107
|
+
the event cannot be protected by the hook.
|
|
98
108
|
|
|
99
109
|
## Known limitations
|
|
100
110
|
|
|
@@ -134,11 +144,11 @@ absolute path found, so the rewritten command does not depend on `PATH` again.
|
|
|
134
144
|
The guard emits their documented `PreToolUse` decision contract, including a
|
|
135
145
|
complete `updatedInput.command`. No-op hooks emit no output. Codex unified exec
|
|
136
146
|
also matches `Bash`; a `Delete File` patch must use `trash put` first. Configure
|
|
137
|
-
Codex
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
and refuse a second overlapping input-rewriting hook. Claude
|
|
141
|
-
`hooks install trash-guard --target claude`. The command rewrite defaults to
|
|
147
|
+
Codex using `hooks safety install trash-guard --target codex` and install
|
|
148
|
+
`workspace-repos-guard` through the same safety command. The dedicated bundled
|
|
149
|
+
runner does not require Hooks registry authentication. Preserve other
|
|
150
|
+
registrations and refuse a second overlapping input-rewriting hook. Claude
|
|
151
|
+
uses `hooks safety install trash-guard --target claude`. The command rewrite defaults to
|
|
142
152
|
600 seconds for upload and verification, preserving an explicitly supplied timeout.
|
|
143
153
|
|
|
144
154
|
After installation, prove the native harness actually executes the rewritten
|
|
@@ -627,6 +627,17 @@ function isUnder(target: string, root: string): boolean {
|
|
|
627
627
|
return target === root || target.startsWith(`${root}${sep}`);
|
|
628
628
|
}
|
|
629
629
|
|
|
630
|
+
/** Literal execution content only; this path shape does not confer task ownership. */
|
|
631
|
+
function isScratchpadContent(target: string, home: string): boolean {
|
|
632
|
+
const prefix = `${join(home, ".hasna", "scratchpad", "scratch")}${sep}`;
|
|
633
|
+
if (!target.startsWith(prefix)) return false;
|
|
634
|
+
const [session, firstChild] = target.slice(prefix.length).split(sep);
|
|
635
|
+
if (!session || !/^[a-z0-9][a-z0-9._-]{0,63}$/.test(session) || session.endsWith(".") || !firstChild) return false;
|
|
636
|
+
// Session metadata and its mutation lock are app control state, not scratch.
|
|
637
|
+
const control = firstChild.normalize("NFKC").toLowerCase();
|
|
638
|
+
return control !== "meta.json" && control !== ".lock";
|
|
639
|
+
}
|
|
640
|
+
|
|
630
641
|
/**
|
|
631
642
|
* Why `target` belongs to the protected class that is refused rather than
|
|
632
643
|
* redirected, or null when it is an ordinary path. Matches both directions:
|
|
@@ -647,6 +658,7 @@ export function protectedTargetReason(target: string, home: string): string | nu
|
|
|
647
658
|
}
|
|
648
659
|
for (const suffix of PROTECTED_HOME_TREES) {
|
|
649
660
|
const root = join(home, suffix);
|
|
661
|
+
if (suffix === ".hasna" && isScratchpadContent(target, home)) continue;
|
|
650
662
|
// Tree mode, like `pre-bash`'s ~/.hasna rule: the store itself and
|
|
651
663
|
// everything under it.
|
|
652
664
|
if (!isUnder(target, root) && !isUnder(root, target)) continue;
|
package/package.json
CHANGED