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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
6d34a17d9d
commit
d20eb9255c
@@ -51,10 +51,10 @@ npm install && npm run build
|
||||
```
|
||||
|
||||
The build produces `dist/server` (the daemon) and `dist/web` (the dashboard). Both are required;
|
||||
the daemon serves the dashboard itself. It also generates `hooks/hooks.json` from
|
||||
`hooks/hooks.template.json`, and creates `~/.grok/glance/hook.secret` (mode 0600) if it does not
|
||||
exist yet — the shared secret the hook scripts authenticate with. Neither the secret nor anything
|
||||
derived from it ends up in `hooks.json`.
|
||||
the daemon serves the dashboard itself. `hooks/hooks.json` is checked in as-is — nothing about it
|
||||
is generated or machine-specific. The shared secret the hook scripts authenticate with lives in
|
||||
`~/.grok/glance/hook.secret` (mode 0600) and is created by the daemon on first start; it never
|
||||
appears in `hooks.json`.
|
||||
|
||||
Then register the directory with Grok Build. Plugins are installed from a marketplace catalog, so
|
||||
for a local checkout the shortest path is a one-entry catalog. Create
|
||||
@@ -184,7 +184,7 @@ Defaults worth knowing:
|
||||
|---|---|---|---|
|
||||
| Only wait when a phone is watching | on | yes | Otherwise a closed browser tab stalls the agent for 90s per tool call. |
|
||||
| On timeout | allow | yes | Flip to *deny* if you would rather fail closed. |
|
||||
| Timeout | 90s | no — edit `config.json` | The hook's own timeout is derived from this (`+35s` of slack) when `hooks.json` is generated, so re-run `sync-hooks` after changing it. |
|
||||
| Timeout | 90s | no — edit `config.json` | Also the ceiling: the approval hook gets 125s in `hooks.json`, and the daemon clamps a larger `timeoutMs` down to 90s so the script always outlives its own wait. |
|
||||
| Risky-tool pattern | `^(Bash\|Write\|Edit\|MultiEdit\|NotebookEdit)$` | no — edit `config.json` | Shown on the phone but not editable: a typo'd regex would silently change what gets gated. |
|
||||
|
||||
**This is a convenience gate, not a security boundary.** Every failure path is fail-open: daemon
|
||||
@@ -208,7 +208,6 @@ permission settings.
|
||||
| `devices` | List enrolled devices |
|
||||
| `revoke <id-prefix>` | Revoke a device |
|
||||
| `approval <off\|risky\|all>` | Set the approval policy |
|
||||
| `sync-hooks` | Regenerate `hooks/hooks.json` from the template (after changing `config.json`) |
|
||||
|
||||
## Files and configuration
|
||||
|
||||
@@ -235,10 +234,8 @@ instance without touching your real one:
|
||||
| `GLANCE_PORT` | Port to listen on (and, for the CLI and hooks, to talk to) |
|
||||
| `GLANCE_ORIGIN` | Public origin, as if set with `set-origin` — but not persisted |
|
||||
|
||||
The port is not baked into `hooks/hooks.json` — the hook scripts read `config.json` themselves — so
|
||||
changing it needs nothing but a daemon restart. Changing `approval.timeoutMs` does affect the
|
||||
generated file: run `node bin/glance sync-hooks` afterwards so the approval hook's own timeout still
|
||||
outlasts the wait.
|
||||
Nothing in `hooks/hooks.json` is machine-specific: the hook scripts read `config.json` themselves,
|
||||
so changing the port or `approval.timeoutMs` needs nothing but a daemon restart.
|
||||
|
||||
## Security notes
|
||||
|
||||
@@ -308,9 +305,8 @@ stop working and must be enrolled again.
|
||||
|
||||
## Hook wiring
|
||||
|
||||
`hooks/hooks.json` subscribes to all 14 lifecycle events, and **is generated** — from
|
||||
`hooks/hooks.template.json` by `scripts/gen-hooks.mjs`, which runs as part of `npm run build` and on
|
||||
`node bin/glance sync-hooks`. Edit the template, not the output.
|
||||
`hooks/hooks.json` subscribes to all 14 lifecycle events and is a plain checked-in file — edit it
|
||||
directly.
|
||||
|
||||
Every entry is a `command` hook. That is not a style choice: an `http` hook cannot reach this daemon
|
||||
by any route. Grok Build's http runner rejects every scheme but `https`, then **resolves the host**
|
||||
@@ -319,9 +315,8 @@ and refuses the resolved address if it is private, link-local or CGNAT
|
||||
fails the scheme check; the tailnet fails the address check, because `*.ts.net` resolves into
|
||||
`100.64/10` (and `fd7a::/48`, inside the blocked `fc00::/7`). On top of that the runner sends no
|
||||
request header but `Content-Type`, with no configuration surface for one, so such a hook could not
|
||||
authenticate itself even if it could connect. The generator refuses to emit an `http` handler whose
|
||||
URL is not `https://`, because the alternative is what this plugin shipped for a while: 13 passive
|
||||
hooks that failed validation silently on every event.
|
||||
authenticate itself even if it could connect. This plugin shipped `http` hooks for a while, and the
|
||||
result was 13 passive hooks failing validation silently on every event.
|
||||
|
||||
A command hook has none of those problems. It is a local process, so no URL is validated, nothing
|
||||
traverses the tunnel, and it can present the shared secret — hook traffic goes straight to
|
||||
@@ -333,8 +328,8 @@ traverses the tunnel, and it can present the shared secret — hook traffic goes
|
||||
only `^(Bash|Write|Edit|MultiEdit|NotebookEdit)$` that runs `bin/glance-approve.mjs`. PreToolUse
|
||||
is the only blocking event, and a command hook is the only documented way to return a deny
|
||||
decision; keeping the match narrow means the gate's cost is paid only for calls that could
|
||||
actually need a tap. Its `timeout` is derived from `approval.timeoutMs` at generation time rather
|
||||
than hand-copied, which is the other thing `sync-hooks` refreshes.
|
||||
actually need a tap. Its `timeout` is a fixed 125s, and the daemon clamps its own wait to 90s
|
||||
against it (`APPROVAL_MAX_WAIT_MS`) so the script is never killed before it can fail open.
|
||||
|
||||
The hook scripts use nothing but the Node standard library and always exit 0 unless they are
|
||||
deliberately denying — including when the daemon rejects their token.
|
||||
|
||||
Reference in New Issue
Block a user