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>
147 lines
4.7 KiB
JSON
147 lines
4.7 KiB
JSON
{
|
|
"_comment": [
|
|
"Every entry is a `command` hook, including the 13 passive recorders. That is not a style",
|
|
"choice: an `http` hook cannot reach this daemon by any route. Grok Build's http runner",
|
|
"(xai-grok-hooks/src/runner/http.rs, `validate_hook_url`) rejects every scheme but https,",
|
|
"then resolves the host and refuses the resolved address if it is private, link-local or",
|
|
"CGNAT - so plain http on loopback is out, and so is the tailnet, because *.ts.net resolves",
|
|
"into 100.64/10 (and fd7a::/48, inside the blocked fc00::/7). Pointing a hook at the public",
|
|
"https origin therefore fails upstream, before a request is ever sent. The runner also sends",
|
|
"no request header but Content-Type, so such a hook could not authenticate itself even if it",
|
|
"could connect.",
|
|
"",
|
|
"A command hook has none of those problems: it is a local process, so there is no URL to",
|
|
"validate, no proxy in the path, and it reads the shared secret out of $GLANCE_HOME itself.",
|
|
"It costs one Node start (~40ms) per event. Nothing here is secret, and nothing here is",
|
|
"derived from config.json - the scripts read that themselves - so this file is plain,",
|
|
"committed, and edited by hand.",
|
|
"",
|
|
"SessionStart boots the daemon. PreToolUse is wired twice on purpose: one entry records",
|
|
"every call for the timeline, and a second, narrowly matched entry runs the approval gate,",
|
|
"because PreToolUse is the only blocking event and only a command hook can return a deny.",
|
|
"",
|
|
"The gate's 125s timeout is the ceiling for the whole approval round trip. The daemon caps",
|
|
"approval.timeoutMs at 90s against it, so the script always outlives its own wait and gets",
|
|
"to fail open. Changing the number here means changing APPROVAL_HOOK_TIMEOUT_SECS in",
|
|
"bin/glance-lib.mjs and APPROVAL_MAX_WAIT_MS in server/src/config.ts to match."
|
|
],
|
|
"hooks": {
|
|
"SessionStart": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "node \"$GROK_PLUGIN_ROOT/bin/glance-up.mjs\"",
|
|
"timeout": 20
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"PreToolUse": [
|
|
{
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"",
|
|
"timeout": 5
|
|
}
|
|
]
|
|
},
|
|
{
|
|
"matcher": "^(Bash|Write|Edit|MultiEdit|NotebookEdit)$",
|
|
"hooks": [
|
|
{
|
|
"type": "command",
|
|
"command": "node \"$GROK_PLUGIN_ROOT/bin/glance-approve.mjs\"",
|
|
"timeout": 125
|
|
}
|
|
]
|
|
}
|
|
],
|
|
"PostToolUse": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"PostToolUseFailure": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"UserPromptSubmit": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"PermissionDenied": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"Notification": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"Stop": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"StopFailure": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"SubagentStart": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"SubagentStop": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"PreCompact": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"PostCompact": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
],
|
|
"SessionEnd": [
|
|
{
|
|
"hooks": [
|
|
{ "type": "command", "command": "node \"$GROK_PLUGIN_ROOT/bin/glance-record.mjs\"", "timeout": 5 }
|
|
]
|
|
}
|
|
]
|
|
}
|
|
}
|