The cost
Agent harnesses fire PreToolUse on every tool call, and the hook command is the full gortex binary. Measured over one real working session: 223 PreToolUse + 14 PostToolUse fires ≈ 74 seconds of serialized wait — PreToolUse hooks block the tool call they precede. This is the mechanical signature of "the agent feels laggy" with Gortex hooks installed.
Where the time goes (Windows 11, warm cache; every number below includes ~31 ms of shell-spawn measurement overhead — compare rows, not absolutes):
| Invocation |
Binary size |
Wall time |
gortex hook fed a payload that fails JSON parse on line 1 (no daemon dial, no work) |
~380 MB |
278–284 ms |
Scratch binary importing only internal/hooks, same payload |
~291 MB |
76–103 ms |
| Hello-world Go binary (spawn floor for this method) |
2.4 MB |
~31 ms |
So ~250 ms of the per-call cost is pure process startup of the everything-binary — before the hook logic runs at all. The worst case observed in that session was 3.0 s (a cold start eating the entire hook timeout budget).
Why the hook can't currently build small
internal/hooks has modest direct imports, but its daemon transport is internal/daemon — a package that is client and server in one: 72 internal deps including all 31 tree-sitter grammars. internal/mcp (used for wire types) is heavier still (126 deps). Anything that dials the daemon today links the whole engine.
The wire protocol itself needs none of that: internal/daemon/client.go and proto.go are already pure stdlib (unix socket, newline-delimited JSON handshake + control frames). They're heavy only by package association.
The second problem: untracked-cwd sessions pay full price for wrong advice
In a session whose cwd is not under any tracked root, the hook still costs the full ~280 ms per call — and the enrichment it emits mandates MCP tools that answer repository not tracked in that session. Deny plus an unusable alternative is the worst combination; silence would be strictly better and nearly free.
Proposal (sketched, in adoptable phases)
- Extract a leaf transport package — move
client.go / proto.go / socket-path discovery out of internal/daemon into e.g. internal/daemon/client with zero engine imports (mechanical; the files already qualify). Server keeps re-exporting for compatibility.
- A
hook_dispatch control RPC — takes the raw hook event JSON + agent-protocol name, returns the response JSON; internal/hooks' dispatch logic moves behind the daemon boundary, where the graph, config, and telemetry already live. gortex hook keeps working by calling the same RPC.
cmd/gortex-hook — a thin main: read stdin → tracked-roots fast path → hook_dispatch → print. Untracked cwd exits silently after one local config read, no dial.
Proof-of-concept on my fork: poc/thin-hook-client — a single-file stdlib-only client (hand-inlined copy of the transport essentials, which Phase 1 would extract instead). It builds at ~4 MB; the untracked fast path completes in ~47 ms wall (~16 ms process time), and dial + handshake + a status round trip lands ~125 ms wall against a warm daemon. Two findings from measuring the tracked path that inform Phase 2: the handshake currently does full per-session bookkeeping (first dial from a fresh client cost ~1 s), and status is a heavyweight stand-in — a purpose-built hook_dispatch behind a lightweight handshake mode (no session state for one-shot clients) should land well under 50 ms end to end.
Alternatives considered
- Long-lived hook helper (one resident process relaying events): even lower per-call cost, but adds lifecycle management the thin client avoids; could be a later optimization behind the same RPC.
- Trimming the main binary's init so
gortex hook itself starts fast: the grammars are CGO-linked and the import graph is the product — this looked like a much larger fight for a worse ceiling.
Happy to implement whichever shape you prefer — the phases are independent, and Phase 1 alone (the leaf transport package) already unblocks any future slim consumer.
The cost
Agent harnesses fire
PreToolUseon every tool call, and the hook command is the fullgortexbinary. Measured over one real working session: 223 PreToolUse + 14 PostToolUse fires ≈ 74 seconds of serialized wait — PreToolUse hooks block the tool call they precede. This is the mechanical signature of "the agent feels laggy" with Gortex hooks installed.Where the time goes (Windows 11, warm cache; every number below includes ~31 ms of shell-spawn measurement overhead — compare rows, not absolutes):
gortex hookfed a payload that fails JSON parse on line 1 (no daemon dial, no work)internal/hooks, same payloadSo ~250 ms of the per-call cost is pure process startup of the everything-binary — before the hook logic runs at all. The worst case observed in that session was 3.0 s (a cold start eating the entire hook timeout budget).
Why the hook can't currently build small
internal/hookshas modest direct imports, but its daemon transport isinternal/daemon— a package that is client and server in one: 72 internal deps including all 31 tree-sitter grammars.internal/mcp(used for wire types) is heavier still (126 deps). Anything that dials the daemon today links the whole engine.The wire protocol itself needs none of that:
internal/daemon/client.goandproto.goare already pure stdlib (unix socket, newline-delimited JSON handshake + control frames). They're heavy only by package association.The second problem: untracked-cwd sessions pay full price for wrong advice
In a session whose cwd is not under any tracked root, the hook still costs the full ~280 ms per call — and the enrichment it emits mandates MCP tools that answer
repository not trackedin that session. Deny plus an unusable alternative is the worst combination; silence would be strictly better and nearly free.Proposal (sketched, in adoptable phases)
client.go/proto.go/ socket-path discovery out ofinternal/daemoninto e.g.internal/daemon/clientwith zero engine imports (mechanical; the files already qualify). Server keeps re-exporting for compatibility.hook_dispatchcontrol RPC — takes the raw hook event JSON + agent-protocol name, returns the response JSON;internal/hooks' dispatch logic moves behind the daemon boundary, where the graph, config, and telemetry already live.gortex hookkeeps working by calling the same RPC.cmd/gortex-hook— a thin main: read stdin → tracked-roots fast path →hook_dispatch→ print. Untracked cwd exits silently after one local config read, no dial.Proof-of-concept on my fork:
poc/thin-hook-client— a single-file stdlib-only client (hand-inlined copy of the transport essentials, which Phase 1 would extract instead). It builds at ~4 MB; the untracked fast path completes in ~47 ms wall (~16 ms process time), and dial + handshake + astatusround trip lands ~125 ms wall against a warm daemon. Two findings from measuring the tracked path that inform Phase 2: the handshake currently does full per-session bookkeeping (first dial from a fresh client cost ~1 s), andstatusis a heavyweight stand-in — a purpose-builthook_dispatchbehind a lightweight handshake mode (no session state for one-shot clients) should land well under 50 ms end to end.Alternatives considered
gortex hookitself starts fast: the grammars are CGO-linked and the import graph is the product — this looked like a much larger fight for a worse ceiling.Happy to implement whichever shape you prefer — the phases are independent, and Phase 1 alone (the leaf transport package) already unblocks any future slim consumer.