Files
iceBear67andClaude Opus 5 966133eeda Start the daemon from whichever hook fires first
A plugin's SessionStart hook never runs, so nothing was starting the
daemon: no daemon.log, nothing on :8791, and a manual `glance up`
working perfectly.

Grok Build dispatches SessionStart from inside session creation
(xai-grok-shell, agent_ops.rs -> DispatchSessionStartHook) and resolves
it against the session's hook registry as it stands at that moment.
That registry comes from discover_hooks(), whose sources are the config
layers and the global/project settings files; plugin directories are not
among them. Plugin hooks are appended later, under a plugin/ prefix, by
reload_hooks_impl and reload_plugins_impl - which run in response to a
plugin action, a /hooks reload, or a folder-trust grant. So the entry is
always registered after the event it subscribes to has been dispatched.
The other thirteen events work because they happen later in the session.

There is no boot event to move to, so every recorder boots the daemon
instead and whichever fires first wins. The cost is one loopback request
to /healthz per event once it is up, which is the steady state. A
daemon.lock (O_EXCL, 15s staleness takeover) keeps a burst of concurrent
events from starting five daemons and leaving four to die on EADDRINUSE.

glance-up.mjs stays wired: it costs nothing when it does not fire, and
it is the right hook for the job if that ordering is ever fixed.

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

155 lines
5.4 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.",
"",
"Every recorder boots the daemon if it is not already up, rather than one designated boot",
"hook doing it. Grok Build gives a plugin no usable boot event: SessionStart is dispatched",
"from inside session creation, against the hook registry as it stands at that moment, and",
"that registry holds only config-layer and settings-file hooks. Plugin hooks are appended",
"afterwards, under a 'plugin/' prefix, by reload_hooks_impl and reload_plugins_impl - so the",
"SessionStart entry below is registered strictly after SessionStart has already fired and",
"never runs. It is kept because it costs nothing and is the right hook once that is fixed.",
"",
"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 }
]
}
]
}
}