@hasna/hooks 0.10.0 → 0.10.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.
@@ -0,0 +1,8 @@
1
+ /** The owning Skills writer commits Claude settings and discovery together.
2
+ * A missing policy permits ordinary standalone installation. Any other refusal
3
+ * propagates before settings are written; it never selects a fallback writer.
4
+ */
5
+ export declare function coordinateClaudeSettings(before: string | null, replacement: string, options: {
6
+ home: string;
7
+ dataDir?: string;
8
+ }): boolean;
package/dist/sdk/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- // ../contracts/dist/client/transport.js
2
+ // ../hooks0102-standalone-lock/node_modules/@hasna/contracts/dist/client/transport.js
3
3
  import { isIP } from "net";
4
4
  import { spawnSync } from "child_process";
5
5
  import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
@@ -1,6 +1,6 @@
1
1
  # trash-guard
2
2
 
3
- Codewith-native hook installed as `hooks run trash-guard`.
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
- Refused outright, never redirected (plan §15 decision 11.3):
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, including
70
- everything under them.
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 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).
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 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
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.10.0",
3
+ "version": "0.10.2",
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": {
@@ -60,6 +60,7 @@
60
60
  "@hasna/contracts": "~1.2.1"
61
61
  },
62
62
  "dependencies": {
63
+ "@hasna/skills": "0.9.14",
63
64
  "@hasna/events": "^0.1.16",
64
65
  "@hasna/secrets": "0.4.2",
65
66
  "@modelcontextprotocol/sdk": "^1.26.0",