You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Same exercise as #114, run on darwin. rin, M-series, macOS 26.1, WezTerm, herdr.
The headline is that the terminal stack needed nothing. #114 fixed it in home/base, so darwin inherited every one of those wins and now measures faster than the numbers that issue closed on. What was left is the agent hook layer, which #114 never looked at because it is not startup at all: it is a cost charged per tool call, all day, on every agent in the fleet.
The shell number needs env -i to be honest. /etc/profile and hm-session-vars.sh short-circuit on __ETC_PROFILE_SOURCED and __HM_SESS_VARS_SOURCED, so measuring from an inherited environment reads 13ms and hides the login tax that #114 item 2 is about.
What was fixed
PreToolUse is registered with a .* matcher, so every Read, Grep, Glob and Bash an agent issues starts a fresh interpreter. That interpreter was bootstrapping sys.path through pathlib, which pulls in urllib.parse, ipaddress and fnmatch for work os.path does with modules the interpreter has already loaded, and then importing all twelve handlers when the tool's matcher could select at most two.
The handler table now names its module rather than binding an imported function, and run_handlers imports one only once the surface and tool matchers select it. The bootstraps moved to os.path, including in the two matcher-less guards that genuinely do load on every call. Measured against a bare interpreter on the same machine, dispatch overhead fell from 1.11 interpreter floors to 0.45. Through the real invocation path a Read went from 47ms to 36ms, and 58 modules now load on that path.
PostToolUse was already narrowed to Skill|Edit|Write in event-to-dispatcher-map.nix, so a Read never spawns it at all. That is the largest structural saving available in this layer and it was already taken.
Two guards hold the result, both in agents/hooks/__tests__/unit/. test_dispatcher_import_budget.py reads the module set each invocation actually imports and fails on an expensive stdlib module, an unmatched handler, a third-party dependency, or a per-invocation count over budget. test_dispatcher_startup_latency.py measures wall time against the interpreter floor measured on the same machine, so its bound travels to CI hardware of any speed.
Measured and rejected
-S skips site and saves 3.7ms on a bare interpreter, which looked like the next win. Interleaved A/B on the real dispatcher put it at -0.04ms: once json and re are loaded the saving is already spent. It was measured back out rather than kept as unpaid risk, since skipping site would also have tied the hooks to staying free of site-packages forever. run-hook.sh kept only exec, which stops a bash process idling for the hook's lifetime.
Dropping re from hook_dispatch in favour of literal alternation matching was rejected for the same reason: json imports re regardless, so the matcher rewrite would have bought nothing.
Remaining work
The interpreter floor is now the hook budget. A Read spends 36ms end to end: roughly 10ms in the shell Claude Code spawns the command through, 5ms in the run-hook.sh wrapper, 15ms of interpreter boot, and 6ms of our own imports and work. Going below that means not starting a Python process at all: a resident daemon holding the handlers preloaded, with a small client on the hook command. That trades the cost for a supervision surface and a staleness problem, and these are security guards, so the fail-open versus fail-closed call has to be made deliberately rather than inherited.
claude mcp list at 2.5s suggests the two MCP servers are health-checked serially. Worth confirming whether session start pays the same shape, since perf: track startup latency of core terminal and AI apps #114 item 1 measured the servers themselves at ~0.3s each.
There is still no darwin desktop benchmark. benchmark.nix gates benchmark_desktop.py behind pkgs.stdenv.hostPlatform.isLinux because it drives Hyprland, so window and app latency on darwin has no reproducible harness. perf: track startup latency of core terminal and AI apps #114 asked for a reusable benchmark script as a follow-up, and this is the platform where the gap is total.
How to re-measure
Wall-clock medians of env -i bash -l -i -c exit for the shell, --startuptime for nvim, spawn-to-version for the CLIs, a JSON-RPC initialize round trip for the MCP servers, and for the hooks a payload piped into run-hook.sh exactly as settings.json invokes it. The two dispatcher tests above are the parts that now run without being asked.
Same exercise as #114, run on darwin.
rin, M-series, macOS 26.1, WezTerm, herdr.The headline is that the terminal stack needed nothing. #114 fixed it in
home/base, so darwin inherited every one of those wins and now measures faster than the numbers that issue closed on. What was left is the agent hook layer, which #114 never looked at because it is not startup at all: it is a cost charged per tool call, all day, on every agent in the fleet.Measured (median, fresh spawn)
herdr list)claude mcp listThe shell number needs
env -ito be honest./etc/profileandhm-session-vars.shshort-circuit on__ETC_PROFILE_SOURCEDand__HM_SESS_VARS_SOURCED, so measuring from an inherited environment reads 13ms and hides the login tax that #114 item 2 is about.What was fixed
PreToolUseis registered with a.*matcher, so every Read, Grep, Glob and Bash an agent issues starts a fresh interpreter. That interpreter was bootstrappingsys.paththrough pathlib, which pulls inurllib.parse,ipaddressandfnmatchfor workos.pathdoes with modules the interpreter has already loaded, and then importing all twelve handlers when the tool's matcher could select at most two.The handler table now names its module rather than binding an imported function, and
run_handlersimports one only once the surface and tool matchers select it. The bootstraps moved toos.path, including in the two matcher-less guards that genuinely do load on every call. Measured against a bare interpreter on the same machine, dispatch overhead fell from 1.11 interpreter floors to 0.45. Through the real invocation path a Read went from 47ms to 36ms, and 58 modules now load on that path.PostToolUsewas already narrowed toSkill|Edit|Writeinevent-to-dispatcher-map.nix, so a Read never spawns it at all. That is the largest structural saving available in this layer and it was already taken.Two guards hold the result, both in
agents/hooks/__tests__/unit/.test_dispatcher_import_budget.pyreads the module set each invocation actually imports and fails on an expensive stdlib module, an unmatched handler, a third-party dependency, or a per-invocation count over budget.test_dispatcher_startup_latency.pymeasures wall time against the interpreter floor measured on the same machine, so its bound travels to CI hardware of any speed.Measured and rejected
-Sskipssiteand saves 3.7ms on a bare interpreter, which looked like the next win. Interleaved A/B on the real dispatcher put it at -0.04ms: oncejsonandreare loaded the saving is already spent. It was measured back out rather than kept as unpaid risk, since skippingsitewould also have tied the hooks to staying free of site-packages forever.run-hook.shkept onlyexec, which stops a bash process idling for the hook's lifetime.Dropping
refromhook_dispatchin favour of literal alternation matching was rejected for the same reason:jsonimportsreregardless, so the matcher rewrite would have bought nothing.Remaining work
run-hook.shwrapper, 15ms of interpreter boot, and 6ms of our own imports and work. Going below that means not starting a Python process at all: a resident daemon holding the handlers preloaded, with a small client on the hook command. That trades the cost for a supervision surface and a staleness problem, and these are security guards, so the fail-open versus fail-closed call has to be made deliberately rather than inherited.claude mcp listat 2.5s suggests the two MCP servers are health-checked serially. Worth confirming whether session start pays the same shape, since perf: track startup latency of core terminal and AI apps #114 item 1 measured the servers themselves at ~0.3s each./etc/profileand hm-session-vars has not been decomposed.benchmark.nixgatesbenchmark_desktop.pybehindpkgs.stdenv.hostPlatform.isLinuxbecause it drives Hyprland, so window and app latency on darwin has no reproducible harness. perf: track startup latency of core terminal and AI apps #114 asked for a reusable benchmark script as a follow-up, and this is the platform where the gap is total.How to re-measure
Wall-clock medians of
env -i bash -l -i -c exitfor the shell,--startuptimefor nvim, spawn-to-version for the CLIs, a JSON-RPCinitializeround trip for the MCP servers, and for the hooks a payload piped intorun-hook.shexactly assettings.jsoninvokes it. The two dispatcher tests above are the parts that now run without being asked.