fix: prefer local dotcontext binary for hook dispatch, pin npx fallback - #78
fix: prefer local dotcontext binary for hook dispatch, pin npx fallback#78LorranHippolyte wants to merge 2 commits into
Conversation
Hook configs previously persisted `npx -y @dotcontext/cli@latest hook dispatch`, which spawns npx and re-resolves the `latest` dist-tag on every SessionStart/PostToolUse/Stop event. On busy sessions this produces a constant stream of short-lived Node processes and registry lookups on every file edit and shell command. The installer now writes `dotcontext hook dispatch --source <host>` when a global dotcontext binary is found on PATH, and otherwise falls back to npx pinned to the installed CLI version. Both forms are treated as current so installs do not churn configs; the legacy `@latest` form is upgraded on re-install. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR reduces per-hook invocation overhead by changing the persisted hook dispatch command for Claude Code and Codex: it prefers invoking a local dotcontext binary when available and otherwise falls back to a version-pinned npx command (instead of @latest) to avoid repeated npm dist-tag resolution.
Changes:
- Add hook-dispatch command resolution helpers (PATH scan for
dotcontext, pinned-npx fallback, canonical command matching). - Update Claude Code and Codex hook template generation to resolve the dispatch command at install time, while keeping pinned constants for compatibility.
- Update tests and documentation to reflect the new persisted command behavior and legacy
@latestupgrade path.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/integrations/shared/index.ts | Re-exports new hook-dispatch helpers/types for integration callers. |
| src/integrations/shared/hookDispatchCommands.ts | Implements PATH-based local binary detection, pinned fallback, and “current” command matching. |
| src/integrations/shared/tests/hookSessionStore.test.ts | Updates assertions to validate pinned (non-@latest) dispatch commands. |
| src/integrations/shared/tests/hookDispatchCommands.test.ts | Adds coverage for PATH detection, resolution, canonical acceptance, and legacy rejection. |
| src/integrations/codex/hooks/index.ts | Exposes the new Codex hook-template builder. |
| src/integrations/codex/hooks/codexHookTemplates.ts | Switches from static templates to builder-based generation with install-time command resolution. |
| src/integrations/claude-code/hooks/index.ts | Exposes the new Claude Code hook-template builder. |
| src/integrations/claude-code/hooks/claudeCodeHookTemplates.ts | Switches from static templates to builder-based generation with install-time command resolution. |
| src/integrations/tests/hookInstallServices.test.ts | Updates install-service tests to assert against resolved dispatch commands and legacy @latest upgrades. |
| src/cli/services/tests/hookInstallService.test.ts | Updates CLI hook install tests to match resolved dispatch commands. |
| README.md | Updates dispatch table docs to describe local-binary preference + pinned-npx fallback. |
| docs/src/content/docs/pt-br/guides/using-with-hooks.md | Documents local-binary dispatch and pinned-npx fallback (pt-BR). |
| docs/src/content/docs/pt-br/guides/hook-session-flow.md | Documents dispatch flow and why pinning avoids per-event latest resolution (pt-BR). |
| docs/src/content/docs/en/guides/using-with-hooks.md | Documents local-binary dispatch and pinned-npx fallback (en). |
| docs/src/content/docs/en/about/architecture.md | Updates integrations boundary description to reflect new dispatch behavior. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (fs.statSync(path.join(directory, name)).isFile()) { | ||
| return true; | ||
| } |
There was a problem hiding this comment.
Fixed in d576133. isDotcontextBinaryOnPath() now requires execute permission (fs.accessSync with X_OK) on non-win32 platforms before treating a PATH entry as an installed binary, with test coverage for the non-executable-file case.
| if (typeof command !== 'string') { | ||
| return false; | ||
| } | ||
|
|
||
| return command === expected; | ||
| return getCanonicalHookDispatchCommands(source).includes(command); |
There was a problem hiding this comment.
Fixed in d576133. The pinned-npx form is always treated as current, and the local-binary form is only current while dotcontext is actually on PATH — so re-running the installer after a global uninstall repairs the config instead of skipping it. Added tests for both directions.
| export interface BuildCodexTomlHookBlocksOptions { | ||
| includeFeatures?: boolean; | ||
| command?: ResolveHookDispatchCommandOptions; | ||
| } |
- Require execute permission (X_OK) on POSIX before treating a PATH entry named dotcontext as an installed binary, so a stray non-executable file cannot cause the installer to persist a dispatch command that fails at runtime. - Treat the local-binary dispatch command as current only while the binary is actually on PATH; the pinned-npx form stays always current. Re-running the installer now repairs configs left behind by a global uninstall instead of skipping them as up to date. - Rename BuildCodexTomlHookBlocksOptions.command to resolveOptions so the property does not read as the command string itself. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Problem
The hook installer persists this command into host configs (
.claude/settings.json,.codex/config.toml):This command runs on every
SessionStart,PostToolUse(Write/Edit/Bash), andStopevent. Two costs are paid on each invocation:npxspawns a fresh Node process even when the package is cached.@latestdist-tag forces npx to re-resolve the version against the npm registry, adding startup latency and registry traffic to every tool call.On busy sessions this produces a constant stream of short-lived Node processes — we traced sustained CPU load / thermal issues on a dev machine back to exactly this pattern (hooks firing npx on every file edit and shell command).
Change
@dotcontext/clialready ships adotcontextbin, so the installer can do better:dotcontextexecutable is found onPATH, hook configs are written withdotcontext hook dispatch --source <host>— no npx at all.npx -y @dotcontext/cli@<VERSION> hook dispatch ...) instead of@latest, removing per-event registry resolution.isCurrentDotcontextHookDispatchCommand, so installs do not flip configs back and forth when the environment changes. The legacy@latestform is no longer considered current, so a re-install upgrades it.Implementation notes
hookDispatchCommands.tsgainsisDotcontextBinaryOnPath(filesystem-only PATH scan, injectable env/platform for tests, Windows launcher variants covered),resolveHookDispatchCli,buildHookDispatchCommand, andgetCanonicalHookDispatchCommands.buildClaudeCodeHookTemplates/buildCodexHookTemplates) that resolve the command at install time; the static*_HOOK_TEMPLATESconstants are kept (pinned form) for compatibility.using-with-hooksen/pt-br,hook-session-flowpt-br,architecture, README dispatch table). One-shot operator commands in docs (hook install,mcp:install, etc.) intentionally keep@latest.Tests
hookDispatchCommands.test.ts: binary detection (POSIX + win32, absent, directory-name collision), command resolution both ways, canonical-form acceptance, legacy@latestrejection.buildHookDispatchCommand(...)so they stay deterministic on machines with or without a globaldotcontextbinary; legacy-upgrade tests now treat the@latestform as the legacy input.npm run buildandnpm test -- --runInBandpass.🤖 Generated with Claude Code