@coworker-jp/aidr 0.0.1

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,759 @@
1
+ // SOURCE: docker/user-frontend/src/lib/scanner-templates.ts
2
+ // ESM port of the scanner hook templates. Keep in sync with the TS source.
3
+ // Templates are byte-identical to the canonical .{agent}/ files in ai-scanner
4
+ // repo (Claude / Cursor / Windsurf / Kiro) — only the access key value differs.
5
+
6
+ import { buildClaudeSettings, buildGeminiSettings } from "./merge.mjs";
7
+
8
+ export function getSettingsJson(accessKey, scope = "project") {
9
+ return JSON.stringify(buildClaudeSettings(accessKey, scope), null, 2) + "\n";
10
+ }
11
+
12
+ // start_scanner.sh — parameterized by the verify endpoint (prod / dev).
13
+ // `agentType` is currently unused inside the script (endpoint-info has moved
14
+ // to schedule-only invocation via aidr --scheduled), but the parameter
15
+ // is kept for signature stability with callers and the wider TS template.
16
+ // eslint-disable-next-line no-unused-vars
17
+ export function getStartScannerSh(downloadBase = "https://pytd2jd5p7.execute-api.ap-northeast-1.amazonaws.com/download", agentType = "claude-code") {
18
+ return `#!/bin/bash
19
+ # SessionStart hook: Setup ai-scanner for the current platform
20
+ # - Linux (x86_64 / aarch64): download native binary from S3
21
+ # - macOS (arm64 / Apple Silicon): download native binary from S3 (ad-hoc signed)
22
+ # - Binary is unsigned by 3rd party dev cert — quarantine xattr is stripped after download
23
+ # - Fail-open: never block Claude Code startup
24
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
25
+ BIN_DIR="$(dirname "$BIN")"
26
+ PULL_STAMP="$HOME/.claude/.ai-scanner-last-pull"
27
+ PULL_INTERVAL=300
28
+ DOWNLOAD_BASE="${downloadBase}"
29
+
30
+ mkdir -p "$BIN_DIR" "$HOME/.claude/quarantine" "$HOME/.claude/ai-scanner"
31
+
32
+ # macOS has no base \`timeout\` (it's in GNU coreutils). Shim it so hooks work
33
+ # on fresh Macs without brew install coreutils.
34
+ if ! command -v timeout >/dev/null 2>&1; then
35
+ if command -v gtimeout >/dev/null 2>&1; then
36
+ timeout() { gtimeout "$@"; }
37
+ else
38
+ timeout() { shift; "$@"; }
39
+ fi
40
+ fi
41
+
42
+ # Generic throttle: true (0) if stamp missing or older than PULL_INTERVAL
43
+ should_check_update() {
44
+ [ ! -f "$PULL_STAMP" ] && return 0
45
+ local last=$(cat "$PULL_STAMP" 2>/dev/null || echo 0)
46
+ [ $(($(date +%s) - last)) -ge "$PULL_INTERVAL" ]
47
+ }
48
+
49
+ # Download binary from $1 to $BIN, strip quarantine (macOS), chmod +x.
50
+ # Uses curl's native --max-time so no external \`timeout\` dependency.
51
+ # Access key is required by the /download endpoint.
52
+ download_binary() {
53
+ local url="$1"
54
+ local tmp=$(mktemp "\${BIN_DIR}/ai-scanner.XXXXXX")
55
+ local auth_args=()
56
+ if [ -n "\${AI_SCANNER_ACCESS_KEY:-}" ]; then
57
+ auth_args=(-H "x-access-key: \${AI_SCANNER_ACCESS_KEY}")
58
+ fi
59
+ if curl -sfL --max-time 60 "\${auth_args[@]}" -o "$tmp" "$url" 2>/dev/null; then
60
+ chmod +x "$tmp"
61
+ # macOS: strip Gatekeeper quarantine so unsigned binary can execute
62
+ # (no-op on Linux where xattr isn't typically present)
63
+ xattr -d com.apple.quarantine "$tmp" 2>/dev/null || true
64
+ mv -f "$tmp" "$BIN"
65
+ return 0
66
+ fi
67
+ rm -f "$tmp"
68
+ return 1
69
+ }
70
+
71
+ OS=$(uname -s)
72
+ ARCH=$(uname -m)
73
+
74
+ case "$OS" in
75
+ Linux)
76
+ case "$ARCH" in
77
+ x86_64) PLATFORM_SLUG="linux-x86_64" ;;
78
+ aarch64|arm64) PLATFORM_SLUG="linux-aarch64" ;;
79
+ *)
80
+ echo "Warning: Unsupported Linux architecture: $ARCH (expected x86_64 or aarch64)" >&2
81
+ exit 0
82
+ ;;
83
+ esac
84
+ ;;
85
+ Darwin)
86
+ case "$ARCH" in
87
+ arm64) PLATFORM_SLUG="darwin-aarch64" ;;
88
+ x86_64)
89
+ echo "Warning: ai-scanner does not support Intel Mac (x86_64). Apple Silicon required." >&2
90
+ exit 0
91
+ ;;
92
+ *)
93
+ echo "Warning: Unsupported macOS architecture: $ARCH" >&2
94
+ exit 0
95
+ ;;
96
+ esac
97
+ ;;
98
+ *)
99
+ echo "Warning: Unsupported OS for ai-scanner: $OS" >&2
100
+ exit 0
101
+ ;;
102
+ esac
103
+
104
+ # Binary missing → fetch it
105
+ if [ ! -x "$BIN" ]; then
106
+ if download_binary "\${DOWNLOAD_BASE}/\${PLATFORM_SLUG}"; then
107
+ echo "ai-scanner binary installed (\${PLATFORM_SLUG})" >&2
108
+ else
109
+ echo "Warning: Failed to download ai-scanner binary from \${DOWNLOAD_BASE}/\${PLATFORM_SLUG}" >&2
110
+ fi
111
+ exit 0
112
+ fi
113
+
114
+ # Binary exists — throttled self-update check
115
+ if should_check_update; then
116
+ "$BIN" update 2>/dev/null || true
117
+ date +%s > "$PULL_STAMP"
118
+ fi
119
+
120
+ exit 0
121
+ `;
122
+ }
123
+
124
+ export const STOP_SCANNER_SH = `#!/bin/bash
125
+ # Stop hook: intentionally does nothing.
126
+ # Container stays running for next session startup speed.
127
+ exit 0
128
+ `;
129
+
130
+ // Wraps every ai-scanner invocation in a timeout so a hung binary cannot
131
+ // indefinitely block the editor. Exit 124 from GNU `timeout` means "timed
132
+ // out" — we map that to 0 (fail-open), matching the project-wide policy
133
+ // that hook failures never block user work.
134
+ //
135
+ // Portability: Linux ships `timeout` in coreutils; Homebrew macOS exposes
136
+ // it as `gtimeout`; a bare macOS (no coreutils) has neither, in which case
137
+ // we exec without a timeout. This keeps the editor-level timeout as the
138
+ // only safety net on that platform, which is acceptable — installing
139
+ // coreutils is the documented remedy.
140
+ //
141
+ // Default 30s: the v1.4.x scanner's cold-start path does rules-decrypt +
142
+ // advisory_client DDB round-trip + YARA + Quarantine (typosquat + registry
143
+ // metadata) + Semgrep, measured ~8.5s on admin@coworker.co.jp's Linux box
144
+ // with a warm cache — cold-start + slow network can push that past 12s and
145
+ // exhaust the previous default, causing a known-malicious file to slip
146
+ // through as a false-negative (timeout → 124 → fail-open 0). 30s is
147
+ // generous enough to absorb cold-start variance while still bounding a
148
+ // truly hung scanner. AI_SCANNER_HOOK_TIMEOUT (seconds) lets operators
149
+ // tune further per-environment.
150
+ const SCAN_HELPERS = `run_scanner() {
151
+ if command -v timeout >/dev/null 2>&1; then
152
+ timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
153
+ elif command -v gtimeout >/dev/null 2>&1; then
154
+ gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
155
+ else
156
+ "$BIN" "$@"
157
+ fi
158
+ local rc=$?
159
+ [ $rc -eq 124 ] && return 0
160
+ return $rc
161
+ }`;
162
+
163
+ export const SCAN_BASH_SH = `#!/bin/bash
164
+ # PostToolUse hook for Bash: Scan command output.
165
+ # Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
166
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
167
+ [ ! -x "$BIN" ] && exit 0
168
+
169
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
170
+
171
+ ${SCAN_HELPERS}
172
+
173
+ INPUT=$(cat)
174
+ COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
175
+ jq -r '.tool_response // ""' <<<"$INPUT" \\
176
+ | run_scanner scan bash-output --command "$COMMAND"
177
+ `;
178
+
179
+ export const SCAN_BASH_INPUT_SH = `#!/bin/bash
180
+ # PreToolUse hook for Bash: Scan command before execution.
181
+ # Thin Claude Code adapter — Rust handles cap, detection log, actor resolution.
182
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
183
+ [ ! -x "$BIN" ] && exit 0
184
+
185
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
186
+
187
+ ${SCAN_HELPERS}
188
+
189
+ CONTENT=$(jq -r '.tool_input.command // ""')
190
+ [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
191
+
192
+ printf '%s' "$CONTENT" | run_scanner scan bash-input
193
+ `;
194
+
195
+ export const SCAN_EDIT_SH = `#!/bin/bash
196
+ # PreToolUse hook for Edit/MultiEdit: Scan new content before write.
197
+ # Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
198
+ # Only the Edit-vs-MultiEdit content extraction is Claude Code specific.
199
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
200
+ [ ! -x "$BIN" ] && exit 0
201
+
202
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
203
+
204
+ ${SCAN_HELPERS}
205
+
206
+ INPUT=$(cat)
207
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
208
+ TOOL_NAME=$(jq -r '.tool_name // "unknown"' <<<"$INPUT")
209
+
210
+ case "$TOOL_NAME" in
211
+ Edit) CONTENT=$(jq -r '.tool_input.new_string // ""' <<<"$INPUT") ;;
212
+ MultiEdit) CONTENT=$(jq -r '[.tool_input.edits[]?.new_string // ""] | join("\\n")' <<<"$INPUT") ;;
213
+ *) CONTENT=$(jq -r '.tool_input.new_string // .tool_input.content // ""' <<<"$INPUT") ;;
214
+ esac
215
+
216
+ [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
217
+
218
+ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
219
+ `;
220
+
221
+ export const SCAN_READ_SH = `#!/bin/bash
222
+ # PostToolUse hook for Read: Scan file content Claude just read.
223
+ # Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
224
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
225
+ [ ! -x "$BIN" ] && exit 0
226
+
227
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
228
+
229
+ ${SCAN_HELPERS}
230
+
231
+ INPUT=$(cat)
232
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
233
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
234
+
235
+ # tool_response is an object: {type, file: {filePath, content, ...}}.
236
+ jq -r '.tool_response.file.content // .tool_response // ""' <<<"$INPUT" \\
237
+ | run_scanner scan read-output --file "$FILE_PATH"
238
+ `;
239
+
240
+ export const SCAN_EDIT_OUTPUT_SH = `#!/bin/bash
241
+ # PostToolUse hook for Edit/MultiEdit/Write: Scan final file content.
242
+ # Thin Claude Code adapter — Rust handles cap, exclude dirs, detection log.
243
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
244
+ [ ! -x "$BIN" ] && exit 0
245
+
246
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
247
+
248
+ ${SCAN_HELPERS}
249
+
250
+ INPUT=$(cat)
251
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
252
+
253
+ # tool_response for Edit/Write: {filePath, content, ...} or nested {file: {content}}.
254
+ jq -r '.tool_response.file.content // .tool_response.content // .tool_response // ""' <<<"$INPUT" \\
255
+ | run_scanner scan edit-output --file "$FILE_PATH"
256
+ `;
257
+
258
+ export const SCAN_READ_INPUT_SH = `#!/bin/bash
259
+ # PreToolUse hook for Read: Scan file content before Claude reads it.
260
+ # This is a thin Claude Code adapter — all scanning logic lives in the Rust
261
+ # binary, including actor resolution, exclude-dir filtering, content cap, and
262
+ # detection-log writing.
263
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
264
+ [ ! -x "$BIN" ] && exit 0
265
+
266
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
267
+
268
+ ${SCAN_HELPERS}
269
+
270
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""')
271
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
272
+
273
+ # Rust reads the file itself (no need to pipe content). Exit code 2 = detection.
274
+ run_scanner scan read-input --file "$FILE_PATH" </dev/null
275
+ `;
276
+
277
+ export const SCAN_WEBFETCH_SH = `#!/bin/bash
278
+ # PreToolUse hook for WebFetch: Scan URL before Claude fetches it.
279
+ # Thin Claude Code adapter — Rust handles phishing scoring + YARA + detection log.
280
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
281
+ [ ! -x "$BIN" ] && exit 0
282
+
283
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
284
+
285
+ ${SCAN_HELPERS}
286
+
287
+ URL=$(jq -r '.tool_input.url // ""')
288
+ [ -z "$URL" ] || [ "$URL" = "null" ] && exit 0
289
+
290
+ run_scanner scan webfetch --url "$URL" </dev/null
291
+ `;
292
+
293
+ export const SCAN_WEBSEARCH_SH = `#!/bin/bash
294
+ # PostToolUse hook for WebSearch: Scan search results for phishing URLs.
295
+ # Thin Claude Code adapter — Rust handles cap, phishing scoring, detection log.
296
+ BIN="\${AI_SCANNER_BIN:-$HOME/.claude/bin/ai-scanner}"
297
+ [ ! -x "$BIN" ] && exit 0
298
+
299
+ export AI_SCANNER_DETECTION_LOG="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs/detections.log"
300
+
301
+ ${SCAN_HELPERS}
302
+
303
+ jq -r '.tool_response // ""' | run_scanner scan websearch`;
304
+
305
+ export const LOG_WEB_URLS_SH = `#!/bin/bash
306
+ # Log URLs from WebSearch and WebFetch tool calls
307
+ # Hook input is received via stdin as JSON
308
+
309
+ LOG_DIR="\${CLAUDE_PROJECT_DIR:-.}/.claude/logs"
310
+ LOG_FILE="$LOG_DIR/web_urls.log"
311
+
312
+ mkdir -p "$LOG_DIR"
313
+
314
+ INPUT=$(cat)
315
+
316
+ TOOL_NAME=$(echo "$INPUT" | jq -r '.tool_name // "unknown"')
317
+ TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
318
+
319
+ if [ "$TOOL_NAME" = "WebSearch" ]; then
320
+ QUERY=$(echo "$INPUT" | jq -r '.tool_input.query // "unknown"')
321
+ echo "\${TIMESTAMP} [WebSearch] query=\\"\${QUERY}\\"" >> "$LOG_FILE"
322
+ elif [ "$TOOL_NAME" = "WebFetch" ]; then
323
+ URL=$(echo "$INPUT" | jq -r '.tool_input.url // "unknown"')
324
+ echo "\${TIMESTAMP} [WebFetch] url=\\"\${URL}\\"" >> "$LOG_FILE"
325
+ fi
326
+
327
+ exit 0
328
+ `;
329
+
330
+ // env.sh shared by Cursor/Windsurf/Kiro/Codex (byte-identical to canonical,
331
+ // only the access key value differs).
332
+ //
333
+ // Exposes a `run_scanner` bash function that every hook adapter uses to
334
+ // invoke the scanner binary under a timeout. A hung scanner must never
335
+ // stall the editor's tool call (fail-open is the project policy).
336
+ export function agentEnvSh(accessKey, agentDir) {
337
+ return `#!/bin/bash
338
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
339
+ export AI_SCANNER_BIN="\${AI_SCANNER_BIN:-$HOME/${agentDir}/bin/ai-scanner}"
340
+ export AI_SCANNER_ACCESS_KEY="${accessKey}"
341
+ export AI_SCANNER_LEVEL="\${AI_SCANNER_LEVEL:-high}"
342
+ export AI_SCANNER_EXCLUDE_DIRS="\${AI_SCANNER_EXCLUDE_DIRS:-}"
343
+ export AI_SCANNER_QUARANTINE_DAYS="\${AI_SCANNER_QUARANTINE_DAYS:-3}"
344
+ export AI_SCANNER_QUARANTINE_MODE="\${AI_SCANNER_QUARANTINE_MODE:-scan}"
345
+ export AI_SCANNER_EXFIL_MODE="\${AI_SCANNER_EXFIL_MODE:-on}"
346
+ export AI_SCANNER_WEBFETCH_YARA_THRESHOLD="\${AI_SCANNER_WEBFETCH_YARA_THRESHOLD:-50}"
347
+ export AI_SCANNER_HOOK_TIMEOUT="\${AI_SCANNER_HOOK_TIMEOUT:-30}"
348
+ export AI_SCANNER_DETECTION_LOG="\${AI_SCANNER_DETECTION_LOG:-$SCRIPT_DIR/../logs/detections.log}"
349
+ export CLAUDE_PROJECT_DIR="\${CLAUDE_PROJECT_DIR:-.}"
350
+
351
+ # Wraps "$AI_SCANNER_BIN \$*" in an optional timeout. Exit 124 from GNU
352
+ # \`timeout\` (timed out) is mapped to 0 so a hung scanner never blocks the
353
+ # tool call. Bare macOS (no coreutils) falls through without a timeout —
354
+ # install coreutils (\`brew install coreutils\`) to get the safety net.
355
+ run_scanner() {
356
+ if command -v timeout >/dev/null 2>&1; then
357
+ timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_SCANNER_BIN}" "$@"
358
+ elif command -v gtimeout >/dev/null 2>&1; then
359
+ gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT}" "\${AI_SCANNER_BIN}" "$@"
360
+ else
361
+ "\${AI_SCANNER_BIN}" "$@"
362
+ fi
363
+ local rc=$?
364
+ [ $rc -eq 124 ] && return 0
365
+ return $rc
366
+ }
367
+ `;
368
+ }
369
+
370
+ // ── Cursor ────────────────────────────────────────────────────────────
371
+ // hooks.json shape moved to merge.mjs (buildCursorHooksJson) so cursor.mjs can
372
+ // merge with any user-edited existing hooks.json instead of overwriting.
373
+
374
+ export function getCursorSessionInitSh(accessKey) {
375
+ return `#!/bin/bash
376
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
377
+ source "$SCRIPT_DIR/env.sh"
378
+ cat > /dev/null
379
+ bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
380
+ cat <<EOF
381
+ {"env":{"AI_SCANNER_BIN":"\${AI_SCANNER_BIN}","AI_SCANNER_ACCESS_KEY":"${accessKey}","AI_SCANNER_LEVEL":"high"}}
382
+ EOF
383
+ `;
384
+ }
385
+
386
+ export const CURSOR_PRE_SHELL_SH = `#!/bin/bash
387
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
388
+ source "$SCRIPT_DIR/env.sh"
389
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
390
+ export AI_SCANNER_DETECTION_LOG
391
+ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
392
+ `;
393
+
394
+ export const CURSOR_PRE_READ_SH = `#!/bin/bash
395
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
396
+ source "$SCRIPT_DIR/env.sh"
397
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
398
+ export AI_SCANNER_DETECTION_LOG
399
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""')
400
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
401
+ run_scanner scan read-input --file "$FILE_PATH" </dev/null
402
+ `;
403
+
404
+ export const CURSOR_PRE_EDIT_SH = `#!/bin/bash
405
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
406
+ source "$SCRIPT_DIR/env.sh"
407
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
408
+ export AI_SCANNER_DETECTION_LOG
409
+ INPUT=$(cat)
410
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
411
+ TOOL_NAME=$(jq -r '.tool_name // "unknown"' <<<"$INPUT")
412
+ case "$TOOL_NAME" in
413
+ Edit) CONTENT=$(jq -r '.tool_input.new_string // ""' <<<"$INPUT") ;;
414
+ MultiEdit) CONTENT=$(jq -r '[.tool_input.edits[]?.new_string // ""] | join("\\n")' <<<"$INPUT") ;;
415
+ *) CONTENT=$(jq -r '.tool_input.new_string // .tool_input.content // ""' <<<"$INPUT") ;;
416
+ esac
417
+ [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
418
+ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
419
+ `;
420
+
421
+ export const CURSOR_POST_SHELL_SH = `#!/bin/bash
422
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
423
+ source "$SCRIPT_DIR/env.sh"
424
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
425
+ export AI_SCANNER_DETECTION_LOG
426
+ INPUT=$(cat)
427
+ COMMAND=$(jq -r '.tool_output // ""' <<<"$INPUT" | head -1 && jq -r '.tool_input.command // ""' <<<"$INPUT")
428
+ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan bash-output --command "$(jq -r '.tool_input.command // ""' <<<"$INPUT")"
429
+ `;
430
+
431
+ export const CURSOR_POST_READ_SH = `#!/bin/bash
432
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
433
+ source "$SCRIPT_DIR/env.sh"
434
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
435
+ export AI_SCANNER_DETECTION_LOG
436
+ INPUT=$(cat)
437
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
438
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
439
+ jq -r '.tool_output // ""' <<<"$INPUT" | run_scanner scan read-output --file "$FILE_PATH"
440
+ `;
441
+
442
+ export const CURSOR_POST_WRITE_SH = `#!/bin/bash
443
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
444
+ source "$SCRIPT_DIR/env.sh"
445
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
446
+ export AI_SCANNER_DETECTION_LOG
447
+ INPUT=$(cat)
448
+ FILE_PATH=$(jq -r '.tool_input.file_path // ""' <<<"$INPUT")
449
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
450
+ [ ! -f "$FILE_PATH" ] && exit 0
451
+ run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
452
+ `;
453
+
454
+ // ── Windsurf ─────────────────────────────────────────────────────────
455
+ // hooks.json shape moved to merge.mjs (buildWindsurfHooksJson).
456
+
457
+ export const WINDSURF_ENSURE_SCANNER_SH = `#!/bin/bash
458
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
459
+ source "$SCRIPT_DIR/env.sh"
460
+ BIN="$AI_SCANNER_BIN"
461
+ LOCK_FILE="/tmp/.ai-scanner-ensure.lock"
462
+ if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
463
+ return 0 2>/dev/null || true
464
+ fi
465
+ (
466
+ flock -w 120 200 || exit 0
467
+ if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then exit 0; fi
468
+ bash "$SCRIPT_DIR/start_scanner.sh"
469
+ ) 200>"$LOCK_FILE"
470
+ return 0 2>/dev/null || true
471
+ `;
472
+
473
+ export const WINDSURF_PRE_COMMAND_SH = `#!/bin/bash
474
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
475
+ source "$SCRIPT_DIR/ensure_scanner.sh"
476
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
477
+ export AI_SCANNER_DETECTION_LOG
478
+ CONTENT=$(jq -r '.tool_info.command_line // ""')
479
+ [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
480
+ printf '%s' "$CONTENT" | run_scanner scan bash-input
481
+ `;
482
+
483
+ export const WINDSURF_PRE_READ_SH = `#!/bin/bash
484
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
485
+ source "$SCRIPT_DIR/ensure_scanner.sh"
486
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
487
+ export AI_SCANNER_DETECTION_LOG
488
+ FILE_PATH=$(jq -r '.tool_info.file_path // ""')
489
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
490
+ run_scanner scan read-input --file "$FILE_PATH" </dev/null
491
+ `;
492
+
493
+ export const WINDSURF_PRE_WRITE_SH = `#!/bin/bash
494
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
495
+ source "$SCRIPT_DIR/ensure_scanner.sh"
496
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
497
+ export AI_SCANNER_DETECTION_LOG
498
+ INPUT=$(cat)
499
+ FILE_PATH=$(jq -r '.tool_info.file_path // ""' <<<"$INPUT")
500
+ CONTENT=$(jq -r '[.tool_info.edits[]?.new_string // ""] | join("\\n")' <<<"$INPUT")
501
+ [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
502
+ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
503
+ `;
504
+
505
+ export const WINDSURF_POST_READ_SH = `#!/bin/bash
506
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
507
+ source "$SCRIPT_DIR/ensure_scanner.sh"
508
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
509
+ export AI_SCANNER_DETECTION_LOG
510
+ FILE_PATH=$(jq -r '.tool_info.file_path // ""')
511
+ [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
512
+ run_scanner scan read-output --file "$FILE_PATH" < "$FILE_PATH"
513
+ `;
514
+
515
+ export const WINDSURF_POST_WRITE_SH = `#!/bin/bash
516
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
517
+ source "$SCRIPT_DIR/ensure_scanner.sh"
518
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
519
+ export AI_SCANNER_DETECTION_LOG
520
+ FILE_PATH=$(jq -r '.tool_info.file_path // ""')
521
+ [ -z "$FILE_PATH" ] || [ ! -f "$FILE_PATH" ] && exit 0
522
+ run_scanner scan edit-output --file "$FILE_PATH" < "$FILE_PATH"
523
+ `;
524
+
525
+ // ── Kiro ─────────────────────────────────────────────────────────────
526
+ export function getKiroAgentJson(scope = "project", absHome = "") {
527
+ const p = scope === "user" ? `${absHome}/.kiro/coworker-ai` : ".kiro/coworker-ai";
528
+ // timeout_ms is Kiro's native field. Default is 30000ms (30s); explicitly
529
+ // pin to 15000ms so a stalled scanner is killed by the editor in time,
530
+ // and 60000ms for agentSpawn to give the binary fetch room. The
531
+ // shell-side run_scanner cap (12s) fires first under normal conditions.
532
+ return JSON.stringify({
533
+ name: "ai-scanner",
534
+ description: "CoWorker AIDR security scanner",
535
+ tools: ["*"],
536
+ hooks: {
537
+ agentSpawn: [{ command: `${p}/start.sh`, timeout_ms: 60000 }],
538
+ preToolUse: [
539
+ { matcher: "shell", command: `${p}/pre_shell.sh`, timeout_ms: 15000 },
540
+ { matcher: "read", command: `${p}/pre_read.sh`, timeout_ms: 15000 },
541
+ { matcher: "write", command: `${p}/pre_write.sh`, timeout_ms: 15000 },
542
+ ],
543
+ postToolUse: [
544
+ { matcher: "shell", command: `${p}/post_shell.sh`, timeout_ms: 15000 },
545
+ { matcher: "read", command: `${p}/post_read.sh`, timeout_ms: 15000 },
546
+ { matcher: "write", command: `${p}/post_write.sh`, timeout_ms: 15000 },
547
+ ],
548
+ },
549
+ }, null, 2);
550
+ }
551
+
552
+ export const KIRO_START_SH = `#!/bin/bash
553
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
554
+ source "$SCRIPT_DIR/env.sh"
555
+ exec "$SCRIPT_DIR/start_scanner.sh"
556
+ `;
557
+
558
+ export const KIRO_PRE_SHELL_SH = `#!/bin/bash
559
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
560
+ source "$SCRIPT_DIR/env.sh"
561
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
562
+ export AI_SCANNER_DETECTION_LOG
563
+ jq -r '.tool_input.command // ""' | run_scanner scan bash-input
564
+ `;
565
+
566
+ export const KIRO_PRE_READ_SH = `#!/bin/bash
567
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
568
+ source "$SCRIPT_DIR/env.sh"
569
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
570
+ export AI_SCANNER_DETECTION_LOG
571
+ FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""')
572
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
573
+ run_scanner scan read-input --file "$FILE_PATH" </dev/null
574
+ `;
575
+
576
+ export const KIRO_PRE_WRITE_SH = `#!/bin/bash
577
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
578
+ source "$SCRIPT_DIR/env.sh"
579
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
580
+ export AI_SCANNER_DETECTION_LOG
581
+ INPUT=$(cat)
582
+ FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
583
+ CONTENT=$(jq -r '.tool_input.new_string // .tool_input.new_str // .tool_input.file_text // .tool_input.content // ""' <<<"$INPUT")
584
+ [ -z "$CONTENT" ] || [ "$CONTENT" = "null" ] && exit 0
585
+ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FILE_PATH"
586
+ `;
587
+
588
+ export const KIRO_POST_SHELL_SH = `#!/bin/bash
589
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
590
+ source "$SCRIPT_DIR/env.sh"
591
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
592
+ export AI_SCANNER_DETECTION_LOG
593
+ INPUT=$(cat)
594
+ COMMAND=$(jq -r '.tool_input.command // ""' <<<"$INPUT")
595
+ RESPONSE=$(jq -r '.tool_response | if type=="object" then (.result[0].stdout // tostring) else . end' <<<"$INPUT")
596
+ printf '%s' "$RESPONSE" | run_scanner scan bash-output --command "$COMMAND"
597
+ `;
598
+
599
+ export const KIRO_POST_READ_SH = `#!/bin/bash
600
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
601
+ source "$SCRIPT_DIR/env.sh"
602
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
603
+ export AI_SCANNER_DETECTION_LOG
604
+ INPUT=$(cat)
605
+ FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.operations[0].path // ""' <<<"$INPUT")
606
+ [ -z "$FILE_PATH" ] || [ "$FILE_PATH" = "null" ] && exit 0
607
+ RESPONSE=$(jq -r '.tool_response | if type=="object" then (.result | if type=="array" then join("\\n") else tostring end) else . end' <<<"$INPUT")
608
+ printf '%s' "$RESPONSE" | run_scanner scan read-output --file "$FILE_PATH"
609
+ `;
610
+
611
+ export const KIRO_POST_WRITE_SH = `#!/bin/bash
612
+ SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
613
+ source "$SCRIPT_DIR/env.sh"
614
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
615
+ export AI_SCANNER_DETECTION_LOG
616
+ INPUT=$(cat)
617
+ FILE_PATH=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT")
618
+ RESPONSE=$(jq -r '.tool_response | if type=="object" then (.result | if type=="array" then join("\\n") else tostring end) else . end' <<<"$INPUT")
619
+ printf '%s' "$RESPONSE" | run_scanner scan edit-output --file "$FILE_PATH"
620
+ `;
621
+
622
+ // ── Gemini / Codex ────────────────────────────────────────────────────
623
+ // Thin wrappers that shell out to ai-scanner via generic bash-input scan.
624
+
625
+ export function getGeminiSettingsJson(accessKey, binary) {
626
+ return JSON.stringify(buildGeminiSettings(accessKey, binary), null, 2) + "\n";
627
+ }
628
+
629
+ export function getGeminiPreHookSh(binary) {
630
+ return `#!/bin/bash
631
+ # Gemini CLI BeforeTool hook wrapper.
632
+ # Doesn't source env.sh (Gemini binds the binary path at install time), so
633
+ # the run_scanner timeout helper is inlined here. See agentEnvSh for the
634
+ # canonical version and the rationale for fail-open on exit 124.
635
+ BIN="${binary}"
636
+ [ ! -x "$BIN" ] && exit 0
637
+
638
+ run_scanner() {
639
+ if command -v timeout >/dev/null 2>&1; then
640
+ timeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
641
+ elif command -v gtimeout >/dev/null 2>&1; then
642
+ gtimeout --kill-after=2s "\${AI_SCANNER_HOOK_TIMEOUT:-30}" "$BIN" "$@"
643
+ else
644
+ "$BIN" "$@"
645
+ fi
646
+ local rc=$?
647
+ [ $rc -eq 124 ] && return 0
648
+ return $rc
649
+ }
650
+
651
+ INPUT=$(cat)
652
+ TOOL=$(jq -r '.tool_name // ""' <<<"$INPUT" 2>/dev/null || echo "")
653
+ case "$TOOL" in
654
+ shell|run_shell_command)
655
+ CMD=$(jq -r '.tool_input.command // .tool_input.cmd // ""' <<<"$INPUT" 2>/dev/null)
656
+ [ -z "$CMD" ] && exit 0
657
+ printf '%s' "$CMD" | run_scanner scan bash-input
658
+ ;;
659
+ read_file|read)
660
+ FP=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT" 2>/dev/null)
661
+ [ -z "$FP" ] && exit 0
662
+ run_scanner scan read-input --file "$FP" </dev/null
663
+ ;;
664
+ write_file|edit|replace)
665
+ FP=$(jq -r '.tool_input.file_path // .tool_input.path // ""' <<<"$INPUT" 2>/dev/null)
666
+ CONTENT=$(jq -r '.tool_input.content // .tool_input.new_string // ""' <<<"$INPUT" 2>/dev/null)
667
+ [ -z "$CONTENT" ] && exit 0
668
+ printf '%s' "$CONTENT" | run_scanner scan edit-input --file "$FP"
669
+ ;;
670
+ *) exit 0 ;;
671
+ esac
672
+ `;
673
+ }
674
+
675
+ // ── Codex CLI ────────────────────────────────────────────────────────
676
+ // Codex only emits Bash tool events today (spec: https://developers.openai.com/codex/hooks).
677
+ // We wire three hooks:
678
+ // - SessionStart (startup|resume) → ensures scanner binary is present & healthy
679
+ // - PreToolUse (Bash) → scan command before execution, return block JSON on detection
680
+ // - PostToolUse (Bash) → scan output, return feedback JSON on detection
681
+ // hooks.json shape lives in merge.mjs (buildCodexHooksJson).
682
+
683
+ export const CODEX_SESSION_INIT_SH = `#!/bin/bash
684
+ # Codex SessionStart: guarantee binary is ready before the first tool runs.
685
+ # flock prevents two concurrent sessions from racing on the download.
686
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
687
+ source "$SCRIPT_DIR/env.sh"
688
+ cat > /dev/null # drain stdin so Codex doesn't block on a write
689
+ BIN="$AI_SCANNER_BIN"
690
+ LOCK_FILE="/tmp/.ai-scanner-codex-init.lock"
691
+ if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then
692
+ exit 0
693
+ fi
694
+ (
695
+ flock -w 120 200 || exit 0
696
+ if [ -x "$BIN" ] && timeout 2 "$BIN" health >/dev/null 2>&1; then exit 0; fi
697
+ bash "$SCRIPT_DIR/start_scanner.sh" 2>/dev/null || true
698
+ ) 200>"$LOCK_FILE"
699
+ exit 0
700
+ `;
701
+
702
+ export const CODEX_PRE_SHELL_SH = `#!/bin/bash
703
+ # Codex PreToolUse Bash hook: scan command and return block JSON to Codex.
704
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
705
+ source "$SCRIPT_DIR/env.sh"
706
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
707
+ export AI_SCANNER_DETECTION_LOG
708
+ INPUT=$(cat)
709
+ CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
710
+ [ -z "$CMD" ] || [ "$CMD" = "null" ] && exit 0
711
+ SCAN_STDOUT="$(mktemp)"
712
+ SCAN_STDERR="$(mktemp)"
713
+ cleanup() {
714
+ rm -f "$SCAN_STDOUT" "$SCAN_STDERR"
715
+ }
716
+ trap cleanup EXIT
717
+
718
+ printf '%s' "$CMD" | run_scanner scan bash-input >"$SCAN_STDOUT" 2>"$SCAN_STDERR"
719
+ RC=$?
720
+ [ "$RC" -eq 0 ] && exit 0
721
+ [ "$RC" -ne 2 ] && exit 0
722
+
723
+ REASON="$(jq -r '.message // empty' <"$SCAN_STDOUT" 2>/dev/null)"
724
+ [ -n "$REASON" ] || REASON="$(cat "$SCAN_STDERR" 2>/dev/null)"
725
+ [ -n "$REASON" ] || REASON="AIDR blocked the Bash command."
726
+
727
+ jq -cn \
728
+ --arg reason "$REASON" \
729
+ '{hookSpecificOutput:{hookEventName:"PreToolUse",permissionDecision:"deny",permissionDecisionReason:$reason}}'
730
+ `;
731
+
732
+ export const CODEX_POST_SHELL_SH = `#!/bin/bash
733
+ # Codex PostToolUse Bash hook: scan command output and return feedback JSON.
734
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]:-$0}")" && pwd)"
735
+ source "$SCRIPT_DIR/env.sh"
736
+ BIN="$AI_SCANNER_BIN"; [ ! -x "$BIN" ] && exit 0
737
+ export AI_SCANNER_DETECTION_LOG
738
+ INPUT=$(cat)
739
+ CMD=$(jq -r '.tool_input.command // ""' <<<"$INPUT" 2>/dev/null)
740
+ OUTPUT=$(jq -r '.tool_response.output // .tool_output // ""' <<<"$INPUT" 2>/dev/null)
741
+ [ -z "$OUTPUT" ] || [ "$OUTPUT" = "null" ] && exit 0
742
+ SCAN_STDOUT="$(mktemp)"
743
+ SCAN_STDERR="$(mktemp)"
744
+ cleanup() {
745
+ rm -f "$SCAN_STDOUT" "$SCAN_STDERR"
746
+ }
747
+ trap cleanup EXIT
748
+
749
+ printf '%s' "$OUTPUT" | run_scanner scan bash-output --command "$CMD" >"$SCAN_STDOUT" 2>"$SCAN_STDERR"
750
+ RC=$?
751
+ [ "$RC" -eq 0 ] && exit 0
752
+ [ "$RC" -ne 2 ] && exit 0
753
+
754
+ REASON="$(jq -r '.message // empty' <"$SCAN_STDOUT" 2>/dev/null)"
755
+ [ -n "$REASON" ] || REASON="$(cat "$SCAN_STDERR" 2>/dev/null)"
756
+ [ -n "$REASON" ] || REASON="AIDR found risky Bash output."
757
+
758
+ jq -cn --arg reason "$REASON" '{decision:"block",reason:$reason}'
759
+ `;