Skip to content

fix: prefer local dotcontext binary for hook dispatch, pin npx fallback - #78

Open
LorranHippolyte wants to merge 2 commits into
vinilana:mainfrom
LorranHippolyte:fix/hook-dispatch-local-binary
Open

fix: prefer local dotcontext binary for hook dispatch, pin npx fallback#78
LorranHippolyte wants to merge 2 commits into
vinilana:mainfrom
LorranHippolyte:fix/hook-dispatch-local-binary

Conversation

@LorranHippolyte

Copy link
Copy Markdown
Contributor

Problem

The hook installer persists this command into host configs (.claude/settings.json, .codex/config.toml):

npx -y @dotcontext/cli@latest hook dispatch --source <host>

This command runs on every SessionStart, PostToolUse (Write/Edit/Bash), and Stop event. Two costs are paid on each invocation:

  1. npx spawns a fresh Node process even when the package is cached.
  2. The @latest dist-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/cli already ships a dotcontext bin, so the installer can do better:

  • Prefer the local binary: at install time, if a dotcontext executable is found on PATH, hook configs are written with dotcontext hook dispatch --source <host> — no npx at all.
  • Pin the npx fallback: when no global binary is available, the fallback is pinned to the version of the CLI performing the install (npx -y @dotcontext/cli@<VERSION> hook dispatch ...) instead of @latest, removing per-event registry resolution.
  • No config churn: both forms are treated as current by isCurrentDotcontextHookDispatchCommand, so installs do not flip configs back and forth when the environment changes. The legacy @latest form is no longer considered current, so a re-install upgrades it.

Implementation notes

  • hookDispatchCommands.ts gains isDotcontextBinaryOnPath (filesystem-only PATH scan, injectable env/platform for tests, Windows launcher variants covered), resolveHookDispatchCli, buildHookDispatchCommand, and getCanonicalHookDispatchCommands.
  • Claude Code and Codex templates become builders (buildClaudeCodeHookTemplates / buildCodexHookTemplates) that resolve the command at install time; the static *_HOOK_TEMPLATES constants are kept (pinned form) for compatibility.
  • Docs updated where the persisted dispatch command is described (using-with-hooks en/pt-br, hook-session-flow pt-br, architecture, README dispatch table). One-shot operator commands in docs (hook install, mcp:install, etc.) intentionally keep @latest.

Tests

  • New suite hookDispatchCommands.test.ts: binary detection (POSIX + win32, absent, directory-name collision), command resolution both ways, canonical-form acceptance, legacy @latest rejection.
  • Updated install-service tests: assertions now use buildHookDispatchCommand(...) so they stay deterministic on machines with or without a global dotcontext binary; legacy-upgrade tests now treat the @latest form as the legacy input.
  • npm run build and npm test -- --runInBand pass.

🤖 Generated with Claude Code

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>

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 @latest upgrade 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.

Comment on lines +67 to +69
if (fs.statSync(path.join(directory, name)).isFile()) {
return true;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +128 to +132
if (typeof command !== 'string') {
return false;
}

return command === expected;
return getCanonicalHookDispatchCommands(source).includes(command);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines 78 to 81
export interface BuildCodexTomlHookBlocksOptions {
includeFeatures?: boolean;
command?: ResolveHookDispatchCommandOptions;
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to resolveOptions in d576133.

- 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants