#!/usr/bin/env node /** * PreToolUse hook: the remote approval gate. * * Asks the daemon what to do with this tool call. The daemon holds the request open while * your phone decides, then answers allow/deny. Every failure path here is fail-open — * a daemon that is down, slow, or confused must not be able to block your agent. * * Denying is the only outcome that changes behaviour, and approving only lets through a * call Grok was already about to make. This hook can never introduce a new command. */ import { APPROVAL_HOOK_TIMEOUT_SECS, baseUrl, envEnvelope, hookHeaders, postJson, readConfig, readStdinJson, } from "./glance-lib.mjs"; function allow() { process.exit(0); } function deny(reason) { // Belt and braces: the documented deny signals are a JSON decision on stdout *and* // exit code 2. We emit both so a change in precedence cannot silently allow. process.stdout.write( JSON.stringify({ decision: "deny", reason: reason || "Denied from grok-glance" }), ); process.exit(2); } const payload = envEnvelope(await readStdinJson()); const cfg = readConfig(); // Finish inside the hook's own timeout from hooks.json, with room to spare: if Grok Build // kills us first, the fail-open path below never gets to run. const waitMs = Math.min( APPROVAL_HOOK_TIMEOUT_SECS * 1000 - 10_000, Number(cfg.approval?.timeoutMs ?? 90_000) + 15_000, ); try { const { data } = await postJson( `${baseUrl(cfg)}/hook/approve`, payload, waitMs, hookHeaders(), ); if (data && data.decision === "deny") deny(data.reason); allow(); } catch { allow(); }