@mnemonik/claude-code-hooks 0.9.22 → 0.9.23

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,115 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { link, mkdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
3
+ import { homedir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ function errorCode(error) {
6
+ return error && typeof error === 'object' && 'code' in error
7
+ ? String(error.code)
8
+ : undefined;
9
+ }
10
+ function lifecyclePath(input) {
11
+ const key = createHash('sha256')
12
+ .update(input.server.replace(/\/$/, ''))
13
+ .update('\0')
14
+ .update(input.cwd)
15
+ .update('\0')
16
+ .update(input.sessionId)
17
+ .digest('hex');
18
+ return join(homedir(), '.mnemonik', 'hooks', 'claude-code-hooks', 'session-incarnations', `${key}.json`);
19
+ }
20
+ export async function storeSessionLifecycleToken(input) {
21
+ const path = lifecyclePath(input);
22
+ const tmp = `${path}.${process.pid}.tmp`;
23
+ try {
24
+ await mkdir(join(path, '..'), { recursive: true, mode: 0o700 });
25
+ await writeFile(tmp, JSON.stringify({ lifecycleToken: input.lifecycleToken, receivedAt: Date.now() }), { encoding: 'utf8', mode: 0o600 });
26
+ await rename(tmp, path);
27
+ return true;
28
+ }
29
+ catch {
30
+ await unlink(tmp).catch(() => { });
31
+ return false;
32
+ }
33
+ }
34
+ /**
35
+ * Read the token that existed when this hook subprocess started. If a newer
36
+ * SessionStart overwrote the file while an older SessionEnd was delayed, the
37
+ * local receive timestamp makes the old process fail closed instead of reading
38
+ * and presenting the new incarnation's capability. Equality also fails closed:
39
+ * Date.now() has millisecond resolution, so a newer write can share the hook's
40
+ * captured start timestamp.
41
+ */
42
+ export async function readSessionLifecycleToken(input) {
43
+ try {
44
+ const parsed = JSON.parse(await readFile(lifecyclePath(input), 'utf8'));
45
+ if (typeof parsed.lifecycleToken !== 'string' ||
46
+ parsed.lifecycleToken.length === 0 ||
47
+ typeof parsed.receivedAt !== 'number' ||
48
+ parsed.receivedAt >= input.invocationStartedAt) {
49
+ return null;
50
+ }
51
+ return parsed.lifecycleToken;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ /**
58
+ * Compare-and-remove without a read→unlink TOCTOU. Rename the current record to
59
+ * a unique quarantine path first, then inspect the exact inode we moved. If a
60
+ * resumed SessionStart won the race, restore that newer record only when the
61
+ * canonical path is still absent; hard-link creation is atomic and never
62
+ * overwrites a still-newer token.
63
+ */
64
+ export async function removeSessionLifecycleToken(input) {
65
+ const path = lifecyclePath(input);
66
+ const quarantine = `${path}.${process.pid}.${randomUUID()}.remove`;
67
+ let moved = false;
68
+ try {
69
+ // Confirm the source is readable before moving it. The content is read
70
+ // again from quarantine because another process may replace the path after
71
+ // this check and before rename.
72
+ await readFile(path, 'utf8');
73
+ await rename(path, quarantine);
74
+ moved = true;
75
+ const raw = await readFile(quarantine, 'utf8');
76
+ let matches = false;
77
+ try {
78
+ matches = JSON.parse(raw).lifecycleToken === input.lifecycleToken;
79
+ }
80
+ catch {
81
+ // Malformed/unknown state is never safe to delete; restore it below.
82
+ }
83
+ if (matches) {
84
+ await unlink(quarantine);
85
+ moved = false;
86
+ return;
87
+ }
88
+ try {
89
+ await link(quarantine, path);
90
+ }
91
+ catch (error) {
92
+ if (errorCode(error) !== 'EEXIST')
93
+ throw error;
94
+ }
95
+ await unlink(quarantine);
96
+ moved = false;
97
+ }
98
+ catch {
99
+ if (moved) {
100
+ let safeToRemoveQuarantine = false;
101
+ try {
102
+ await link(quarantine, path);
103
+ safeToRemoveQuarantine = true;
104
+ }
105
+ catch (error) {
106
+ safeToRemoveQuarantine = ['EEXIST', 'ENOENT'].includes(errorCode(error) ?? '');
107
+ }
108
+ if (safeToRemoveQuarantine)
109
+ await unlink(quarantine).catch(() => { });
110
+ }
111
+ // Best-effort local cleanup; the server token remains authoritative and a
112
+ // missing capability merely defers to normal inactivity expiry.
113
+ }
114
+ }
115
+ //# sourceMappingURL=sessionLifecycle.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessionLifecycle.js","sourceRoot":"","sources":["../../src/lib/sessionLifecycle.ts"],"names":[],"mappings":"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"}
@@ -9,6 +9,8 @@ export type HookEventName = 'PreToolUse' | 'PostToolUse' | 'SessionStart' | 'Sto
9
9
  export interface HookInput {
10
10
  session_id: string;
11
11
  transcript_path?: string;
12
+ trigger?: string;
13
+ custom_instructions?: string;
12
14
  cwd: string;
13
15
  hook_event_name: HookEventName;
14
16
  permission_mode?: string;
@@ -17,6 +19,8 @@ export interface HookInput {
17
19
  tool_input?: Record<string, unknown>;
18
20
  tool_response?: Record<string, unknown>;
19
21
  tool_use_id?: string;
22
+ /** Claude SessionEnd reason (for example clear/logout/exit). */
23
+ reason?: string;
20
24
  }
21
25
  /** PreToolUse decision shape recognised by Claude Code. */
22
26
  export type PermissionDecision = 'allow' | 'deny' | 'ask' | 'defer';
@@ -93,6 +97,8 @@ export interface PolicyReminderResponse {
93
97
  export interface InjectionsResponse {
94
98
  ok: boolean;
95
99
  reason?: 'unauthorized' | 'no_project';
100
+ /** SessionStart recovery readback for a lost /hooks/session-start response. */
101
+ lifecycleToken?: string;
96
102
  injections: Array<{
97
103
  signal: string;
98
104
  kind: 'directive' | 'nudge';
@@ -1 +1 @@
1
- {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAmOH,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC"}
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/lib/types.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAyOH,MAAM,CAAC,MAAM,cAAc,GAAG,0BAA0B,CAAC"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@mnemonik/claude-code-hooks",
3
- "version": "0.9.22",
4
- "description": "Claude Code lifecycle contextual hooks for Mnemonik. PreToolUse Edit/Write/NotebookEdit/Bash gates plus SessionStart bootstrap pre-injection, Stop checkpoint gate (advisory), PreCompact context_snapshot pre-injection, SessionEnd save, and UserPromptSubmit context inject. filesToWrite delivery of skill/rules/.mnemonik.json continues to flow for ALL clients including Claude Code — this package adds host-side hook gating only. Versioned independently from the @mnemonik/server release cadence (mirrors @mnemonik/scanner).",
3
+ "version": "0.9.23",
4
+ "description": "Claude Code lifecycle contextual hooks for Mnemonik. PreToolUse Edit/Write/NotebookEdit/Bash gates plus SessionStart bootstrap warming, clean Stop, PreCompact context_snapshot creation, bounded SessionEnd cleanup, and UserPromptSubmit context injection. This hooks-only package does not install skills, rules, project identity, or MCP config. Versioned independently from the @mnemonik/server release cadence (mirrors @mnemonik/scanner).",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "mnemonik-claude-code-hooks": "dist/install.js"