#!/usr/bin/env node // copilot-adapter.mjs — Normalizes GitHub Copilot hook I/O to internal format. // // Copilot CLI hooks receive JSON on stdin with camelCase field names // (toolName, toolArgs, toolResult) and signal blocking via exit code 2 // or stdout JSON with permissionDecision: "deny". // // This adapter provides a consistent interface so hook logic stays // platform-agnostic. If Copilot changes field names, only this file // needs updating. import { readFileSync } from 'node:fs'; /** * Parse hook input from stdin. Returns normalized object or null on failure. * Supports both Copilot (camelCase) and Claude Code (snake_case) field names. */ export function parseInput() { try { const raw = readFileSync(0, 'utf-8'); const input = JSON.parse(raw); return { toolName: input.toolName ?? input.tool_name ?? '', toolInput: input.toolArgs ?? input.tool_input ?? {}, toolOutput: input.toolResult ?? input.tool_output ?? '', message: input.message ?? {}, sessionId: input.sessionId ?? input.session_id ?? '', raw: input, }; } catch { return null; } } /** * Block the tool call with a reason message. */ export function block(reason) { process.stderr.write(reason + '\n'); process.stdout.write(JSON.stringify({ permissionDecision: 'deny', reason, })); process.exit(2); } /** * Allow the tool call, optionally with an advisory message. */ export function allow(message) { if (message) { process.stdout.write(JSON.stringify({ permissionDecision: 'allow', message, })); } process.exit(0); } /** * Emit a warning to stderr without blocking. Exit 0. */ export function warn(message) { process.stderr.write(message + '\n'); process.exit(0); } /** * Fail-open: exit 0 silently. Used when input can't be parsed. */ export function failOpen() { process.exit(0); }