permissions hook helper
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Delayed notification on passthrough, cancelled by PostToolUse if tool proceeds

+50 -2
+50 -2
chook.mjs
··· 1 1 #!/usr/bin/env node 2 2 3 - import { readFileSync, existsSync } from "fs"; 3 + import { readFileSync, existsSync, unlinkSync } from "fs"; 4 4 import { appendFileSync, openSync, closeSync, writeFileSync } from "fs"; 5 - import { execFileSync } from "child_process"; 5 + import { spawn } from "child_process"; 6 6 import { resolve } from "path"; 7 7 import { homedir } from "os"; 8 8 ··· 245 245 } 246 246 } 247 247 248 + // --- Pending notification --- 249 + 250 + const NOTIFY_DELAY_SECS = 3; 251 + 252 + function pendingFile(sessionId) { 253 + return `/tmp/chook-pending-${sessionId}`; 254 + } 255 + 256 + function scheduleNotification(sessionId, toolName, detail) { 257 + const short = detail.length > 80 ? detail.slice(0, 77) + "..." : detail; 258 + const msg = short.replace(/"/g, '\\"'); 259 + // Spawn a detached shell that sleeps then notifies 260 + const child = spawn( 261 + "sh", 262 + [ 263 + "-c", 264 + `sleep ${NOTIFY_DELAY_SECS} && osascript -e 'display notification "${msg}" with title "chook: ${toolName} needs approval" sound name "Ping"'`, 265 + ], 266 + { detached: true, stdio: "ignore" }, 267 + ); 268 + child.unref(); 269 + // Write PID so PostToolUse can cancel it 270 + try { 271 + writeFileSync(pendingFile(sessionId), String(child.pid)); 272 + } catch {} 273 + } 274 + 275 + function cancelNotification(sessionId) { 276 + const f = pendingFile(sessionId); 277 + try { 278 + const pid = parseInt(readFileSync(f, "utf-8").trim()); 279 + // Kill the process group (negative PID) to get the sleep + osascript 280 + try { process.kill(-pid); } catch {} 281 + try { process.kill(pid); } catch {} 282 + unlinkSync(f); 283 + } catch {} 284 + } 285 + 248 286 // --- Hook output --- 249 287 250 288 function makeOutput(decision, reason) { ··· 415 453 } 416 454 417 455 audit(config.audit, input, "passthrough", null); 456 + // Schedule a delayed notification — PostToolUse will cancel it if tool proceeds quickly 457 + const detail = toolInput.command || toolInput.file_path || toolInput.notebook_path || toolName; 458 + scheduleNotification(input.session_id, toolName, detail); 418 459 } catch (e) { 419 460 process.stderr.write(`chook error: ${e.message}\n`); 420 461 process.exit(1); 421 462 } 463 + } else if (command === "post") { 464 + // PostToolUse handler — cancel any pending notification 465 + try { 466 + const stdin = readFileSync(0, "utf-8"); 467 + const input = JSON.parse(stdin); 468 + cancelNotification(input.session_id); 469 + } catch {} 422 470 } else if (command === "allow-last" || command === "deny-last") { 423 471 const configPath = getConfigPath(); 424 472 const ruleType = command === "allow-last" ? "allow" : "deny";