/hook/record and /hook/approve accepted anything that reached the port. That is not "loopback only": `tailscale serve` proxies tailnet traffic to 127.0.0.1, so anyone who could reach the tunnel could forge timeline events and answer approval prompts. Both endpoints now require a 32-byte secret from $GLANCE_HOME/hook.secret (0600, created once, never rotated so nothing in flight is 403'd mid-session), compared in constant time before the body is read, as an x-glance-hook header or a ?k= parameter. Requests carrying x-forwarded-* are refused outright: a local hook process never sends them and a tunnelled caller always does. The check applies to /hook/* only, so the dashboard is unaffected. While wiring that up: the 13 passive `type: "http"` hooks could never have worked. Grok Build's http runner rejects every scheme but https, then resolves the host and blocks private/link-local/CGNAT addresses (validate_hook_url + is_blocked_ip), so neither loopback-over-http nor *.ts.net (100.64/10) can be a hook target - and it sends no header but Content-Type, so such a hook could not authenticate anyway. They were failing validation silently on every event. All of them are now command hooks running bin/glance-record.mjs, which costs a Node start and can present the secret. hooks.json is generated from hooks/hooks.template.json by scripts/gen-hooks.mjs (npm run build, glance sync-hooks). It creates the secret, derives the approval hook's timeout from approval.timeoutMs instead of hand-copying 125, and refuses to write a hook that cannot fire: bad type, non-positive timeout, non-https http URL, missing bin/ script, or a leftover placeholder. A template that embeds the token makes the output 0600 with a warning. Fail-open is unchanged: a missing, stale or rejected secret degrades to "no telemetry", and glance-approve.mjs still allows on every error path. glance status warns when the on-disk secret no longer matches the daemon's. Validated with the e2e suite (190 checks, including no-token/wrong-token/ same-length-token 403s, ?k= acceptance, x-forwarded-* refusal, and the recorder's fail-open paths) and a clean npm run build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
3.9 KiB
name, description
| name | description |
|---|---|
| glance | Set up, inspect, or control grok-glance — the passkey-guarded phone dashboard for this Grok Build session. Use when the user asks to watch a session from their phone, enrol a device, expose the dashboard over Tailscale, or turn remote approve/deny on or off. |
grok-glance
A local daemon plus web dashboard that shows what Grok Build is doing, readable from a phone behind a WebAuthn passkey. It can also pause risky tool calls until someone taps approve.
The daemon is started automatically by the SessionStart hook. Everything below is done through
the glance CLI at $GROK_PLUGIN_ROOT/bin/glance.
First check whether it is even built
The plugin ships as TypeScript and must be built once:
cd "$GROK_PLUGIN_ROOT" && npm install && npm run build
glance status prints a "not built" error with this same instruction if it is missing. Do not
attempt to skip the build — the daemon entry point is dist/server/index.js.
The commands
glance status # is it running, which origin, how many devices
glance up # start the daemon in the background
glance stop # stop it
glance logs # last 60 lines of the daemon log
glance enroll # mint a one-time code + URL for a new phone
glance set-origin <https-url> # set the public origin and WebAuthn RP ID
glance devices # list enrolled devices
glance revoke <id-prefix> # revoke one
glance approval <off|risky|all> # remote approve/deny policy
glance sync-hooks # regenerate hooks/hooks.json from hooks/hooks.template.json
Getting it onto a phone
The dashboard listens on 127.0.0.1 only. Passkeys need a real hostname with valid TLS — a bare
IP can never be a WebAuthn RP ID — so the supported path is Tailscale Serve:
tailscale serve --bg 127.0.0.1:8791
tailscale serve status # read the https://<box>.<tailnet>.ts.net URL
glance set-origin https://<box>.<tailnet>.ts.net
glance enroll
Then open the printed URL on the phone, type the code, and create the passkey. The phone must be on the same tailnet.
Changing the origin changes the RP ID, which invalidates existing passkeys. Say so before running
set-origin on a working setup.
Remote approve/deny
glance approval risky makes Bash, Write, Edit, MultiEdit and NotebookEdit calls pause
and wait for a tap on the phone. Defaults that matter:
- Nothing waits unless a phone is actually watching the dashboard (
requireWatcher). - If nobody answers within 90s the call is allowed, not denied. Flip that on the phone's settings panel if you want the opposite.
- Every failure path is fail-open: daemon down, timeout, bad JSON, a rejected hook secret — the tool call proceeds. This is a convenience gate, not a security boundary.
glance approval off (the default) means Grok Build never blocks on the phone.
When something does not work
- "not running" →
glance up, thenglance logs. - Passkey prompt fails with a security error → the phone is on a hostname the RP ID does not
cover. Compare
glance status'srp idwith the hostname in the phone's address bar. - Dashboard loads but shows nothing → hooks are not firing.
glance statuswarns if the hook secret in$GLANCE_HOME/hook.secretno longer matches the one the daemon loaded (events are being dropped with a 403);glance stop && glance upfixes that. Otherwise check thathooks/hooks.jsonexists and is registered — it is generated, soglance sync-hooksrebuilds it from the template. - Page says "run npm install && npm run build" → the web bundle is missing; build it.
What it deliberately does not do
Read-only plus approve/deny. It cannot send prompts, edit files, run tools, or resume a session. Do not tell the user otherwise.