Files
grok-glance/bin/glance-approve.mjs
iceBear67andClaude Opus 5 d20eb9255c Drop the hook-template scheme and other dead weight
hooks/hooks.json was generated from a template by scripts/gen-hooks.mjs. Once every
hook became a command hook that reads hook.secret from $GLANCE_HOME itself, the
template had exactly two placeholders left: {{HOOK_TOKEN}}, which nothing had ever
substituted into anything, and {{APPROVAL_TIMEOUT_SECS}}. Generating a whole file to
compute one number is not a good trade, so the number is now fixed at 125s in the
committed hooks.json and the coupling is enforced in code instead: the daemon clamps
approval.timeoutMs to APPROVAL_MAX_WAIT_MS (90s), which keeps the script inside its
own hook timeout no matter what a hand-edited config.json says. Losing that clamp is
what would actually hurt — a killed script never runs its fail-open path.

Also removed:

- `glance sync-hooks`, `npm run build:hooks`, and hookSecret({create}). The daemon is
  the only thing that should ever mint the secret.
- The ?k= query-string carrier for the hook secret. It existed for hooks that cannot
  set headers; there are none, and a secret in a URL lands in logs and shell history.
- Snapshot.now and SessionView.startedAt, which were written on every snapshot and
  every persist and read by nobody.
- An unused crypto import.

Docs and the e2e suite follow. The suite's ~12 sync-hooks assertions become static
checks on the committed file, plus new ones that hooks.json, APPROVAL_HOOK_TIMEOUT_SECS
and APPROVAL_MAX_WAIT_MS still agree, and that ?k= is refused. 220 checks, all passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-09 07:04:50 +00:00

58 lines
1.6 KiB
JavaScript
Executable File

#!/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();
}