permissions hook helper
0
fork

Configure Feed

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

Make review command interactive with arrow keys, allow/deny/skip per entry

+139 -11
+139 -11
chook.mjs
··· 446 446 console.error("No audit_file configured."); 447 447 process.exit(1); 448 448 } 449 - // Show deny and passthrough entries (things that weren't auto-allowed) 450 449 const entries = readAuditEntries(auditPath, ["deny", "passthrough"]); 451 450 if (entries.length === 0) { 452 451 console.log("No deny/passthrough entries in audit log."); 453 452 process.exit(0); 454 453 } 455 - // Deduplicate by tool+detail to show unique patterns 454 + // Deduplicate by tool+detail 456 455 const seen = new Set(); 457 456 const unique = []; 458 457 for (const entry of entries) { 459 458 const ti = entry.tool_input; 460 - const key = entry.tool_name + ":" + (ti.command || ti.file_path || ti.notebook_path || ti.subagent_type || ""); 459 + const key = 460 + entry.tool_name + 461 + ":" + 462 + (ti.command || ti.file_path || ti.notebook_path || ti.subagent_type || ""); 461 463 if (!seen.has(key)) { 462 464 seen.add(key); 463 465 unique.push(entry); 464 466 } 465 467 } 466 - console.log(`Denied/passthrough entries (${unique.length} unique):\n`); 467 - console.log(" IDX DECISION TOOL DETAIL"); 468 - console.log(" --- -------- ---- ------"); 469 - unique.forEach((e, i) => console.log(formatEntry(e, i))); 470 - console.log(`\nTo add a rule, run:`); 471 - console.log(` chook allow-last --config ${configPath} # allow the most recent entry`); 472 - console.log(` chook deny-last --config ${configPath} # deny the most recent entry`); 473 - console.log(`\nOr edit ${configPath} directly for more control.`); 468 + 469 + // Interactive review 470 + let cursor = 0; 471 + let added = 0; 472 + 473 + function getDetail(entry) { 474 + const ti = entry.tool_input; 475 + switch (entry.tool_name) { 476 + case "Bash": 477 + return ti.command || ""; 478 + case "Read": 479 + case "Write": 480 + case "Edit": 481 + case "MultiEdit": 482 + case "Glob": 483 + case "NotebookEdit": 484 + return ti.file_path || ti.notebook_path || ""; 485 + case "Task": 486 + return ti.subagent_type || ""; 487 + default: 488 + return JSON.stringify(ti).slice(0, 120); 489 + } 490 + } 491 + 492 + function render() { 493 + // Clear screen and move to top 494 + process.stdout.write("\x1b[2J\x1b[H"); 495 + console.log( 496 + `chook review - ${unique.length} unique denied/passthrough entries\n`, 497 + ); 498 + console.log(" [a] allow [d] deny [s] skip [q] quit\n"); 499 + 500 + const pageSize = process.stdout.rows ? process.stdout.rows - 10 : 15; 501 + const halfPage = Math.floor(pageSize / 2); 502 + let start = Math.max(0, cursor - halfPage); 503 + let end = Math.min(unique.length, start + pageSize); 504 + if (end - start < pageSize) start = Math.max(0, end - pageSize); 505 + 506 + for (let i = start; i < end; i++) { 507 + const e = unique[i]; 508 + const marker = i === cursor ? ">" : " "; 509 + const decision = e.decision.toUpperCase().padEnd(11); 510 + const tool = e.tool_name.padEnd(12); 511 + let detail = getDetail(e); 512 + const maxDetail = (process.stdout.columns || 100) - 32; 513 + if (detail.length > maxDetail) 514 + detail = detail.slice(0, maxDetail - 3) + "..."; 515 + 516 + if (i === cursor) { 517 + // Highlight current line 518 + process.stdout.write(`\x1b[7m ${marker} ${decision} ${tool} ${detail}\x1b[0m\n`); 519 + } else { 520 + console.log(` ${marker} ${decision} ${tool} ${detail}`); 521 + } 522 + } 523 + 524 + // Show proposed rule for current entry 525 + console.log(""); 526 + const rule = buildRuleFromEntry(unique[cursor], "allow"); 527 + console.log(` Proposed rule:\n ${rule.split("\n").join("\n ")}`); 528 + } 529 + 530 + // Raw mode for keypress handling 531 + process.stdin.setRawMode(true); 532 + process.stdin.resume(); 533 + process.stdin.setEncoding("utf-8"); 534 + 535 + render(); 536 + 537 + process.stdin.on("data", (key) => { 538 + // Ctrl-C or q 539 + if (key === "\x03" || key === "q") { 540 + process.stdout.write("\x1b[2J\x1b[H"); 541 + if (added > 0) { 542 + console.log(`Done. Added ${added} rule(s) to ${configPath}`); 543 + } else { 544 + console.log("No rules added."); 545 + } 546 + process.exit(0); 547 + } 548 + 549 + // Arrow up or k 550 + if (key === "\x1b[A" || key === "k") { 551 + if (cursor > 0) cursor--; 552 + render(); 553 + return; 554 + } 555 + 556 + // Arrow down or j 557 + if (key === "\x1b[B" || key === "j") { 558 + if (cursor < unique.length - 1) cursor++; 559 + render(); 560 + return; 561 + } 562 + 563 + // Allow 564 + if (key === "a") { 565 + const ruleBlock = buildRuleFromEntry(unique[cursor], "allow"); 566 + appendRuleToConfig(configPath, ruleBlock); 567 + added++; 568 + unique.splice(cursor, 1); 569 + if (unique.length === 0) { 570 + process.stdout.write("\x1b[2J\x1b[H"); 571 + console.log(`Done. Added ${added} rule(s) to ${configPath}`); 572 + process.exit(0); 573 + } 574 + if (cursor >= unique.length) cursor = unique.length - 1; 575 + render(); 576 + return; 577 + } 578 + 579 + // Deny 580 + if (key === "d") { 581 + const ruleBlock = buildRuleFromEntry(unique[cursor], "deny"); 582 + appendRuleToConfig(configPath, ruleBlock); 583 + added++; 584 + unique.splice(cursor, 1); 585 + if (unique.length === 0) { 586 + process.stdout.write("\x1b[2J\x1b[H"); 587 + console.log(`Done. Added ${added} rule(s) to ${configPath}`); 588 + process.exit(0); 589 + } 590 + if (cursor >= unique.length) cursor = unique.length - 1; 591 + render(); 592 + return; 593 + } 594 + 595 + // Skip 596 + if (key === "s") { 597 + if (cursor < unique.length - 1) cursor++; 598 + render(); 599 + return; 600 + } 601 + }); 474 602 } else { 475 603 console.error("Usage: chook <command> --config <path>"); 476 604 console.error("");