@hasna/hooks 0.8.0 → 0.9.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.
- package/README.md +33 -5
- package/bin/hooks-mcp.js +4862 -0
- package/bin/index.js +592 -170
- package/bin/serve.js +33 -10
- package/dist/db/index.d.ts +8 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +2870 -2709
- package/dist/lib/db-writer.d.ts +30 -1
- package/dist/lib/installer.d.ts +40 -1
- package/dist/lib/local-opt-in.d.ts +26 -0
- package/dist/lib/registration.d.ts +75 -0
- package/dist/lib/registry.d.ts +14 -0
- package/dist/lib/sync.d.ts +17 -1
- package/dist/sdk/index.d.ts +103 -0
- package/dist/sdk/index.js +1050 -0
- package/dist/storage.js +15 -1
- package/hooks/hook-agent-rules-version-check/README.md +1 -1
- package/hooks/hook-trash-guard/README.md +147 -0
- package/hooks/hook-trash-guard/package.json +12 -0
- package/hooks/hook-trash-guard/src/hook.ts +1142 -0
- package/package.json +10 -3
- package/scripts/validate-package.ts +31 -4
- package/hooks/codewith-native-common.test.ts +0 -1935
- package/hooks/hook-affected-tests/tsconfig.json +0 -25
- package/hooks/hook-agent-rules-version-check/src/hook.test.ts +0 -104
- package/hooks/hook-agent-rules-version-check/tsconfig.json +0 -25
- package/hooks/hook-announce-start/tsconfig.json +0 -25
- package/hooks/hook-announce-stop/tsconfig.json +0 -25
- package/hooks/hook-autoformat/tsconfig.json +0 -25
- package/hooks/hook-branchprotect/tsconfig.json +0 -25
- package/hooks/hook-checkbugs/tsconfig.json +0 -15
- package/hooks/hook-checkdocs/tsconfig.json +0 -15
- package/hooks/hook-checkfiles/tsconfig.json +0 -15
- package/hooks/hook-checklint/tsconfig.json +0 -15
- package/hooks/hook-checkpoint/tsconfig.json +0 -25
- package/hooks/hook-checksecurity/tsconfig.json +0 -15
- package/hooks/hook-checktasks/tsconfig.json +0 -20
- package/hooks/hook-checktests/tsconfig.json +0 -15
- package/hooks/hook-conflict-detect/tsconfig.json +0 -25
- package/hooks/hook-contextrefresh/tsconfig.json +0 -25
- package/hooks/hook-desktopnotify/tsconfig.json +0 -25
- package/hooks/hook-dm-inject/tsconfig.json +0 -25
- package/hooks/hook-envsetup/tsconfig.json +0 -25
- package/hooks/hook-failure-to-task/tsconfig.json +0 -25
- package/hooks/hook-filelock/tsconfig.json +0 -25
- package/hooks/hook-fleet-blockers-gate/src/hook.test.ts +0 -302
- package/hooks/hook-fleet-blockers-gate/tsconfig.json +0 -25
- package/hooks/hook-fleet-catchup/src/hook.test.ts +0 -156
- package/hooks/hook-fleet-catchup/tsconfig.json +0 -25
- package/hooks/hook-gitguard/tsconfig.json +0 -25
- package/hooks/hook-knowledge-context/src/hook.test.ts +0 -379
- package/hooks/hook-packageage/tsconfig.json +0 -25
- package/hooks/hook-permissionguard/tsconfig.json +0 -25
- package/hooks/hook-phonenotify/tsconfig.json +0 -25
- package/hooks/hook-precompact/tsconfig.json +0 -25
- package/hooks/hook-protectfiles/tsconfig.json +0 -25
- package/hooks/hook-scanoutput/src/hook.test.ts +0 -217
- package/hooks/hook-spiral-detector/src/hook.test.ts +0 -72
- package/hooks/hook-stylescheck/tsconfig.json +0 -25
- package/hooks/hook-typecheck-gate/tsconfig.json +0 -25
- package/hooks/hook-workspace-repos-guard/src/hook.test.ts +0 -466
- package/hooks/hook-workspace-repos-guard/tsconfig.json +0 -21
- package/hooks/mention-context/src/hook.test.ts +0 -68
package/dist/storage.js
CHANGED
|
@@ -458,6 +458,18 @@ function runRetention(db, days) {
|
|
|
458
458
|
import { Database } from "bun:sqlite";
|
|
459
459
|
import { existsSync as existsSync3, mkdirSync, cpSync } from "fs";
|
|
460
460
|
import { join as join3 } from "path";
|
|
461
|
+
function refuseLocalStore(message) {
|
|
462
|
+
localStoreRefusal = message;
|
|
463
|
+
}
|
|
464
|
+
function allowLocalStore() {
|
|
465
|
+
localStoreRefusal = null;
|
|
466
|
+
}
|
|
467
|
+
function isLocalStoreRefused() {
|
|
468
|
+
return localStoreRefusal !== null;
|
|
469
|
+
}
|
|
470
|
+
function localStoreRefusalMessage() {
|
|
471
|
+
return localStoreRefusal;
|
|
472
|
+
}
|
|
461
473
|
function resolveDataDir() {
|
|
462
474
|
const effective = getEffectiveDataRoot();
|
|
463
475
|
const oldDir = join3(getHomeDir(), ".hooks");
|
|
@@ -481,6 +493,8 @@ function ensureDir(dbPath) {
|
|
|
481
493
|
}
|
|
482
494
|
}
|
|
483
495
|
function getDb() {
|
|
496
|
+
if (localStoreRefusal !== null)
|
|
497
|
+
throw new Error(localStoreRefusal);
|
|
484
498
|
if (instance)
|
|
485
499
|
return instance;
|
|
486
500
|
const dbPath = getDbPath();
|
|
@@ -506,7 +520,7 @@ function getDb() {
|
|
|
506
520
|
}
|
|
507
521
|
return instance;
|
|
508
522
|
}
|
|
509
|
-
var instance = null;
|
|
523
|
+
var instance = null, localStoreRefusal = null;
|
|
510
524
|
var init_db = __esm(() => {
|
|
511
525
|
init_app_home();
|
|
512
526
|
init_migrations();
|
|
@@ -43,7 +43,7 @@ export HOOKS_FLEET_TIMEOUT_MS=500 # configs CLI timeout (default 5
|
|
|
43
43
|
|
|
44
44
|
## Requirements
|
|
45
45
|
|
|
46
|
-
- `configs` CLI (@hasna/configs) — optional; without it the hook falls back to cross-artifact consistency checking
|
|
46
|
+
- `configs` CLI (@hasna/instructions — the package that ships the `configs` bin) — optional; without it the hook falls back to cross-artifact consistency checking
|
|
47
47
|
|
|
48
48
|
## Event
|
|
49
49
|
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# trash-guard
|
|
2
|
+
|
|
3
|
+
Codewith-native hook installed as `hooks run trash-guard`.
|
|
4
|
+
|
|
5
|
+
PreToolUse guard for `rm` issued through the Bash tool. It rewrites the verb
|
|
6
|
+
into `@hasna/trash`'s guard subcommand — `<abs>/trash guard <same args>` — so
|
|
7
|
+
the delete lands in a trash store and stays recoverable. When there is nothing
|
|
8
|
+
to redirect to, it **refuses** the command.
|
|
9
|
+
|
|
10
|
+
## The decision, and why it fails the way it does
|
|
11
|
+
|
|
12
|
+
The block decision is **self-contained**: it never waits on the trash store, a
|
|
13
|
+
network, a credential or the `@hasna/trash` package. The binary is consulted
|
|
14
|
+
only as an opportunistic upgrade:
|
|
15
|
+
|
|
16
|
+
| `trash` on PATH | verdict |
|
|
17
|
+
|---|---|
|
|
18
|
+
| present | `permissionDecision: "allow"` + `updatedInput` rewriting `rm` to `<abs>/trash guard` |
|
|
19
|
+
| absent | `permissionDecision: "deny"` + a reason telling the agent to install `@hasna/trash` |
|
|
20
|
+
|
|
21
|
+
There is no third row. It degrades **redirect → block**, never
|
|
22
|
+
**redirect → allow**: a delete this hook cannot redirect is never silently run.
|
|
23
|
+
|
|
24
|
+
The rewrite emits a **complete** `tool_input` — every key the model supplied,
|
|
25
|
+
plus `command`, `description`, `timeout` and `run_in_background`, plus
|
|
26
|
+
`dangerouslyDisableSandbox` when it was present. That care is deliberate: the
|
|
27
|
+
harness falls back to the **original** tool input when `updatedInput` is
|
|
28
|
+
missing or empty, so a partial rewrite would run the raw `rm`. The hook
|
|
29
|
+
re-scans the rewritten text before allowing it, and if the re-scan still finds
|
|
30
|
+
a live delete verb it denies instead. A command containing both an owned `rm`
|
|
31
|
+
and one handed to another guard is refused rather than partially rewritten.
|
|
32
|
+
|
|
33
|
+
## What it rewrites
|
|
34
|
+
|
|
35
|
+
Only `rm`'s own grammar, and only in **command position**: the verb must be the
|
|
36
|
+
command, after any assignment prefixes (`FOO=1 rm …`), shell keywords, and
|
|
37
|
+
recognized wrappers (`sudo`, `doas`, `env`, `nice`, `ionice`, `stdbuf`, `time`,
|
|
38
|
+
`timeout`, `nohup`, `setsid`, `command`, `builtin`, `exec`). Quoting, `~`,
|
|
39
|
+
`$HOME` and `${HOME}` spellings, `--`, redirections and every other byte of the
|
|
40
|
+
command are preserved.
|
|
41
|
+
|
|
42
|
+
Wrappers that take positional arguments are handled (`timeout 5 rm -rf x`
|
|
43
|
+
rewrites the `rm`, not the duration).
|
|
44
|
+
|
|
45
|
+
## What it refuses
|
|
46
|
+
|
|
47
|
+
- `rmdir`, `unlink`, `shred` — their flags and semantics are not `rm`'s.
|
|
48
|
+
- `git rm` (without `--cached`), `git clean` — they delete outside `rm`.
|
|
49
|
+
- `find -delete`, `find -exec rm …`, `xargs rm` — the delete does not run
|
|
50
|
+
through a verb this hook can swap.
|
|
51
|
+
- A delete inside a command substitution (`$(rm …)`, backticks).
|
|
52
|
+
- `busybox rm` / `toybox rm` — a different applet, not GNU `rm`.
|
|
53
|
+
- Shells and opaque command strings (`sh -c 'rm …'`, `eval`, `su -c …`).
|
|
54
|
+
- An unparseable command (unterminated quote, substitution or here-document)
|
|
55
|
+
that mentions a delete verb.
|
|
56
|
+
- The protected class, below.
|
|
57
|
+
|
|
58
|
+
Each refusal carries a reason and a way forward: re-run the delete as
|
|
59
|
+
`rm -- <path>`, which this hook intercepts and redirects.
|
|
60
|
+
|
|
61
|
+
## The protected class
|
|
62
|
+
|
|
63
|
+
Refused outright, never redirected (plan §15 decision 11.3):
|
|
64
|
+
|
|
65
|
+
- the filesystem root and the system roots `pre-bash`'s protected-path rules
|
|
66
|
+
already name (`/etc`, `/usr`, `/bin`, `/home`, `/var`, …) — the root itself,
|
|
67
|
+
or any ancestor of it;
|
|
68
|
+
- the home directory itself (`~`, `$HOME`, `${HOME}`);
|
|
69
|
+
- `~/.hasna`, `~/.ssh`, `~/.aws` — the state and credential stores, including
|
|
70
|
+
everything under them.
|
|
71
|
+
|
|
72
|
+
These are the catastrophic cases where "move it to trash" is not an acceptable
|
|
73
|
+
answer. Everything else this hook owns is rewritten, not refused.
|
|
74
|
+
|
|
75
|
+
## Scope: what it deliberately does NOT own
|
|
76
|
+
|
|
77
|
+
Deletes under the protected repo-checkout roots `$HOME/.hasna/repos/clones` and
|
|
78
|
+
`$HOME/workspace/repos` belong to **`workspace-repos-guard`**, which blocks
|
|
79
|
+
every delete under them at any depth. This hook does not restate that policy:
|
|
80
|
+
it recognizes the boundary and abstains, so the other hook's decision stands.
|
|
81
|
+
A command that mixes a delete inside those roots with one outside is refused,
|
|
82
|
+
because a partial rewrite would leave one of them unredirected.
|
|
83
|
+
|
|
84
|
+
## Conflict discipline
|
|
85
|
+
|
|
86
|
+
`trash-guard` declares `rewritesInput: true` in the registry. Two PreToolUse
|
|
87
|
+
hooks on overlapping matchers that both rewrite the tool input are a silent
|
|
88
|
+
data-loss hazard: the harness keeps one rewrite (last writer wins), so the
|
|
89
|
+
losing hook's guard disappears with no error. Installing a second such hook is
|
|
90
|
+
therefore **refused**, and `hooks doctor` reports more than one input-rewriting
|
|
91
|
+
hook on an overlapping matcher. Other overlaps still install with the usual
|
|
92
|
+
advisory warning.
|
|
93
|
+
|
|
94
|
+
The registration is written with `timeout: 5`. The harness's documented
|
|
95
|
+
default is 600s, and a hook that times out **does not block** — only a verdict
|
|
96
|
+
already on stdout does. Five seconds is several orders of magnitude above this
|
|
97
|
+
hook's measured cost (a bounded lexer and a 500 ms identity check against a verified package binary).
|
|
98
|
+
|
|
99
|
+
## Known limitations
|
|
100
|
+
|
|
101
|
+
The guard is a best-effort **text** classifier, not an execution sandbox.
|
|
102
|
+
|
|
103
|
+
- **Variable indirection is undetectable.** `R=...; rm -rf $R`, a loop over
|
|
104
|
+
computed paths, or a script downloaded and run at runtime cannot be
|
|
105
|
+
classified before the shell expands it. This is the same limitation
|
|
106
|
+
`workspace-repos-guard` documents, and it is inherent to inspecting the
|
|
107
|
+
command string rather than the syscall. An operand that is a variable or a
|
|
108
|
+
glob is still **rewritten** (the `trash` binary classifies what the shell
|
|
109
|
+
actually expands to), but it cannot be checked against the protected class
|
|
110
|
+
here.
|
|
111
|
+
- **A hook only ever sees the agent's own tool calls.** It cannot stop a file
|
|
112
|
+
being deleted by another process, by a build tool, by a script the agent
|
|
113
|
+
runs, or by `unlink(2)` called directly. Bash `rm` is rewritten, and native `apply_patch` whole-file deletion is refused with an instruction to call Trash first. Ordinary `Write`/`Edit` pre-image capture is not implemented.
|
|
114
|
+
- **Nested and generated commands escape it.** `bash -c`, `eval`, `make`,
|
|
115
|
+
`npm run`, a `Dockerfile`, a heredoc-fed interpreter: the hook can only see
|
|
116
|
+
that a shell string mentions a delete verb and refuse it, never redirect
|
|
117
|
+
inside it.
|
|
118
|
+
- **Threat model: accident, not adversary.** A hostile same-user agent can
|
|
119
|
+
remove the hook, edit the store, or call `unlink(2)` directly; nothing here
|
|
120
|
+
holds against that.
|
|
121
|
+
- **Fail-closed only for deletes.** On an internal error the hook denies a
|
|
122
|
+
command that mentions a delete verb and stays silent otherwise, so a guard
|
|
123
|
+
defect cannot wedge unrelated work. Fail-closed cannot be guaranteed where
|
|
124
|
+
the harness itself never delivers the hook input.
|
|
125
|
+
|
|
126
|
+
## Configuration
|
|
127
|
+
|
|
128
|
+
None. The home directory comes from `os.homedir()`; the trash binary is
|
|
129
|
+
resolved by scanning `PATH` for a verified `@hasna/trash` package and its `--identity` protocol and rewriting to the
|
|
130
|
+
absolute path found, so the rewritten command does not depend on `PATH` again.
|
|
131
|
+
|
|
132
|
+
## Native Codex and Claude
|
|
133
|
+
|
|
134
|
+
The guard emits their documented `PreToolUse` decision contract, including a
|
|
135
|
+
complete `updatedInput.command`. No-op hooks emit no output. Codex unified exec
|
|
136
|
+
also matches `Bash`; a `Delete File` patch must use `trash put` first. Configure
|
|
137
|
+
Codex in `~/.codex/hooks.json` with matcher
|
|
138
|
+
`^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$` and the command
|
|
139
|
+
`hooks run trash-guard`, with hook timeout 5 seconds. Preserve other registrations
|
|
140
|
+
and refuse a second overlapping input-rewriting hook. Claude registration uses
|
|
141
|
+
`hooks install trash-guard --target claude`. The command rewrite defaults to
|
|
142
|
+
600 seconds for upload and verification, preserving an explicitly supplied timeout.
|
|
143
|
+
|
|
144
|
+
After installation, prove the native harness actually executes the rewritten
|
|
145
|
+
command using a disposable file and a hosted entry/restore receipt; a hook JSON
|
|
146
|
+
response alone does not prove interception. Other harnesses can use the Trash
|
|
147
|
+
CLI/MCP directly; this hook does not claim their native interception.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "trash-guard",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Codewith-native Trash Guard hook for @hasna/hooks",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/hook.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"typecheck": "tsc --noEmit"
|
|
9
|
+
},
|
|
10
|
+
"author": "Hasna",
|
|
11
|
+
"license": "Apache-2.0"
|
|
12
|
+
}
|