@hasna/hooks 0.6.4 → 0.6.6

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,42 @@
1
+ /**
2
+ * Hook child-process environment — a fixed allowlist plus a name-based deny
3
+ * list (P1-1 env isolation).
4
+ *
5
+ * A hook executes third-party bytes inside the agent's session. Passing the
6
+ * parent's process.env wholesale hands every credential the agent can reach
7
+ * to arbitrary code. The child gets:
8
+ *
9
+ * 1. a fixed allowlist of non-secret session variables (PATH, HOME, LANG,
10
+ * TZ, SHELL, TERM, USER, PWD);
11
+ * 2. non-secret HOOKS_* projections of the parent's HASNA_HOOKS_* config
12
+ * (data dir, DB path, lock path, config path) so in-process DB/config
13
+ * resolution inside the hook still finds the store the parent configured;
14
+ * 3. caller-supplied extras, filtered through the same deny list.
15
+ *
16
+ * Denial is NAME-BASED by design. A value-shape test is explicitly rejected
17
+ * as an acceptability test: a value test would have to miss something for a
18
+ * credential to leak, so the deny list is the documented prefix/suffix set
19
+ * below. The deny list strips:
20
+ *
21
+ * suffix: KEY, TOKEN, SECRET, PASSWORD, PASSWD, CREDENTIAL, CREDENTIALS,
22
+ * URL, URI (a URL/URI-bearing variable can embed credentials)
23
+ * prefix: HASNA_, AWS_, AZURE_, GCP_, VAULT_, GOOGLE_, OPENAI_,
24
+ * ANTHROPIC_, POSTGRES_, MYSQL_, REDIS_, MONGO_, MEMENTOS_
25
+ * contains: DATABASE_URL (any variant)
26
+ *
27
+ * Names that merely resemble credentials (e.g. HOOKS_DATA_DIR) survive; a
28
+ * path is not a credential.
29
+ */
30
+ /**
31
+ * True when a variable NAME is credential-bearing and must never reach a
32
+ * hook child process. Case-insensitive.
33
+ */
34
+ export declare function isDeniedEnvName(name: string): boolean;
35
+ /**
36
+ * Build the sanitized environment for a hook child process.
37
+ *
38
+ * `source` is the parent's environment; `extra` is any caller-supplied env
39
+ * the hook may need. The deny list applies to BOTH — a caller cannot
40
+ * reintroduce a credential-bearing name.
41
+ */
42
+ export declare function buildHookEnv(source: Record<string, string | undefined>, extra?: Record<string, string | undefined>): Record<string, string>;
@@ -9,6 +9,13 @@ export declare const manifestSchema: z.ZodObject<{
9
9
  description: z.ZodOptional<z.ZodString>;
10
10
  events: z.ZodArray<z.ZodString, "many">;
11
11
  script: z.ZodString;
12
+ /**
13
+ * Explicit script-kind discriminator (P2-14): "inline" means the script
14
+ * value IS the hook body; "file" means it names a relative path. When
15
+ * absent, the legacy newline heuristic applies (documented fallback for
16
+ * older manifests — a value with a newline is inline, otherwise a path).
17
+ */
18
+ script_kind: z.ZodOptional<z.ZodEnum<["inline", "file"]>>;
12
19
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
20
  timeout_ms: z.ZodOptional<z.ZodNumber>;
14
21
  }, "strip", z.ZodTypeAny, {
@@ -17,6 +24,7 @@ export declare const manifestSchema: z.ZodObject<{
17
24
  name: string;
18
25
  script: string;
19
26
  description?: string | undefined;
27
+ script_kind?: "inline" | "file" | undefined;
20
28
  args?: string[] | undefined;
21
29
  timeout_ms?: number | undefined;
22
30
  }, {
@@ -25,6 +33,7 @@ export declare const manifestSchema: z.ZodObject<{
25
33
  name: string;
26
34
  script: string;
27
35
  description?: string | undefined;
36
+ script_kind?: "inline" | "file" | undefined;
28
37
  args?: string[] | undefined;
29
38
  timeout_ms?: number | undefined;
30
39
  }>;
@@ -52,10 +61,23 @@ export declare class ScriptContainmentError extends Error {
52
61
  * root, then call assertContainedReal.
53
62
  */
54
63
  export declare function assertContained(target: string, root: string, raw?: string): string;
64
+ export declare function isInlineScript(manifest: HookManifest): boolean;
65
+ /**
66
+ * Resolve a manifest's script to its RELATIVE target path under the hook
67
+ * directory. Shared by every install path (P2-14 / P1-2 round 2): the explicit
68
+ * script_kind discriminator wins; without it the legacy heuristic applies (a
69
+ * value containing a newline is inline content, otherwise a relative path).
70
+ * This is the ONE decision point — registry sync, exact-pin fetch and custom
71
+ * installs all call it, so a one-line inline manifest can never be mistaken
72
+ * for a path again.
73
+ */
74
+ export declare function scriptRelFor(manifest: HookManifest): string;
55
75
  /**
56
76
  * Resolve a manifest's script to (relative path, content).
57
- * A script value containing a newline is inline content; anything else is a
58
- * relative path resolved against the manifest directory.
77
+ * The explicit script_kind discriminator wins (P2-14); without it the
78
+ * legacy heuristic applies: a script value containing a newline is inline
79
+ * content, anything else is a relative path resolved against the manifest
80
+ * directory.
59
81
  */
60
82
  export declare function resolveScript(manifest: HookManifest, manifestDir: string): {
61
83
  path: string;
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Event-safe redaction for hook event rows (P1-3).
3
+ *
4
+ * tool_input / error / metadata were stored verbatim (truncated only) and
5
+ * returned by the MCP log tools and `hooks log`. A tool call that carries a
6
+ * credential (a Bash command embedding a token, a tool_input object with an
7
+ * api_key field, an error echoing a secret) persisted it to disk and served
8
+ * it back.
9
+ *
10
+ * The projection is applied:
11
+ * - at WRITE time, in db-writer.writeHookEvent, so nothing sensitive is
12
+ * persisted locally or pushed to the remote sync store;
13
+ * - at READ time, on every MCP log response and `hooks log` output, so
14
+ * rows stored by older versions (which wrote verbatim) are redacted
15
+ * before they reach a reader (truncate-on-read projection; documented
16
+ * retention note in CHANGELOG.md — no destructive backfill command).
17
+ *
18
+ * Two mechanisms:
19
+ * 1. JSON-aware: when the payload parses, values under secret-typed keys
20
+ * are replaced with [REDACTED] (covers tool_input objects and metadata).
21
+ * 2. Shape-based: known credential shapes anywhere in the text are
22
+ * replaced (covers inline tokens in command strings and error text).
23
+ *
24
+ * Redaction is intentionally conservative: these fields are internal
25
+ * observability data, so over-redaction costs little and under-redaction
26
+ * costs a leaked credential.
27
+ */
28
+ export declare function redactText(value: string): string;
29
+ export declare function redactValue(value: unknown): unknown;
30
+ /**
31
+ * Project one event payload (tool_input / error / metadata text) to its
32
+ * event-safe form. Null passthrough, JSON-aware, always shape-scrubbed.
33
+ */
34
+ export declare function redactEventPayload(text: string | null | undefined): string | null;
35
+ /**
36
+ * Read projection for a full hook_events row: the fixed-field allowlist is
37
+ * the row schema; tool_input, error and metadata pass through
38
+ * redactEventPayload, everything else is returned unchanged.
39
+ */
40
+ export declare function projectEventRowForRead(row: Record<string, unknown>): Record<string, unknown>;
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared semver validation (P2-10).
3
+ *
4
+ * Every surface that accepts a hook version — manifest validation, publish,
5
+ * the serve and worker artifact routes — must accept and reject the SAME
6
+ * set. Previously the manifest accepted "1.2.3-beta" (unanchored regex)
7
+ * while the artifact routes demanded a bare `\d+\.\d+\.\d+` at the end of
8
+ * the path, so a pinned prerelease install 404'd. This is the one pattern:
9
+ * full semver with optional prerelease and build metadata.
10
+ */
11
+ export declare const SEMVER_PATTERN: RegExp;
12
+ /**
13
+ * A version segment used inside a URL path. Prerelease and build metadata
14
+ * can contain '+' (build metadata) which must be percent-encoded in URLs;
15
+ * the route regex accepts the encoded and decoded forms.
16
+ */
17
+ export declare const SEMVER_PATH_SEGMENT = "[0-9A-Za-z.+_-]+";
18
+ export declare function isValidSemver(value: string): boolean;
19
+ export declare function semverError(value: string): string;
@@ -21,6 +21,13 @@ export interface LockEntry {
21
21
  version: string;
22
22
  sha256: string;
23
23
  source: string;
24
+ /**
25
+ * True when the pin came from an explicit `hooks install/update
26
+ * <name>@<version>` (P2-9): an explicit older pin is preserved across
27
+ * syncs instead of being silently bumped to the latest. Absent/undefined
28
+ * pins are ordinary sync-maintained pins and follow the latest.
29
+ */
30
+ pinned?: boolean;
24
31
  }
25
32
  export interface LockFile {
26
33
  hooks: Record<string, LockEntry>;
@@ -39,6 +46,15 @@ export declare function upsertHookRecord(db: Database, record: {
39
46
  last_verified_at?: string | null;
40
47
  }): void;
41
48
  export declare function removeHookRecord(db: Database, name: string): boolean;
49
+ /**
50
+ * Raised when hooks.lock is present but malformed. P1-9: a malformed lock
51
+ * used to degrade to {hooks:{}} — a fail-open that would let the next sync
52
+ * re-trust hooks as if nothing had been pinned. It now fails hard with a
53
+ * repair message; no execution path treats a broken lock as an empty store.
54
+ */
55
+ export declare class LockFileError extends Error {
56
+ constructor(message: string);
57
+ }
42
58
  export declare function readLock(): LockFile;
43
59
  export declare function writeLock(lock: LockFile): string;
44
60
  export declare function setPinnedHook(name: string, entry: LockEntry): string;
@@ -6,6 +6,7 @@
6
6
  * failure mid-sync leaves the local store untouched. Local-only hooks are
7
7
  * never deleted by a remote sync.
8
8
  */
9
+ import { parseManifest } from "./manifest.js";
9
10
  export interface SyncDiff {
10
11
  added: string[];
11
12
  updated: string[];
@@ -24,15 +25,62 @@ export interface ArtifactResponse {
24
25
  description?: string;
25
26
  events: string[];
26
27
  script: string;
28
+ script_kind?: "inline" | "file";
27
29
  args?: string[];
28
30
  timeout_ms?: number;
29
31
  };
30
32
  script: string;
31
33
  }
32
- export declare function planSync(): Promise<SyncPlan>;
34
+ interface RemoteLock {
35
+ hooks: Record<string, {
36
+ version: string;
37
+ sha256: string;
38
+ source: string;
39
+ versions?: string[];
40
+ }>;
41
+ }
42
+ export declare function planSync(options?: {
43
+ dryRun?: boolean;
44
+ }): Promise<SyncPlan>;
33
45
  export declare function syncHooks(options?: {
34
46
  dryRun?: boolean;
35
47
  }): Promise<SyncPlan>;
48
+ /**
49
+ * One fully-validated artifact ready to be committed. Nothing has touched
50
+ * the store at this point: every fetch, sha check and manifest parse has
51
+ * already passed, so a failure inside staging leaves the store unchanged.
52
+ */
53
+ export interface StagedSyncArtifact {
54
+ name: string;
55
+ version: string;
56
+ sha256: string;
57
+ source: string;
58
+ manifest: ReturnType<typeof parseManifest>;
59
+ scriptContent: string;
60
+ scriptRel: string;
61
+ }
62
+ /**
63
+ * P1-9 stage phase: fetch + validate ALL artifacts before anything is
64
+ * written. A failure here (network, sha mismatch, malformed manifest,
65
+ * containment violation) throws with the store untouched.
66
+ */
67
+ export declare function stageSyncArtifacts(apiUrl: string, names: string[], remoteLock: RemoteLock): Promise<StagedSyncArtifact[]>;
68
+ /**
69
+ * P1-9 commit phase, ordered so a mid-commit failure cannot leave a partial
70
+ * store that reads as trusted:
71
+ *
72
+ * Phase A — write ALL hook files and verify their hashes. A failure here
73
+ * leaves lock and DB untouched; files written without pins are
74
+ * refused at run time (fail-closed trust), never silently
75
+ * trusted.
76
+ * Phase B — write ALL lock pins in ONE atomic write (temp + rename).
77
+ * Phase C — DB records in one transaction.
78
+ *
79
+ * A failure between A and B leaves the DB untouched and the old pins in
80
+ * place; a failure between B and C leaves old DB records, which take
81
+ * precedence in checkScriptHash and refuse the new bytes until re-sync.
82
+ */
83
+ export declare function commitSyncArtifacts(staged: StagedSyncArtifact[], apiUrl: string, remoteLock: RemoteLock): Promise<void>;
36
84
  export interface PinnedHookInstall {
37
85
  name: string;
38
86
  version: string;
@@ -44,11 +92,14 @@ export interface PinnedHookInstall {
44
92
  }
45
93
  /**
46
94
  * Fetch one exact hook version from the remote registry, verify its sha
47
- * against the remote lock, write it to the custom store, and pin it.
95
+ * against the remote lock (or the exact-version header for older versions),
96
+ * write it to the custom store, and pin it.
48
97
  * Powers `hooks install <name>@<version>` / `hooks update <name>@<version>`
49
98
  * (QA-2 finding: pinned-version install/update was unsupported).
50
99
  *
51
- * Requires an api_url (remote registry). The sha check is against the remote
52
- * lock — the exact version named by the user, never a mutable latest.
100
+ * Requires an api_url (remote registry). P1-4: the exact version named by
101
+ * the user is fetched from the versioned registry — older-than-latest pins
102
+ * are first-class, never rejected as "not the latest".
53
103
  */
54
104
  export declare function fetchPinnedHook(name: string, version: string, apiUrl: string): Promise<PinnedHookInstall>;
105
+ export {};
package/dist/serve.d.ts CHANGED
@@ -15,6 +15,7 @@ export interface CatalogEntry {
15
15
  events: string[];
16
16
  description: string;
17
17
  source: string;
18
+ versions: string[];
18
19
  }
19
20
  export interface ArtifactPayload {
20
21
  manifest: {
@@ -23,6 +24,7 @@ export interface ArtifactPayload {
23
24
  description?: string;
24
25
  events: string[];
25
26
  script: string;
27
+ script_kind?: "inline" | "file";
26
28
  args?: string[];
27
29
  timeout_ms?: number;
28
30
  };
@@ -32,5 +34,4 @@ export declare function handleServeRequest(req: Request, apiKey: string | undefi
32
34
  export declare function startServeServer(options: {
33
35
  port?: number;
34
36
  host?: string;
35
- apiKey?: string;
36
37
  }): ReturnType<typeof Bun.serve>;
package/dist/storage.js CHANGED
@@ -399,6 +399,60 @@ var init_db = __esm(() => {
399
399
  init_legacy_import();
400
400
  });
401
401
 
402
+ // src/lib/redact.ts
403
+ function redactText(value) {
404
+ let out = value;
405
+ for (const pattern of SECRET_SHAPES) {
406
+ out = out.replace(pattern, REDACTED);
407
+ }
408
+ return out;
409
+ }
410
+ function redactValue(value) {
411
+ if (typeof value === "string")
412
+ return redactText(value);
413
+ if (Array.isArray(value))
414
+ return value.map(redactValue);
415
+ if (value !== null && typeof value === "object") {
416
+ const out = {};
417
+ for (const [key, child] of Object.entries(value)) {
418
+ out[key] = SECRET_KEY_NAMES.test(key) ? REDACTED : redactValue(child);
419
+ }
420
+ return out;
421
+ }
422
+ return value;
423
+ }
424
+ function redactEventPayload(text) {
425
+ if (text === null || text === undefined)
426
+ return null;
427
+ try {
428
+ const parsed = JSON.parse(text);
429
+ return redactText(JSON.stringify(redactValue(parsed)));
430
+ } catch {
431
+ return redactText(text);
432
+ }
433
+ }
434
+ var REDACTED = "[REDACTED]", SECRET_KEY_NAMES, SECRET_SHAPES;
435
+ var init_redact = __esm(() => {
436
+ SECRET_KEY_NAMES = /^(key|keys|token|tokens|secret|secrets|password|passwd|api[_-]?key|apikey|access[_-]?key|private[_-]?key|client[_-]?secret|refresh[_-]?token|authorization|auth|cookie|credential|credentials|session[_-]?id|otp|mfa[_-]?code|two[_-]?fa[_-]?code|verification[_-]?code|database_url|db_url|connection_string|dsn)$/i;
437
+ SECRET_SHAPES = [
438
+ /\bsk-(?:proj|svc|ant)-[A-Za-z0-9_-]{10,}/g,
439
+ /\bsk-[A-Za-z0-9]{16,}/g,
440
+ /\btpe_[A-Za-z0-9]{8,}/g,
441
+ /\b(?:rk|sk)_(?:live|test)_[A-Za-z0-9]{8,}/g,
442
+ /\bgithub_pat_[A-Za-z0-9_]{20,}/g,
443
+ /gh[pousr]_[A-Za-z0-9]{20,}/g,
444
+ /xox[baprs]-[A-Za-z0-9-]{20,}/g,
445
+ /AKIA[0-9A-Z]{16}/g,
446
+ /ASIA[0-9A-Z]{16}/g,
447
+ /eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
448
+ /-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----/g,
449
+ /-----BEGIN OPENSSH PRIVATE KEY-----/g,
450
+ /\bBearer\s+[A-Za-z0-9._~+/=-]{6,}/g,
451
+ /\b[a-z][a-z0-9+.-]*:\/\/[^/\s:@]+:[^/\s@]+@/gi,
452
+ /\b(?:password|passwd|pwd|token|api[_-]?key|secret|access[_-]?key|client[_-]?secret|authorization|auth|credential|database_url|db_url|connection_string|dsn)\s*[:=]\s*(?:"[^"]*"|'[^']*'|[^\s,;"']{6,})/gi
453
+ ];
454
+ });
455
+
402
456
  // src/db/storage-sync.ts
403
457
  init_db();
404
458
 
@@ -409,7 +463,7 @@ var PG_MIGRATIONS = [
409
463
  timestamp TEXT NOT NULL,
410
464
  session_id TEXT NOT NULL,
411
465
  hook_name TEXT NOT NULL,
412
- event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit')),
466
+ event_type TEXT NOT NULL CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart')),
413
467
  tool_name TEXT,
414
468
  tool_input TEXT,
415
469
  result TEXT CHECK (result IN ('continue', 'block', NULL)),
@@ -442,7 +496,10 @@ var PG_MIGRATIONS = [
442
496
  )`,
443
497
  `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
444
498
  `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
445
- CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`
499
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit'))`,
500
+ `ALTER TABLE hook_events DROP CONSTRAINT IF EXISTS hook_events_event_type_check`,
501
+ `ALTER TABLE hook_events ADD CONSTRAINT hook_events_event_type_check
502
+ CHECK (event_type IN ('PreToolUse', 'PostToolUse', 'Stop', 'Notification', 'SessionStart', 'SessionEnd', 'UserPromptSubmit', 'SubagentStart'))`
446
503
  ];
447
504
 
448
505
  // src/db/remote-storage.ts
@@ -456,7 +513,34 @@ function normalizeParams(params) {
456
513
  return flat.map((value) => value === undefined ? null : value);
457
514
  }
458
515
  function sslConfigFor(connectionString) {
459
- return connectionString.includes("sslmode=require") || connectionString.includes("ssl=true") ? { rejectUnauthorized: false } : undefined;
516
+ const wantsSsl = wantsSslFor(connectionString);
517
+ if (!wantsSsl)
518
+ return;
519
+ const insecure = process.env.HASNA_HOOKS_PG_INSECURE_TLS === "1" || process.env.HOOKS_PG_INSECURE_TLS === "1";
520
+ if (insecure && false) {}
521
+ return insecure ? { rejectUnauthorized: false } : { rejectUnauthorized: true };
522
+ }
523
+ function wantsSslFor(connectionString) {
524
+ let raw;
525
+ try {
526
+ raw = new URL(connectionString).search.replace(/^\?/, "");
527
+ } catch {
528
+ raw = connectionString;
529
+ }
530
+ try {
531
+ raw = decodeURIComponent(raw);
532
+ } catch {}
533
+ const paramValue = (name) => {
534
+ const match = raw.match(new RegExp(`(?:^|[&\\s])${name}\\s*=\\s*([^&\\s]+)`, "i"));
535
+ return match ? match[1].toLowerCase() : undefined;
536
+ };
537
+ const sslmode = paramValue("sslmode");
538
+ if (sslmode !== undefined)
539
+ return sslmode !== "disable" && sslmode !== "allow";
540
+ const ssl = paramValue("ssl");
541
+ if (ssl !== undefined)
542
+ return ssl !== "false" && ssl !== "0" && ssl !== "no";
543
+ return false;
460
544
  }
461
545
 
462
546
  class PgAdapterAsync {
@@ -478,6 +562,7 @@ class PgAdapterAsync {
478
562
  }
479
563
 
480
564
  // src/db/storage-sync.ts
565
+ init_redact();
481
566
  var STORAGE_TABLES = [
482
567
  "hook_events",
483
568
  "schema_migrations",
@@ -654,9 +739,15 @@ async function pushTable(db, remote, table) {
654
739
  result.rowsRead = rows.length;
655
740
  if (rows.length === 0)
656
741
  return result;
742
+ const projected = table === "hook_events" ? rows.map((row) => ({
743
+ ...row,
744
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
745
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
746
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
747
+ })) : rows;
657
748
  const remoteColumns = await getRemoteColumns(remote, table);
658
- const columns = filterRemoteColumns(remoteColumns, Object.keys(rows[0]));
659
- result.rowsWritten = await upsertPg(remote, table, columns, rows, remoteColumns);
749
+ const columns = filterRemoteColumns(remoteColumns, Object.keys(projected[0]));
750
+ result.rowsWritten = await upsertPg(remote, table, columns, projected, remoteColumns);
660
751
  } catch (error) {
661
752
  result.errors.push(error instanceof Error ? error.message : String(error));
662
753
  }
@@ -671,8 +762,14 @@ async function pullTable(remote, db, table) {
671
762
  result.rowsRead = rows.length;
672
763
  if (rows.length === 0)
673
764
  return result;
674
- const columns = filterLocalColumns(db, table, Object.keys(rows[0]));
675
- result.rowsWritten = upsertSqlite(db, table, columns, rows);
765
+ const projected = table === "hook_events" ? rows.map((row) => ({
766
+ ...row,
767
+ tool_input: typeof row.tool_input === "string" ? redactEventPayload(row.tool_input) : row.tool_input,
768
+ error: typeof row.error === "string" ? redactEventPayload(row.error) : row.error,
769
+ metadata: typeof row.metadata === "string" ? redactEventPayload(row.metadata) : row.metadata
770
+ })) : rows;
771
+ const columns = filterLocalColumns(db, table, Object.keys(projected[0]));
772
+ result.rowsWritten = upsertSqlite(db, table, columns, projected);
676
773
  } catch (error) {
677
774
  result.errors.push(error instanceof Error ? error.message : String(error));
678
775
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.6.4",
3
+ "version": "0.6.6",
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": {
@@ -53,7 +53,7 @@
53
53
  },
54
54
  "dependencies": {
55
55
  "@hasna/events": "^0.1.6",
56
- "@hasna/secrets": "^0.2.22",
56
+ "@hasna/secrets": "0.3.0",
57
57
  "@modelcontextprotocol/sdk": "^1.26.0",
58
58
  "chalk": "^5.3.0",
59
59
  "commander": "^12.1.0",