@jmylchreest/aide-plugin 0.0.65 → 0.1.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.
@@ -23,9 +23,12 @@ import type {
23
23
  SessionState,
24
24
  SessionInitResult,
25
25
  MemoryInjection,
26
+ InjectedSource,
26
27
  StartupNotices,
27
28
  } from "./types.js";
28
29
  import { DEFAULT_CONFIG } from "./types.js";
30
+ import { isTruthy, isFalsy } from "../lib/hook-utils.js";
31
+ import { findProjectRoot } from "../lib/project-root.js";
29
32
 
30
33
  /**
31
34
  * Ensure all .aide directories exist
@@ -34,14 +37,19 @@ export function ensureDirectories(cwd: string): {
34
37
  created: number;
35
38
  existed: number;
36
39
  } {
40
+ // Resolve to the canonical project root so we don't plant a stray .aide/
41
+ // in a subdirectory the harness happened to launch from. When no marker
42
+ // is found, fall back to cwd (caller's hasMarker gate elsewhere refuses
43
+ // bootstrap unless AIDE_FORCE_INIT is set).
44
+ const { root } = findProjectRoot(cwd);
37
45
  const dirs = [
38
- join(cwd, ".aide"),
39
- join(cwd, ".aide", "skills"),
40
- join(cwd, ".aide", "config"),
41
- join(cwd, ".aide", "state"),
42
- join(cwd, ".aide", "memory"),
43
- join(cwd, ".aide", "worktrees"),
44
- join(cwd, ".aide", "_logs"),
46
+ join(root, ".aide"),
47
+ join(root, ".aide", "skills"),
48
+ join(root, ".aide", "config"),
49
+ join(root, ".aide", "state"),
50
+ join(root, ".aide", "memory"),
51
+ join(root, ".aide", "worktrees"),
52
+ join(root, ".aide", "_logs"),
45
53
  join(homedir(), ".aide"),
46
54
  join(homedir(), ".aide", "skills"),
47
55
  join(homedir(), ".aide", "config"),
@@ -66,7 +74,7 @@ export function ensureDirectories(cwd: string): {
66
74
  // Ensure .gitignore exists in .aide directory.
67
75
  // Structure: exclude all local-only runtime data, allow shared/ and config/.
68
76
  // shared/ contains git-friendly markdown exports (decisions, memories).
69
- const gitignorePath = join(cwd, ".aide", ".gitignore");
77
+ const gitignorePath = join(root, ".aide", ".gitignore");
70
78
  const requiredGitignoreContent = `# AIDE local runtime files - do not commit
71
79
  # These are machine-specific and/or binary (non-mergeable)
72
80
  _logs/
@@ -186,23 +194,40 @@ export function getProjectName(cwd: string): string {
186
194
  }
187
195
 
188
196
  /**
189
- * Load config from .aide/config/aide.json (if it exists).
190
- * Returns DEFAULT_CONFIG if no config file exists or it can't be parsed.
197
+ * Load config from ~/.aide/config/aide.json (global).
198
+ * Used before a project root has been resolved (e.g. by the SessionStart
199
+ * hook deciding whether to honour `requireGit`).
200
+ */
201
+ export function loadGlobalConfig(): AideConfig {
202
+ const configPath = join(homedir(), ".aide", "config", "aide.json");
203
+ if (!existsSync(configPath)) return DEFAULT_CONFIG;
204
+ try {
205
+ return { ...DEFAULT_CONFIG, ...JSON.parse(readFileSync(configPath, "utf-8")) };
206
+ } catch {
207
+ return DEFAULT_CONFIG;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Load config layered as: defaults → global (~/.aide/config/aide.json) →
213
+ * project (`<cwd>/.aide/config/aide.json`). Project values override global.
191
214
  * Does NOT create a default config file — only user-set values are persisted.
192
215
  */
193
216
  export function loadConfig(cwd: string): AideConfig {
194
- const configPath = join(cwd, ".aide", "config", "aide.json");
217
+ const global = loadGlobalConfig();
218
+ const { root } = findProjectRoot(cwd);
219
+ const projectPath = join(root, ".aide", "config", "aide.json");
195
220
 
196
- if (existsSync(configPath)) {
221
+ if (existsSync(projectPath)) {
197
222
  try {
198
- const content = readFileSync(configPath, "utf-8");
199
- return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
223
+ const project = JSON.parse(readFileSync(projectPath, "utf-8"));
224
+ return { ...DEFAULT_CONFIG, ...global, ...project };
200
225
  } catch {
201
- return DEFAULT_CONFIG;
226
+ return global;
202
227
  }
203
228
  }
204
229
 
205
- return DEFAULT_CONFIG;
230
+ return global;
206
231
  }
207
232
 
208
233
  /**
@@ -236,7 +261,8 @@ export function cleanupStaleStateFiles(cwd: string): {
236
261
  scanned: number;
237
262
  deleted: number;
238
263
  } {
239
- const stateDir = join(cwd, ".aide", "state");
264
+ const { root } = findProjectRoot(cwd);
265
+ const stateDir = join(root, ".aide", "state");
240
266
  if (!existsSync(stateDir)) {
241
267
  return { scanned: 0, deleted: 0 };
242
268
  }
@@ -275,7 +301,8 @@ export function cleanupStaleStateFiles(cwd: string): {
275
301
  * Reset HUD state file for clean session start
276
302
  */
277
303
  export function resetHudState(cwd: string): void {
278
- const hudPath = join(cwd, ".aide", "state", "hud.txt");
304
+ const { root } = findProjectRoot(cwd);
305
+ const hudPath = join(root, ".aide", "state", "hud.txt");
279
306
  try {
280
307
  if (existsSync(hudPath)) {
281
308
  writeFileSync(hudPath, "mode:idle");
@@ -303,7 +330,7 @@ export function runSessionInit(
303
330
  dynamic: { sessions: [] },
304
331
  };
305
332
 
306
- if (process.env.AIDE_MEMORY_INJECT === "0") {
333
+ if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
307
334
  return result;
308
335
  }
309
336
 
@@ -318,7 +345,7 @@ export function runSessionInit(
318
345
  // Add --share-import if configured or env var set
319
346
  if (
320
347
  config?.share?.autoImport ||
321
- process.env.AIDE_SHARE_AUTO_IMPORT === "1"
348
+ isTruthy(process.env.AIDE_SHARE_AUTO_IMPORT)
322
349
  ) {
323
350
  args.push("--share-import");
324
351
  }
@@ -333,7 +360,7 @@ export function runSessionInit(
333
360
 
334
361
  const data: SessionInitResult = JSON.parse(output);
335
362
 
336
- if (process.env.AIDE_MEMORY_INJECT === "0") {
363
+ if (isFalsy(process.env.AIDE_MEMORY_INJECT)) {
337
364
  return result;
338
365
  }
339
366
 
@@ -353,6 +380,55 @@ export function runSessionInit(
353
380
  .join("\n");
354
381
  result.dynamic.sessions.push(`${header}:\n${memories}`);
355
382
  }
383
+
384
+ const sources: InjectedSource[] = [];
385
+ for (const m of data.global_memories) {
386
+ sources.push({
387
+ kind: "memory",
388
+ scope: "global",
389
+ id: m.id,
390
+ name: m.tags?.[0] ?? m.category ?? "memory",
391
+ content: m.content,
392
+ category: m.category,
393
+ tags: m.tags,
394
+ score: m.score,
395
+ });
396
+ }
397
+ for (const m of data.project_memories) {
398
+ sources.push({
399
+ kind: "memory",
400
+ scope: "project",
401
+ id: m.id,
402
+ name: m.tags?.[0] ?? m.category ?? "memory",
403
+ content: m.content,
404
+ category: m.category,
405
+ tags: m.tags,
406
+ score: m.score,
407
+ });
408
+ }
409
+ for (const d of data.decisions) {
410
+ sources.push({
411
+ kind: "decision",
412
+ scope: "project",
413
+ id: d.topic,
414
+ name: d.topic,
415
+ content: `${d.value}${d.rationale ? ` (${d.rationale})` : ""}`,
416
+ });
417
+ }
418
+ for (const sess of data.recent_sessions) {
419
+ for (const m of sess.memories) {
420
+ sources.push({
421
+ kind: "session_memory",
422
+ scope: "session",
423
+ id: `${sess.session_id}:${m.content.slice(0, 32)}`,
424
+ name: m.category || "session",
425
+ content: m.content,
426
+ category: m.category,
427
+ sessionId: sess.session_id,
428
+ });
429
+ }
430
+ }
431
+ result.sources = sources;
356
432
  } catch {
357
433
  // Best effort
358
434
  }
@@ -229,40 +229,36 @@ export function loadSkill(path: string): Skill | null {
229
229
  export function discoverSkills(cwd: string, pluginRoot?: string): Skill[] {
230
230
  const skills: Skill[] = [];
231
231
  const seenPaths = new Set<string>();
232
+ // Dedupe by skill name too — the same skill (same `name:` frontmatter)
233
+ // is often present in both repo/skills/ and the plugin-install
234
+ // skills/ dir under different file paths. Without this, matchSkills
235
+ // returns the same skill twice and injects it twice.
236
+ const seenNames = new Set<string>();
237
+
238
+ const add = (file: string) => {
239
+ if (seenPaths.has(file)) return;
240
+ seenPaths.add(file);
241
+ const skill = loadSkill(file);
242
+ if (!skill) return;
243
+ if (seenNames.has(skill.name)) return;
244
+ seenNames.add(skill.name);
245
+ skills.push(skill);
246
+ };
232
247
 
233
248
  // Project-local skills (higher priority)
234
249
  for (const location of SKILL_LOCATIONS) {
235
250
  const dir = join(cwd, location);
236
- const files = findSkillFiles(dir);
237
- for (const file of files) {
238
- if (seenPaths.has(file)) continue;
239
- seenPaths.add(file);
240
- const skill = loadSkill(file);
241
- if (skill) skills.push(skill);
242
- }
251
+ for (const file of findSkillFiles(dir)) add(file);
243
252
  }
244
253
 
245
254
  // Plugin-bundled skills (if pluginRoot provided)
246
255
  if (pluginRoot) {
247
- const pluginSkillDir = join(pluginRoot, "skills");
248
- const files = findSkillFiles(pluginSkillDir);
249
- for (const file of files) {
250
- if (seenPaths.has(file)) continue;
251
- seenPaths.add(file);
252
- const skill = loadSkill(file);
253
- if (skill) skills.push(skill);
254
- }
256
+ for (const file of findSkillFiles(join(pluginRoot, "skills"))) add(file);
255
257
  }
256
258
 
257
259
  // Global skills (lower priority)
258
260
  for (const dir of GLOBAL_SKILL_LOCATIONS) {
259
- const files = findSkillFiles(dir);
260
- for (const file of files) {
261
- if (seenPaths.has(file)) continue;
262
- seenPaths.add(file);
263
- const skill = loadSkill(file);
264
- if (skill) skills.push(skill);
265
- }
261
+ for (const file of findSkillFiles(dir)) add(file);
266
262
  }
267
263
 
268
264
  return skills;
@@ -269,6 +269,18 @@ export function recordToolEvent(
269
269
  if (input.sessionId) args.push(`--session=${input.sessionId}`);
270
270
  if (startLine !== undefined) args.push(`--attr=start_line=${startLine}`);
271
271
  if (endLine !== undefined) args.push(`--attr=end_line=${endLine}`);
272
+ // Capture the command (Bash) or pattern (Grep) text so the repetition
273
+ // detector can group calls by canonical signature instead of lumping
274
+ // every Bash invocation under a single "Bash" bucket. Truncated to keep
275
+ // the attr cheap; the normaliser only uses the first token anyway.
276
+ const cmd = input.toolInput?.command;
277
+ if (typeof cmd === "string" && cmd.length > 0) {
278
+ args.push(`--attr=command=${cmd.slice(0, 500)}`);
279
+ }
280
+ const pattern = input.toolInput?.pattern;
281
+ if (typeof pattern === "string" && pattern.length > 0) {
282
+ args.push(`--attr=pattern=${pattern.slice(0, 200)}`);
283
+ }
272
284
  execFileSync(binary, args, {
273
285
  cwd,
274
286
  timeout: 3000,
package/src/core/types.ts CHANGED
@@ -9,6 +9,16 @@
9
9
  // =============================================================================
10
10
 
11
11
  export interface AideConfig {
12
+ /**
13
+ * When true (default), AIDE refuses to bootstrap if no `.git/` or `.aide/`
14
+ * marker is found walking up from the launched cwd. This prevents the hook
15
+ * from planting an orphan `.aide/` folder in an arbitrary subdirectory of a
16
+ * git repo when `claude` is launched there. Set to false in
17
+ * `~/.aide/config/aide.json` to allow init in non-git directories.
18
+ * Only the global-config value is consulted (the project layer is moot
19
+ * because, if a project root was found, the gate has already passed).
20
+ */
21
+ requireGit?: boolean;
12
22
  share?: {
13
23
  /** Auto-import shared data from .aide/shared/ on session start (default: false) */
14
24
  autoImport?: boolean;
@@ -60,12 +70,14 @@ export interface SessionInitResult {
60
70
  content: string;
61
71
  category: string;
62
72
  tags: string[];
73
+ score?: number;
63
74
  }>;
64
75
  project_memories: Array<{
65
76
  id: string;
66
77
  content: string;
67
78
  category: string;
68
79
  tags: string[];
80
+ score?: number;
69
81
  }>;
70
82
  project_memory_overflow?: boolean;
71
83
  decisions: Array<{ topic: string; value: string; rationale?: string }>;
@@ -80,6 +92,18 @@ export interface SessionInitResult {
80
92
  // Memory Injection
81
93
  // =============================================================================
82
94
 
95
+ export interface InjectedSource {
96
+ kind: "memory" | "decision" | "session_memory";
97
+ scope: "global" | "project" | "session";
98
+ id: string;
99
+ name: string;
100
+ content: string;
101
+ category?: string;
102
+ tags?: string[];
103
+ sessionId?: string;
104
+ score?: number;
105
+ }
106
+
83
107
  export interface MemoryInjection {
84
108
  static: {
85
109
  global: string[];
@@ -90,6 +114,7 @@ export interface MemoryInjection {
90
114
  dynamic: {
91
115
  sessions: string[];
92
116
  };
117
+ sources?: InjectedSource[];
93
118
  }
94
119
 
95
120
  // =============================================================================
@@ -14,6 +14,7 @@ import { readStdin } from "../lib/hook-utils.js";
14
14
  import { findAideBinary } from "../core/aide-client.js";
15
15
  import { cleanupAgent } from "../core/cleanup.js";
16
16
  import { debug } from "../lib/logger.js";
17
+ import { findProjectRoot } from "../lib/project-root.js";
17
18
 
18
19
  const SOURCE = "agent-cleanup";
19
20
 
@@ -48,7 +49,8 @@ async function main(): Promise<void> {
48
49
  if (binary) {
49
50
  const cleared = cleanupAgent(binary, cwd, agentId);
50
51
  if (cleared) {
51
- const logDir = join(cwd, ".aide", "_logs");
52
+ const { root } = findProjectRoot(cwd);
53
+ const logDir = join(root, ".aide", "_logs");
52
54
  if (existsSync(logDir)) {
53
55
  const logPath = join(logDir, "agent-cleanup.log");
54
56
  const timestamp = new Date().toISOString();
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Agent Signals Hook (PreToolUse)
4
+ *
5
+ * Fires before every tool call. In subagent contexts (session has a parent
6
+ * recorded by SubagentStart) it checks for halt/pause flags and unread
7
+ * high-priority messages, blocking or injecting context accordingly.
8
+ *
9
+ * In orchestrator / solo sessions this hook is a no-op — gated on the
10
+ * session having a registered parent. Cost when gated out: one cheap
11
+ * `aide agent identify` call (~10ms).
12
+ */
13
+
14
+ import { execFileSync } from "child_process";
15
+ import { Logger } from "../lib/logger.js";
16
+ import { readStdin } from "../lib/hook-utils.js";
17
+ import { findAideBinary } from "../core/aide-client.js";
18
+ import { emitInjectionEvent } from "../core/read-tracking.js";
19
+
20
+ const SOURCE = "agent-signals";
21
+
22
+ interface PreToolUseInput {
23
+ hook_event_name: "PreToolUse";
24
+ session_id: string;
25
+ tool_name: string;
26
+ cwd: string;
27
+ }
28
+
29
+ interface HookOutput {
30
+ continue: boolean;
31
+ message?: string;
32
+ hookSpecificOutput?: {
33
+ hookEventName: string;
34
+ additionalContext?: string;
35
+ };
36
+ }
37
+
38
+ interface SignalsResponse {
39
+ agent: string;
40
+ halt?: boolean;
41
+ paused?: boolean;
42
+ reason?: string;
43
+ deadline?: string;
44
+ deadline_remaining_sec?: number;
45
+ high_priority_messages?: Array<{
46
+ id: number;
47
+ from: string;
48
+ content: string;
49
+ type?: string;
50
+ }>;
51
+ }
52
+
53
+ let log: Logger | null = null;
54
+
55
+ function passThrough(): void {
56
+ console.log(JSON.stringify({ continue: true }));
57
+ }
58
+
59
+ function block(message: string): void {
60
+ const out: HookOutput = { continue: false, message };
61
+ console.log(JSON.stringify(out));
62
+ }
63
+
64
+ function injectContext(context: string): void {
65
+ const out: HookOutput = {
66
+ continue: true,
67
+ hookSpecificOutput: {
68
+ hookEventName: "PreToolUse",
69
+ additionalContext: context,
70
+ },
71
+ };
72
+ console.log(JSON.stringify(out));
73
+ }
74
+
75
+ function runAide(binary: string, cwd: string, args: string[]): string | null {
76
+ try {
77
+ return execFileSync(binary, args, {
78
+ cwd,
79
+ stdio: ["pipe", "pipe", "pipe"],
80
+ timeout: 2000,
81
+ }).toString();
82
+ } catch (err) {
83
+ log?.debug(`aide ${args.join(" ")} failed: ${err}`);
84
+ return null;
85
+ }
86
+ }
87
+
88
+ // Tools the subagent is still allowed to call while paused — limited to
89
+ // communication primitives so it can report back / receive instruction.
90
+ const PAUSE_ALLOWLIST = new Set([
91
+ "mcp__plugin_aide_aide__message_send",
92
+ "mcp__plugin_aide_aide__message_list",
93
+ "mcp__plugin_aide_aide__message_ack",
94
+ "mcp__plugin_aide_aide__state_get",
95
+ ]);
96
+
97
+ async function main(): Promise<void> {
98
+ try {
99
+ const raw = await readStdin();
100
+ if (!raw.trim()) {
101
+ passThrough();
102
+ return;
103
+ }
104
+ const data: PreToolUseInput = JSON.parse(raw);
105
+ const cwd = data.cwd || process.cwd();
106
+ log = new Logger("agent-signals", cwd);
107
+
108
+ const binary = findAideBinary({
109
+ cwd,
110
+ pluginRoot:
111
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
112
+ });
113
+ if (!binary) {
114
+ passThrough();
115
+ return;
116
+ }
117
+
118
+ // Gate: is this session a registered subagent? identify returns JSON
119
+ // with parent_session if and only if SubagentStart recorded one.
120
+ const idRaw = runAide(binary, cwd, [
121
+ "agent",
122
+ "identify",
123
+ `--agent=${data.session_id}`,
124
+ ]);
125
+ if (!idRaw) {
126
+ passThrough();
127
+ return;
128
+ }
129
+ let identity: Record<string, string>;
130
+ try {
131
+ identity = JSON.parse(idRaw);
132
+ } catch {
133
+ passThrough();
134
+ return;
135
+ }
136
+ if (!identity.parent_session) {
137
+ // Not a subagent — orchestrator/solo session. No-op.
138
+ passThrough();
139
+ return;
140
+ }
141
+
142
+ // Pull signal snapshot.
143
+ const sigRaw = runAide(binary, cwd, [
144
+ "agent",
145
+ "signals",
146
+ `--agent=${data.session_id}`,
147
+ ]);
148
+ if (!sigRaw) {
149
+ passThrough();
150
+ return;
151
+ }
152
+ let sig: SignalsResponse;
153
+ try {
154
+ sig = JSON.parse(sigRaw);
155
+ } catch {
156
+ passThrough();
157
+ return;
158
+ }
159
+
160
+ // 1. Halt — hard stop with reason surfaced to model.
161
+ if (sig.halt) {
162
+ const reason = sig.reason || "halted by orchestrator";
163
+ log?.info(`halt active for ${data.session_id}: ${reason}`);
164
+ block(
165
+ `[aide] This subagent has been halted by the orchestrator. Reason: ${reason}\n` +
166
+ `Stop work. You may still send a final status message via aide message send --to=<orchestrator>.`,
167
+ );
168
+ return;
169
+ }
170
+
171
+ // 2. Pause — block all tools except the comm allowlist.
172
+ if (sig.paused && !PAUSE_ALLOWLIST.has(data.tool_name)) {
173
+ log?.info(`paused — blocking ${data.tool_name}`);
174
+ block(
175
+ `[aide] This subagent is paused by the orchestrator. Only messaging tools are allowed until resumed.`,
176
+ );
177
+ return;
178
+ }
179
+
180
+ // 3. Deadline approaching — inject warning at < 20% remaining.
181
+ let deadlineWarn = "";
182
+ if (sig.deadline_remaining_sec !== undefined) {
183
+ if (sig.deadline_remaining_sec <= 0) {
184
+ block(
185
+ `[aide] Soft deadline reached (${sig.deadline}). Halting per orchestrator policy.`,
186
+ );
187
+ return;
188
+ }
189
+ if (sig.deadline_remaining_sec < 60 * 5) {
190
+ deadlineWarn = `[aide] Deadline ${sig.deadline} — ~${sig.deadline_remaining_sec}s remaining.`;
191
+ }
192
+ }
193
+
194
+ // 4. High-priority messages — inject + ack so we don't re-inject next call.
195
+ const parts: string[] = [];
196
+ if (deadlineWarn) parts.push(deadlineWarn);
197
+ if (sig.high_priority_messages && sig.high_priority_messages.length > 0) {
198
+ parts.push("[aide] Mid-flight messages from orchestrator:");
199
+ for (const m of sig.high_priority_messages) {
200
+ parts.push(`- (${m.from}${m.type ? `, ${m.type}` : ""}): ${m.content}`);
201
+ runAide(binary, cwd, [
202
+ "message",
203
+ "ack",
204
+ String(m.id),
205
+ `--agent=${data.session_id}`,
206
+ ]);
207
+ }
208
+ }
209
+
210
+ if (parts.length > 0) {
211
+ const ctx = parts.join("\n");
212
+ try {
213
+ emitInjectionEvent(binary, cwd, {
214
+ source: SOURCE,
215
+ subtype: "signal",
216
+ content: ctx,
217
+ sessionId: data.session_id,
218
+ attrs: {
219
+ tool: data.tool_name,
220
+ ...(sig.deadline ? { deadline: sig.deadline } : {}),
221
+ high_priority_messages: String(
222
+ sig.high_priority_messages?.length ?? 0,
223
+ ),
224
+ },
225
+ });
226
+ } catch {
227
+ // Non-fatal
228
+ }
229
+ injectContext(ctx);
230
+ return;
231
+ }
232
+
233
+ passThrough();
234
+ } catch (err) {
235
+ log?.error(`agent-signals failed: ${err}`);
236
+ passThrough();
237
+ }
238
+ }
239
+
240
+ process.on("uncaughtException", () => {
241
+ passThrough();
242
+ process.exit(0);
243
+ });
244
+ process.on("unhandledRejection", () => {
245
+ passThrough();
246
+ process.exit(0);
247
+ });
248
+
249
+ main();
@@ -16,6 +16,8 @@ import {
16
16
  getCheckableFilePath,
17
17
  getContentToCheck,
18
18
  } from "../core/comment-checker.js";
19
+ import { findAideBinary } from "../core/aide-client.js";
20
+ import { emitInjectionEvent } from "../core/read-tracking.js";
19
21
 
20
22
  const SOURCE = "comment-checker";
21
23
 
@@ -53,6 +55,8 @@ async function main(): Promise<void> {
53
55
  const data: HookInput = JSON.parse(input);
54
56
  const toolName = data.tool_name || "";
55
57
  const toolInput = data.tool_input || {};
58
+ const cwd = data.cwd || process.cwd();
59
+ const sessionId = data.session_id || "";
56
60
 
57
61
  // Only check Write/Edit/MultiEdit tool calls
58
62
  const filePath = getCheckableFilePath(toolName, toolInput);
@@ -76,6 +80,28 @@ async function main(): Promise<void> {
76
80
  SOURCE,
77
81
  `Detected ${result.suspiciousCount} suspicious comments in ${filePath}`,
78
82
  );
83
+ try {
84
+ const binary = findAideBinary({
85
+ cwd,
86
+ pluginRoot:
87
+ process.env.AIDE_PLUGIN_ROOT || process.env.CLAUDE_PLUGIN_ROOT,
88
+ });
89
+ if (binary && result.warning) {
90
+ emitInjectionEvent(binary, cwd, {
91
+ source: SOURCE,
92
+ subtype: "guard",
93
+ content: result.warning,
94
+ sessionId,
95
+ attrs: {
96
+ tool: toolName,
97
+ file: filePath,
98
+ suspicious_count: String(result.suspiciousCount),
99
+ },
100
+ });
101
+ }
102
+ } catch {
103
+ // Non-fatal
104
+ }
79
105
  const output: HookOutput = {
80
106
  continue: true,
81
107
  hookSpecificOutput: {