praxis-agent 0.31.0 → 0.32.0

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.
@@ -110,7 +110,20 @@ const TUI_HOOK_EVENT_DEFINITIONS = [
110
110
  detail: [],
111
111
  },
112
112
  ];
113
- export const TUI_HOOK_EVENTS = TUI_HOOK_EVENT_DEFINITIONS.filter((definition) => HOOK_EVENTS.includes(definition.name));
113
+ const OBSERVED_TUI_EVENTS_2_1_208 = new Set([
114
+ 'PreToolUse',
115
+ 'PostToolUse',
116
+ 'PostToolUseFailure',
117
+ 'UserPromptSubmit',
118
+ 'SessionStart',
119
+ 'Stop',
120
+ 'SubagentStart',
121
+ 'SubagentStop',
122
+ 'SessionEnd',
123
+ 'PermissionRequest',
124
+ ]);
125
+ export const TUI_HOOK_EVENTS = TUI_HOOK_EVENT_DEFINITIONS.filter((definition) => OBSERVED_TUI_EVENTS_2_1_208.has(definition.name) &&
126
+ HOOK_EVENTS.includes(definition.name));
114
127
  function isRecord(value) {
115
128
  return typeof value === 'object' && value !== null && !Array.isArray(value);
116
129
  }
@@ -39,6 +39,7 @@ interface SessionCommands {
39
39
  rewindFiles?(sessionId: string, userMessageId: string): Promise<void>;
40
40
  rewindPoints?(sessionId: string): Promise<RewindPoint[]>;
41
41
  changeCwd?(sessionId: string | undefined, cwd: string): Promise<string>;
42
+ notify?(sessionId: string | undefined, message: string, notificationType: string, title?: string): void;
42
43
  recordCdUsage?(sessionId: string): Promise<void>;
43
44
  approveRecentlyDenied?(sessionId: string, display: string): Promise<void>;
44
45
  retryRecentlyDenied?(sessionId: string, display: string, signal?: AbortSignal): Promise<SessionRunResult>;
@@ -33,6 +33,7 @@ import { createClaudeModelAutoClassifier, defaultClaudeAutoModeConfig, loadClaud
33
33
  import { ClaudeExtensionPermissionResolver, ClaudeExtensionToolRegistry, } from './extensions/claude-extension-tools.js';
34
34
  import { ClaudeExtensionCatalog } from './extensions/claude-extensions.js';
35
35
  import { ClaudeHookRunner } from './hooks/claude-hooks.js';
36
+ import { ClaudeSessionEnvironment } from './hooks/claude-session-environment.js';
36
37
  import { ClaudeMcpToolRegistry, } from './mcp/claude-mcp-tools.js';
37
38
  import { ClaudeMcpManagement, filterDisabledMcpResources, mcpScope, } from './mcp/claude-mcp-management.js';
38
39
  import { authenticateMcpServer, ClaudeMcpOAuthStore, mcpOAuthServerIdentity, readMcpClientSecret, } from './mcp/claude-mcp-oauth.js';
@@ -1226,6 +1227,15 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1226
1227
  : {}),
1227
1228
  }), extensions);
1228
1229
  const permissions = permissionResolverForMode(cli.dangerouslySkipPermissions ? 'bypassPermissions' : cli.permissionMode);
1230
+ const hookSessionEnvironment = new ClaudeSessionEnvironment({
1231
+ stateRoot: resolveDataPlanePaths({
1232
+ dataPlane,
1233
+ root: configRoot,
1234
+ cwd,
1235
+ sessionId: '00000000-0000-4000-8000-000000000000',
1236
+ }).stateRoot,
1237
+ warn: (message) => runtimeEventSink({ type: 'warning', message }),
1238
+ });
1229
1239
  const localTools = new LocalToolRegistry({
1230
1240
  cwd,
1231
1241
  cwdProvider: () => workspace.cwd(),
@@ -1237,6 +1247,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1237
1247
  homeDirectory: sandboxEnvironment.HOME ?? sandboxEnvironment.USERPROFILE ?? homedir(),
1238
1248
  configRoot,
1239
1249
  dataPlane,
1250
+ sessionEnvironment: (sessionId) => hookSessionEnvironment.environment(sessionId),
1240
1251
  ...(environment ? { environment } : {}),
1241
1252
  });
1242
1253
  const runtimeMcpResources = async (candidates) => {
@@ -1437,6 +1448,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1437
1448
  settings,
1438
1449
  cwd,
1439
1450
  onEvent: (event) => runtimeEventSink({ type: 'hook', event }),
1451
+ sessionEnvironment: hookSessionEnvironment,
1440
1452
  });
1441
1453
  const interactiveTools = selectedInteractiveTools.length > 0 && askUser && approvePlan
1442
1454
  ? new ClaudeInteractiveToolManager({
@@ -1454,6 +1466,11 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1454
1466
  : undefined;
1455
1467
  contextAssembler = new ClaudeContextAssembler({
1456
1468
  loadResources: loadContextResources,
1469
+ onInstructionsLoaded: async (instructions, context) => {
1470
+ if (!context.lifecycleId)
1471
+ return;
1472
+ await service.instructionsLoaded(context.lifecycleId, instructions, context.reason);
1473
+ },
1457
1474
  loadDynamicContext: (runtimeCwd = workspace.cwd()) => loadClaudeDynamicContext({
1458
1475
  cwd: runtimeCwd,
1459
1476
  ...(memoryDirectory ? { memoryDirectory } : {}),
@@ -1624,6 +1641,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
1624
1641
  rewindFiles: (sessionId, userMessageId) => service.rewindFiles(sessionId, userMessageId),
1625
1642
  rewindPoints: (sessionId) => service.rewindPoints(sessionId),
1626
1643
  changeCwd: (sessionId, cwd) => service.changeCwd(sessionId, cwd),
1644
+ notify: (sessionId, message, notificationType, title) => service.notifyDetached(sessionId, message, notificationType, title),
1627
1645
  recordCdUsage: (sessionId) => service.recordCdUsage(sessionId),
1628
1646
  approveRecentlyDenied: (sessionId, display) => service.approveRecentlyDenied(sessionId, display),
1629
1647
  retryRecentlyDenied: (sessionId, display, retrySignal) => service.retryRecentlyDenied(sessionId, display, retrySignal),
@@ -1,8 +1,13 @@
1
1
  import type { AssembledContext, ContextAssembler, ContextAssemblyOptions, ContextInvalidationOptions } from '../../core/context.js';
2
2
  import { type ClaudeDynamicContextSections } from './dynamic-context.js';
3
- import type { ClaudeContextResources, ClaudeConditionalRule } from './shared-resources.js';
3
+ import type { ClaudeContextResources, ClaudeConditionalRule, ClaudeTextResource } from './shared-resources.js';
4
4
  export interface ClaudeContextAssemblerOptions {
5
5
  loadResources(cwd?: string): Promise<ClaudeContextResources>;
6
+ onInstructionsLoaded?(resources: readonly ClaudeTextResource[], context: {
7
+ lifecycleId?: string;
8
+ cwd?: string;
9
+ reason: 'session_start' | 'compact' | 'resource_reload';
10
+ }): Promise<void>;
6
11
  loadDynamicContext?(cwd?: string): Promise<ClaudeDynamicContextSections>;
7
12
  loadMcpInstructions?(): Promise<readonly ClaudeMcpInstruction[]>;
8
13
  loadSessionGuidance?(): Promise<string | undefined>;
@@ -171,6 +171,8 @@ ${sections.join('\n\n')}`,
171
171
  if (options.reason === 'resource-reload' ||
172
172
  options.reason === 'compact') {
173
173
  delete snapshot.resources;
174
+ snapshot.resourceLoadReason =
175
+ options.reason === 'compact' ? 'compact' : 'resource_reload';
174
176
  continue;
175
177
  }
176
178
  if (options.reason === 'tool-pool') {
@@ -199,7 +201,18 @@ ${sections.join('\n\n')}`,
199
201
  if (!snapshot)
200
202
  return this.options.loadResources(cwd);
201
203
  if (!snapshot.resources) {
202
- const pending = this.options.loadResources(cwd);
204
+ const reason = snapshot.resourceLoadReason ?? 'session_start';
205
+ delete snapshot.resourceLoadReason;
206
+ const pending = this.options
207
+ .loadResources(cwd)
208
+ .then(async (resources) => {
209
+ await this.options.onInstructionsLoaded?.(resources.instructions, {
210
+ lifecycleId: snapshot.lifecycleId,
211
+ ...(cwd === undefined ? {} : { cwd }),
212
+ reason,
213
+ });
214
+ return resources;
215
+ });
203
216
  snapshot.resources = pending;
204
217
  void pending.catch(() => {
205
218
  if (snapshot.resources === pending)
@@ -319,6 +319,7 @@ export interface ToolExecutionResult {
319
319
  }
320
320
  export interface ToolExecutionContext {
321
321
  cwd: string;
322
+ sessionId?: string;
322
323
  messages?: readonly ModelMessage[];
323
324
  signal?: AbortSignal;
324
325
  toolResultDirectory?: string;
@@ -407,6 +408,7 @@ export type PermissionDecision = {
407
408
  } | {
408
409
  behavior: 'deny';
409
410
  reason: string;
411
+ followUpUserMessages?: readonly string[];
410
412
  source?: PermissionDecisionSource;
411
413
  };
412
414
  export interface PermissionResolver {
@@ -455,6 +457,7 @@ export interface AgentRuntimeOptions {
455
457
  }
456
458
  export interface AgentRunRequest {
457
459
  messages: readonly ModelMessage[];
460
+ sessionId?: string;
458
461
  /** Number of leading system messages that form the stable prompt prefix. */
459
462
  stableSystemMessageCount?: number;
460
463
  cwd?: string;
@@ -481,7 +484,7 @@ export interface AgentRunRequest {
481
484
  maxModelTurns?: number;
482
485
  betas?: readonly string[];
483
486
  }
484
- export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'toolResultDirectory'> {
487
+ export interface AgentToolRecoveryRequest extends Pick<AgentRunRequest, 'approveTool' | 'cwd' | 'observer' | 'onPermissionUpdates' | 'permissionUpdates' | 'signal' | 'sessionId' | 'toolResultDirectory'> {
485
488
  approveRecovery?: (call: ModelToolCall) => boolean | Promise<boolean>;
486
489
  messages?: readonly ModelMessage[];
487
490
  }
@@ -845,6 +845,8 @@ export class AgentRuntime {
845
845
  permissionPhase: 'request',
846
846
  permissionUpdates: request.permissionUpdates ?? [],
847
847
  };
848
+ if (request.sessionId)
849
+ context.sessionId = request.sessionId;
848
850
  if (request.signal)
849
851
  context.signal = request.signal;
850
852
  if (request.toolResultDirectory) {
@@ -926,7 +928,13 @@ export class AgentRuntime {
926
928
  : decision.behavior === 'ask'
927
929
  ? (decision.reason ?? 'Permission approval was not provided')
928
930
  : 'Permission approval was not provided');
929
- return { content: reason, isError: true };
931
+ return {
932
+ content: reason,
933
+ isError: true,
934
+ ...(decision.behavior === 'deny' && decision.followUpUserMessages
935
+ ? { followUpUserMessages: decision.followUpUserMessages }
936
+ : {}),
937
+ };
930
938
  }
931
939
  if (request.signal?.aborted) {
932
940
  throw request.signal.reason ?? new AgentRunCancelledError();
@@ -0,0 +1,43 @@
1
+ export type ClaudeFileChangeEvent = 'add' | 'change' | 'unlink';
2
+ interface DirectoryWatcher {
3
+ close(): void;
4
+ on(event: 'error', listener: (error: Error) => void): DirectoryWatcher;
5
+ unref?(): void;
6
+ }
7
+ type WatchDirectory = (directory: string, listener: (filename: string | null) => void) => DirectoryWatcher;
8
+ export interface ClaudeFileChangeWatcherOptions {
9
+ cwd: string;
10
+ staticPaths: readonly string[];
11
+ onFileChanged(filePath: string, event: ClaudeFileChangeEvent, signal: AbortSignal): Promise<readonly string[] | undefined>;
12
+ warn(message: string): void;
13
+ debounceMs?: number;
14
+ pathExists?(path: string): boolean;
15
+ watchDirectory?: WatchDirectory;
16
+ }
17
+ export declare class ClaudeFileChangeWatcher {
18
+ private readonly options;
19
+ private cwd;
20
+ private staticPaths;
21
+ private dynamicPaths;
22
+ private readonly watchers;
23
+ private readonly knownExistence;
24
+ private readonly pending;
25
+ private readonly inFlight;
26
+ private readonly debounceMs;
27
+ private readonly pathExists;
28
+ private readonly watchDirectory;
29
+ private generation;
30
+ private closed;
31
+ constructor(options: ClaudeFileChangeWatcherOptions);
32
+ updateForCwd(cwd: string, staticPaths: readonly string[], dynamicPaths: readonly string[]): void;
33
+ updateDynamicPaths(paths: readonly string[]): void;
34
+ close(timeoutMs?: number): Promise<void>;
35
+ private validDynamicPaths;
36
+ private targetPaths;
37
+ private rebuild;
38
+ private schedule;
39
+ private dispatch;
40
+ private clearResources;
41
+ }
42
+ export {};
43
+ //# sourceMappingURL=claude-file-change-watcher.d.ts.map
@@ -0,0 +1,185 @@
1
+ import { existsSync, watch } from 'node:fs';
2
+ import { basename, dirname, isAbsolute, resolve } from 'node:path';
3
+ function defaultWatchDirectory(directory, listener) {
4
+ return watch(directory, (_event, filename) => {
5
+ listener(filename);
6
+ });
7
+ }
8
+ export class ClaudeFileChangeWatcher {
9
+ options;
10
+ cwd;
11
+ staticPaths;
12
+ dynamicPaths = [];
13
+ watchers = new Map();
14
+ knownExistence = new Map();
15
+ pending = new Map();
16
+ inFlight = new Map();
17
+ debounceMs;
18
+ pathExists;
19
+ watchDirectory;
20
+ generation = 0;
21
+ closed = false;
22
+ constructor(options) {
23
+ this.options = options;
24
+ this.cwd = options.cwd;
25
+ this.staticPaths = options.staticPaths;
26
+ this.debounceMs = options.debounceMs ?? 500;
27
+ this.pathExists = options.pathExists ?? existsSync;
28
+ this.watchDirectory = options.watchDirectory ?? defaultWatchDirectory;
29
+ this.rebuild();
30
+ }
31
+ updateForCwd(cwd, staticPaths, dynamicPaths) {
32
+ if (this.closed)
33
+ return;
34
+ this.cwd = cwd;
35
+ this.staticPaths = staticPaths;
36
+ this.dynamicPaths = this.validDynamicPaths(dynamicPaths);
37
+ this.rebuild();
38
+ }
39
+ updateDynamicPaths(paths) {
40
+ if (this.closed)
41
+ return;
42
+ const next = this.validDynamicPaths(paths);
43
+ if (next.length === this.dynamicPaths.length &&
44
+ next.every((path, index) => path === this.dynamicPaths[index])) {
45
+ return;
46
+ }
47
+ this.dynamicPaths = next;
48
+ this.rebuild();
49
+ }
50
+ async close(timeoutMs = 5_000) {
51
+ if (this.closed)
52
+ return;
53
+ if (!Number.isFinite(timeoutMs) || timeoutMs < 0) {
54
+ throw new Error('FileChanged watcher close timeout must be non-negative');
55
+ }
56
+ this.closed = true;
57
+ this.generation += 1;
58
+ this.clearResources();
59
+ const pending = [...this.inFlight.keys()];
60
+ if (pending.length === 0)
61
+ return;
62
+ let timer;
63
+ try {
64
+ if ((await Promise.race([
65
+ Promise.allSettled(pending).then(() => 'settled'),
66
+ new Promise((resolveTimeout) => {
67
+ timer = setTimeout(() => resolveTimeout('timeout'), timeoutMs);
68
+ }),
69
+ ])) === 'timeout') {
70
+ for (const controller of this.inFlight.values())
71
+ controller.abort();
72
+ this.inFlight.clear();
73
+ await Promise.resolve();
74
+ }
75
+ }
76
+ finally {
77
+ if (timer)
78
+ clearTimeout(timer);
79
+ }
80
+ }
81
+ validDynamicPaths(paths) {
82
+ const valid = [];
83
+ for (const path of paths) {
84
+ if (!isAbsolute(path)) {
85
+ this.options.warn(`FileChanged hook ignored non-absolute watch path: ${path}`);
86
+ continue;
87
+ }
88
+ valid.push(resolve(path));
89
+ }
90
+ return [...new Set(valid)];
91
+ }
92
+ targetPaths() {
93
+ return [
94
+ ...new Set([
95
+ ...this.staticPaths.map((path) => isAbsolute(path) ? resolve(path) : resolve(this.cwd, path)),
96
+ ...this.dynamicPaths,
97
+ ]),
98
+ ];
99
+ }
100
+ rebuild() {
101
+ this.generation += 1;
102
+ this.clearResources();
103
+ const targets = this.targetPaths();
104
+ const byDirectory = new Map();
105
+ for (const target of targets) {
106
+ this.knownExistence.set(target, this.pathExists(target));
107
+ const directory = dirname(target);
108
+ const names = byDirectory.get(directory) ?? new Set();
109
+ names.add(basename(target));
110
+ byDirectory.set(directory, names);
111
+ }
112
+ for (const [directory, names] of byDirectory) {
113
+ try {
114
+ const watcher = this.watchDirectory(directory, (filename) => {
115
+ if (filename === null) {
116
+ for (const name of names)
117
+ this.schedule(resolve(directory, name));
118
+ return;
119
+ }
120
+ if (names.has(filename))
121
+ this.schedule(resolve(directory, filename));
122
+ });
123
+ watcher.on('error', (error) => {
124
+ this.options.warn(`FileChanged watcher failed for ${directory}: ${error.message}`);
125
+ });
126
+ watcher.unref?.();
127
+ this.watchers.set(directory, watcher);
128
+ }
129
+ catch (error) {
130
+ this.options.warn(`FileChanged watcher could not watch ${directory}: ${error instanceof Error ? error.message : String(error)}`);
131
+ }
132
+ }
133
+ }
134
+ schedule(path) {
135
+ const previous = this.pending.get(path);
136
+ if (previous)
137
+ clearTimeout(previous);
138
+ const generation = this.generation;
139
+ const timer = setTimeout(() => {
140
+ this.pending.delete(path);
141
+ if (this.closed || generation !== this.generation)
142
+ return;
143
+ const controller = new AbortController();
144
+ const dispatch = this.dispatch(path, generation, controller.signal);
145
+ this.inFlight.set(dispatch, controller);
146
+ void dispatch.finally(() => this.inFlight.delete(dispatch));
147
+ }, this.debounceMs);
148
+ timer.unref?.();
149
+ this.pending.set(path, timer);
150
+ }
151
+ async dispatch(path, generation, signal) {
152
+ const existed = this.knownExistence.get(path) ?? false;
153
+ const exists = this.pathExists(path);
154
+ this.knownExistence.set(path, exists);
155
+ if (!existed && !exists)
156
+ return;
157
+ const event = !existed
158
+ ? 'add'
159
+ : !exists
160
+ ? 'unlink'
161
+ : 'change';
162
+ try {
163
+ const watchPaths = await this.options.onFileChanged(path, event, signal);
164
+ if (!this.closed &&
165
+ generation === this.generation &&
166
+ watchPaths &&
167
+ watchPaths.length > 0) {
168
+ this.updateDynamicPaths(watchPaths);
169
+ }
170
+ }
171
+ catch (error) {
172
+ this.options.warn(`FileChanged hook failed for ${path}: ${error instanceof Error ? error.message : String(error)}`);
173
+ }
174
+ }
175
+ clearResources() {
176
+ for (const timer of this.pending.values())
177
+ clearTimeout(timer);
178
+ this.pending.clear();
179
+ for (const watcher of this.watchers.values())
180
+ watcher.close();
181
+ this.watchers.clear();
182
+ this.knownExistence.clear();
183
+ }
184
+ }
185
+ //# sourceMappingURL=claude-file-change-watcher.js.map
@@ -12,6 +12,7 @@ export interface ClaudeHookToolCoordinatorOptions {
12
12
  hooks: ClaudeHookRunner;
13
13
  session: ClaudeHookSessionInput;
14
14
  recordOutcome(outcome: ClaudeHookOutcome, deferUntilApproval?: boolean): Promise<void>;
15
+ warn?(message: string): void;
15
16
  deferPreToolUseOutcome?(call: ModelToolCall): boolean;
16
17
  }
17
18
  export declare class ClaudeHookToolCoordinator implements ToolRegistry, PermissionResolver {
@@ -25,6 +26,7 @@ export declare class ClaudeHookToolCoordinator implements ToolRegistry, Permissi
25
26
  };
26
27
  prepare(call: ModelToolCall, context: ToolExecutionContext): Promise<ModelToolCall>;
27
28
  resolve(call: ModelToolCall, context?: PermissionResolutionContext): Promise<PermissionDecision>;
29
+ private withPermissionDeniedHook;
28
30
  execute(call: ModelToolCall, context: ToolExecutionContext): Promise<ToolExecutionResult>;
29
31
  }
30
32
  //# sourceMappingURL=claude-hook-tools.d.ts.map
@@ -1,3 +1,4 @@
1
+ import { permissionDecisionSource } from '../core/runtime.js';
1
2
  export class ClaudeHookToolCoordinator {
2
3
  options;
3
4
  prepared = new Map();
@@ -57,11 +58,13 @@ export class ClaudeHookToolCoordinator {
57
58
  }
58
59
  async resolve(call, context) {
59
60
  const prepared = this.prepared.get(call.id);
60
- if (prepared?.permissionDecision)
61
- return prepared.permissionDecision;
61
+ if (prepared?.permissionDecision) {
62
+ return this.withPermissionDeniedHook(call, prepared.permissionDecision, prepared.signal);
63
+ }
62
64
  const decision = await this.options.permissions.resolve(call, context);
63
- if (decision.behavior !== 'ask')
64
- return decision;
65
+ if (decision.behavior !== 'ask') {
66
+ return this.withPermissionDeniedHook(call, decision, prepared?.signal);
67
+ }
65
68
  const outcome = await this.options.hooks.run({
66
69
  ...this.options.session,
67
70
  hook_event_name: 'PermissionRequest',
@@ -72,17 +75,53 @@ export class ClaudeHookToolCoordinator {
72
75
  }, call.name, prepared?.signal);
73
76
  await this.options.recordOutcome(outcome);
74
77
  if (outcome.blockedReason || outcome.permissionDecision === 'deny') {
75
- return {
78
+ return this.withPermissionDeniedHook(call, {
76
79
  behavior: 'deny',
77
80
  reason: outcome.blockedReason ??
78
81
  outcome.permissionDecisionReason ??
79
82
  `PermissionRequest:${call.name} hook denied tool`,
80
- };
83
+ }, prepared?.signal);
81
84
  }
82
85
  if (outcome.permissionDecision === 'allow')
83
86
  return { behavior: 'allow' };
84
87
  return decision;
85
88
  }
89
+ async withPermissionDeniedHook(call, decision, signal) {
90
+ const decisionSource = permissionDecisionSource(decision);
91
+ if (decision.behavior !== 'deny' || decisionSource !== 'auto-classifier') {
92
+ return decision;
93
+ }
94
+ const prepared = this.prepared.get(call.id);
95
+ try {
96
+ const outcome = await this.options.hooks.run({
97
+ ...this.options.session,
98
+ hook_event_name: 'PermissionDenied',
99
+ tool_name: call.name,
100
+ tool_input: prepared?.hookInput ?? call.input,
101
+ tool_use_id: call.id,
102
+ reason: decision.reason,
103
+ }, call.name, signal);
104
+ await this.options.recordOutcome(outcome);
105
+ if (outcome.retry) {
106
+ return {
107
+ ...decision,
108
+ followUpUserMessages: [
109
+ ...(decision.followUpUserMessages ?? []),
110
+ 'The PermissionDenied hook indicated this command is now approved. You may retry it if you would like.',
111
+ ],
112
+ source: decisionSource,
113
+ };
114
+ }
115
+ }
116
+ catch (error) {
117
+ if (signal?.aborted ||
118
+ (error instanceof Error && error.name === 'AbortError')) {
119
+ throw error;
120
+ }
121
+ this.options.warn?.(`PermissionDenied:${call.name} hook failed: ${error instanceof Error ? error.message : String(error)}`);
122
+ }
123
+ return decision;
124
+ }
86
125
  async execute(call, context) {
87
126
  const prepared = this.prepared.get(call.id);
88
127
  try {
@@ -1,6 +1,7 @@
1
1
  import type { ClaudeJsonResource } from '../compatibility/claude/shared-resources.js';
2
2
  import type { PermissionBehavior } from '../core/runtime.js';
3
- export type ClaudeHookEventName = 'Setup' | 'SessionStart' | 'SubagentStart' | 'UserPromptSubmit' | 'PreToolUse' | 'PermissionRequest' | 'PostToolUse' | 'PostToolUseFailure' | 'Stop' | 'SubagentStop' | 'SessionEnd';
3
+ import type { ClaudeSessionEnvironment } from './claude-session-environment.js';
4
+ export type ClaudeHookEventName = 'Setup' | 'SessionStart' | 'SubagentStart' | 'UserPromptSubmit' | 'PreToolUse' | 'PermissionRequest' | 'PermissionDenied' | 'PostToolUse' | 'PostToolUseFailure' | 'Notification' | 'Stop' | 'StopFailure' | 'SubagentStop' | 'PreCompact' | 'PostCompact' | 'TaskCreated' | 'TaskCompleted' | 'ConfigChange' | 'InstructionsLoaded' | 'WorktreeCreate' | 'WorktreeRemove' | 'CwdChanged' | 'FileChanged' | 'SessionEnd';
4
5
  export interface ClaudeHookInput extends Record<string, unknown> {
5
6
  session_id: string;
6
7
  transcript_path: string;
@@ -53,6 +54,9 @@ export interface ClaudeHookOutcome {
53
54
  updatedInput?: Record<string, unknown>;
54
55
  permissionDecision?: PermissionBehavior;
55
56
  permissionDecisionReason?: string;
57
+ retry?: boolean;
58
+ watchPaths?: readonly string[];
59
+ systemMessages?: readonly string[];
56
60
  blockedReason?: string;
57
61
  }
58
62
  interface ProcessResult {
@@ -71,6 +75,7 @@ export interface ClaudeHookRunnerOptions {
71
75
  maxTimeoutMs?: number;
72
76
  executeCommand?: ClaudeHookCommandExecutor;
73
77
  onEvent?: (event: ClaudeHookStreamEvent) => void;
78
+ sessionEnvironment?: ClaudeSessionEnvironment;
74
79
  }
75
80
  export declare const HOOK_EVENTS: readonly ClaudeHookEventName[];
76
81
  export declare function validateClaudeHooks(settings: readonly ClaudeJsonResource[], maxTimeoutMs?: number): void;
@@ -81,9 +86,16 @@ export declare class ClaudeHookRunner {
81
86
  private readonly executeCommand;
82
87
  private readonly onEvent;
83
88
  private readonly cwd;
84
- constructor(options: ClaudeHookRunnerOptions);
89
+ private readonly sessionEnvironment;
90
+ private readonly asynchronousHooks;
91
+ constructor(options: ClaudeHookRunnerOptions, asynchronousHooks?: Map<Promise<void>, AbortController>);
85
92
  withAdditionalSettings(settings: readonly ClaudeJsonResource[]): ClaudeHookRunner;
93
+ fileChangedWatchPaths(cwd?: string): readonly string[];
86
94
  run(input: ClaudeHookInput, matcherValue?: string, signal?: AbortSignal): Promise<ClaudeHookOutcome>;
95
+ drainAsync(timeoutMs: number): Promise<void>;
96
+ clearCwdEnvironment(sessionId: string): Promise<void>;
97
+ private executionEnvironment;
98
+ private invalidateSessionEnvironment;
87
99
  private runCommand;
88
100
  }
89
101
  export {};