Skip to content

Handle missing Entire CLI in agent hooks#929

Open
gtrrz-victor wants to merge 11 commits intomainfrom
warn-entire-enable-but-not-installed
Open

Handle missing Entire CLI in agent hooks#929
gtrrz-victor wants to merge 11 commits intomainfrom
warn-entire-enable-but-not-installed

Conversation

@gtrrz-victor
Copy link
Copy Markdown
Contributor

@gtrrz-victor gtrrz-victor commented Apr 12, 2026

Summary

Fixes #880.

Handle missing entire gracefully across built-in agent hooks, and tighten managed-hook detection so Entire only matches its own direct or wrapped hook commands.

What Changed

  • add shared production hook wrappers for missing-entire handling
  • add a shared managed-hook matcher that recognizes Entire's direct commands and production wrappers without using broad substring matching
  • switch Claude Code, Gemini CLI, Codex, Copilot CLI, Cursor, and Factory AI Droid hook cleanup logic to the shared matcher
  • preserve unrelated user hooks whose command text merely mentions entire
  • add regression tests for wrapper-aware matching and uninstall preservation of a user hook containing entire as plain text
  • keep warning behavior unchanged for integrations that surface hook messages, while silent integrations remain silent when entire is unavailable

Verification

  • go test ./cmd/entire/cli/agent/...

Agent Examples

Claude Code
image

Codex
image

Gemini
image

Droid (on stop hooks)
image

Copilot AI review requested due to automatic review settings April 12, 2026 05:41
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

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 improves robustness of agent hook integrations by making production-installed hooks gracefully no-op (or emit a one-time session-start warning) when the entire CLI binary is missing from PATH, and by making stored hook JSON configs more readable by disabling HTML escaping.

Changes:

  • Added shared production hook-command wrappers to warn once on session start and silently exit for other hooks when entire is missing.
  • Updated multiple agent hook installers (and tests) to use the new wrappers in production mode.
  • Stopped HTML-escaping JSON when writing hook config files and updated OpenCode plugin generation to include the missing-entire warning.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated no comments.

Show a summary per file
File Description
cmd/entire/cli/jsonutil/json.go Disable HTML escaping for indented JSON output and add a no-escape marshal helper for compact JSON.
cmd/entire/cli/agent/hook_command.go Introduce shared wrapper helpers for production hook commands (warn-once session-start and silent no-op).
cmd/entire/cli/agent/hook_command_test.go Add unit tests for new hook command wrappers and warning formatting.
cmd/entire/cli/agent/opencode/plugin.go Add placeholder for embedding a missing-entire warning in the generated plugin.
cmd/entire/cli/agent/opencode/hooks.go Populate the new warning placeholder during plugin generation.
cmd/entire/cli/agent/opencode/entire_plugin.ts Implement session-start warning and silent no-op behavior when entire is missing.
cmd/entire/cli/agent/opencode/hooks_test.go Update OpenCode plugin tests for the new spawn/wrapper behavior and warning constant.
cmd/entire/cli/agent/geminicli/hooks.go Wrap production hook commands; switch settings JSON writes to no-HTML-escape marshaling; adjust hook detection.
cmd/entire/cli/agent/geminicli/hooks_test.go Update expected commands/fixtures to match wrapped production hooks.
cmd/entire/cli/agent/factoryaidroid/hooks.go Wrap production hook commands; switch JSON writes to no-HTML-escape marshaling; adjust hook detection.
cmd/entire/cli/agent/factoryaidroid/hooks_test.go Update expected commands to match wrapped production hooks.
cmd/entire/cli/agent/cursor/hooks.go Wrap production hook commands; switch JSON writes to no-HTML-escape marshaling; adjust hook detection.
cmd/entire/cli/agent/cursor/hooks_test.go Update expected commands to match wrapped production hooks.
cmd/entire/cli/agent/copilotcli/hooks.go Wrap production hook commands; switch JSON writes to no-HTML-escape marshaling; adjust hook detection.
cmd/entire/cli/agent/copilotcli/hooks_test.go Update expected commands to match wrapped production hooks.
cmd/entire/cli/agent/codex/hooks.go Wrap production hook commands; switch JSON writes to no-HTML-escape marshaling; adjust hook detection.
cmd/entire/cli/agent/codex/hooks_test.go Update assertions to account for wrapped production hook commands/warnings.
cmd/entire/cli/agent/claudecode/hooks.go Wrap production hook commands; switch JSON writes to no-HTML-escape marshaling; adjust hook detection.
cmd/entire/cli/agent/claudecode/hooks_test.go Update expected commands to match wrapped production hooks.
Comments suppressed due to low confidence (6)

cmd/entire/cli/agent/geminicli/hooks.go:480

  • isEntireHook was changed to use strings.Contains(command, prefix) with very broad prefixes like "entire ". This will incorrectly classify user hooks such as echo "entire is great" as an Entire hook and remove them during force install/uninstall. Consider matching more precisely (e.g., HasPrefix on trimmed command for raw commands, plus a separate check for wrapped commands like strings.Contains(command, " exec entire ") / " exec go run ", or a stricter pattern that targets entire hooks <agent> specifically).
// isEntireHook checks if a command is an Entire hook
func isEntireHook(command string) bool {
	for _, prefix := range entireHookPrefixes {
		if strings.Contains(command, prefix) {
			return true
		}
	}
	return false

cmd/entire/cli/agent/factoryaidroid/hooks.go:466

  • isEntireHook now uses strings.Contains(command, "entire "), which can match unrelated user commands that merely mention the word "entire" (e.g., echo "entire..."). That can cause user-defined hooks to be deleted when force is used or during uninstall. Prefer a stricter match that targets actual hook invocations (e.g., HasPrefix for raw entire ... / go run ... plus a narrower check for wrapped commands containing exec entire ...).
// isEntireHook checks if a command is an Entire hook
func isEntireHook(command string) bool {
	for _, prefix := range entireHookPrefixes {
		if strings.Contains(command, prefix) {
			return true
		}
	}
	return false

cmd/entire/cli/agent/cursor/hooks.go:369

  • isEntireHook switched from HasPrefix to Contains using prefixes like "entire ". This can incorrectly treat user hooks that mention "entire" anywhere in the command as managed by Entire, and remove them on force/uninstall. Use a more specific predicate (e.g., match entire hooks cursor / go run ... hooks cursor at the start, or detect wrapped commands by looking for exec entire hooks cursor rather than any entire substring).
func isEntireHook(command string) bool {
	for _, prefix := range entireHookPrefixes {
		if strings.Contains(command, prefix) {
			return true
		}
	}
	return false

cmd/entire/cli/agent/copilotcli/hooks.go:325

  • isEntireHook now uses strings.Contains(bash, prefix) where prefix includes "entire ". This will match unrelated user hook commands that merely contain the word "entire" (e.g., echo "entire") and may remove them on force/uninstall. Suggest narrowing the match to actual Entire hook invocations (e.g., check for (^|\s)exec\s+entire\s+hooks\s+copilot-cli\b or HasPrefix for raw commands, plus a targeted check for the wrapped exec ... form).
// isEntireHook checks if a hook entry's bash command belongs to Entire.
func isEntireHook(bash string) bool {
	for _, prefix := range entireHookPrefixes {
		if strings.Contains(bash, prefix) {
			return true
		}
	}
	return false

cmd/entire/cli/agent/codex/hooks.go:306

  • isEntireHook uses strings.Contains(command, "entire "), which can misclassify user hooks that mention "entire" somewhere in the command as Entire-managed and remove them during force/uninstall. Consider tightening the check to match actual hook invocations (e.g., raw HasPrefix("entire hooks codex ") / local-dev prefix, and for wrapped commands check for "exec entire hooks codex " specifically).
func isEntireHook(command string) bool {
	for _, prefix := range entireHookPrefixes {
		if strings.Contains(command, prefix) {
			return true
		}
	}
	return false

cmd/entire/cli/agent/claudecode/hooks.go:470

  • isEntireHook now matches prefixes via strings.Contains, including the very generic "entire ". This can cause user hooks that simply mention "entire" to be treated as Entire hooks and removed on force/uninstall. Recommend a narrower match that specifically targets entire hooks claude-code ... / go run ${CLAUDE_PROJECT_DIR}... hooks claude-code ..., and separately recognizes the wrapped exec entire ... form rather than any substring match.
// isEntireHook checks if a command is an Entire hook (old or new format)
func isEntireHook(command string) bool {
	for _, prefix := range entireHookPrefixes {
		if strings.Contains(command, prefix) {
			return true
		}
	}
	return false

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d2d1b8f. Configure here.

gtrrz-victor and others added 8 commits April 12, 2026 08:18
Entire-Checkpoint: 3600e9354bd4
This function was defined but never called by any agent — only exercised
by its own unit test.  Removing dead code to keep the public API surface
accurate.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
WrapProductionJSONSessionStartHookCommand → WrapProductionJSONWarningHookCommand
WrapProductionPlainTextSessionStartHookCommand → WrapProductionPlainTextWarningHookCommand

These functions describe an output format (JSON vs plain text warning on
stdout), not a lifecycle event.  The Droid agent already uses the plain
text variant on the Stop hook, making the old name actively confusing.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Parse the hooks.json as structured data and assert exact command strings
using the wrapper helpers, consistent with how all other agents' tests
verify installed hooks.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@gtrrz-victor
Copy link
Copy Markdown
Contributor Author

Code review follow-ups:

  • Remove unused WrapProductionSessionStartHookCommand (stderr variant) — dead code, no agent called it
  • Rename WrapProductionJSON/PlainTextSessionStartHookCommand*WarningHookCommand — the old names implied a lifecycle event (SessionStart) but these describe an output format, and Droid already used the plain text variant on its Stop hook
  • Strengthen Codex test to parse JSON and verify exact wrapped commands, consistent with all other agents' tests

@gtrrz-victor gtrrz-victor marked this pull request as ready for review April 12, 2026 07:23
@gtrrz-victor gtrrz-victor requested a review from a team as a code owner April 12, 2026 07:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Suppress missing 'entire' failures in Codex and Claude hooks

2 participants