cli: stamp agent hook configs with CLI version for drift detection#982
cli: stamp agent hook configs with CLI version for drift detection#982gtrrz-victor wants to merge 3 commits intomainfrom
Conversation
Adds an `entireMeta.cli_version` stamp to every hook config file written by `entire enable` (across all 7 agents: claude-code, codex, copilot-cli, cursor, factoryai-droid, gemini, opencode). A new `CheckHookDrift` compares each installed stamp against a single global `MinCompatibleCLIVersion` floor; `entire status` and `entire enable` surface stale installs and suggest `entire enable --force`. Seeded at "0.0.0" so no drift warnings fire today — bump the constant in hook_command.go when a future release breaks a hook contract. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 44058d39a9cc
There was a problem hiding this comment.
Pull request overview
Adds hook-config version stamping and a drift-detection path so the CLI can warn when installed agent hooks were generated by an older (potentially incompatible) Entire CLI version.
Changes:
- Introduces
entireMeta.cli_versionstamping for JSON-based agent hook configs and an equivalent stamp for OpenCode’s TS plugin. - Adds
agent.CheckHookDrift+ status/enable-time warnings when hooks are stale relative to a single global compatibility floor. - Expands unit tests for hook-meta round-trips and basic drift helpers.
Reviewed changes
Copilot reviewed 17 out of 17 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| cmd/entire/cli/status.go | Shows a “Hooks · stale for …” warning in entire status output. |
| cmd/entire/cli/setup.go | Emits a drift warning before entire enable prompts. |
| cmd/entire/cli/agent/drift.go | Implements drift detection via installed hook stamps vs global floor. |
| cmd/entire/cli/agent/drift_test.go | Adds tests for semver normalization and dev-build short-circuit. |
| cmd/entire/cli/agent/hook_command.go | Adds hook meta types/helpers, TS meta parsing, and the global floor constant. |
| cmd/entire/cli/agent/hook_command_test.go | Adds tests for JSON/TS hook meta read/write utilities. |
| cmd/entire/cli/agent/capabilities.go | Adds AsHookVersionSupport type-assertion helper. |
| cmd/entire/cli/agent/claudecode/hooks.go | Stamps .claude/settings.json and implements ReadHookMeta. |
| cmd/entire/cli/agent/claudecode/hooks_test.go | Verifies stamp is written and backfilled on no-op installs. |
| cmd/entire/cli/agent/cursor/hooks.go | Stamps .cursor/hooks.json and implements ReadHookMeta. |
| cmd/entire/cli/agent/copilotcli/hooks.go | Stamps .github/hooks/entire.json and implements ReadHookMeta. |
| cmd/entire/cli/agent/codex/hooks.go | Preserves top-level keys, stamps .codex/hooks.json, implements ReadHookMeta. |
| cmd/entire/cli/agent/geminicli/hooks.go | Stamps .gemini/settings.json and implements ReadHookMeta. |
| cmd/entire/cli/agent/factoryaidroid/hooks.go | Stamps .factory/settings.json and implements ReadHookMeta. |
| cmd/entire/cli/agent/opencode/plugin.go | Adds install-time placeholder for embedding hook meta JSON in TS plugin. |
| cmd/entire/cli/agent/opencode/hooks.go | Embeds meta stamp into plugin output and implements ReadHookMeta. |
| cmd/entire/cli/agent/opencode/entire_plugin.ts | Adds the // entireMeta: ... stamp line template. |
| names := make([]string, 0, len(drift)) | ||
| for _, r := range drift { | ||
| names = append(names, string(r.Agent)) | ||
| } | ||
| fmt.Fprintf(w, "Warning: hooks for %s were installed by an older CLI. Re-run `entire enable --force` to refresh.\n\n", strings.Join(names, ", ")) |
There was a problem hiding this comment.
This re-implements drift agent list formatting inline. Since cli/status.go already added formatDriftAgentList(reports), consider reusing it here to keep output consistent and avoid duplicated logic.
| names := make([]string, 0, len(drift)) | |
| for _, r := range drift { | |
| names = append(names, string(r.Agent)) | |
| } | |
| fmt.Fprintf(w, "Warning: hooks for %s were installed by an older CLI. Re-run `entire enable --force` to refresh.\n\n", strings.Join(names, ", ")) | |
| fmt.Fprintf(w, "Warning: hooks for %s were installed by an older CLI. Re-run `entire enable --force` to refresh.\n\n", formatDriftAgentList(drift)) |
| continue | ||
| } | ||
| hs, ok := AsHookSupport(ag) | ||
| if !ok || !hs.AreHooksInstalled(ctx) { | ||
| continue |
There was a problem hiding this comment.
CheckHookDrift calls AreHooksInstalled before checking whether MinCompatibleCLIVersion disables drift ("0.0.0") and before confirming the agent implements HookVersionSupport. Since external agents implement HookSupport, this can spawn external binaries during entire status/entire enable even when drift detection is globally disabled. Consider short-circuiting early when the floor normalizes to v0.0.0 and/or checking HookVersionSupport first so external agents are skipped without calling AreHooksInstalled.
| // The check is intentionally cheap — it does a filesystem read per installed | ||
| // agent — so `entire status` and `entire enable` can call it on every run | ||
| // without concern. | ||
| func CheckHookDrift(ctx context.Context) []DriftReport { |
There was a problem hiding this comment.
There are tests for normalizeSemver and the dev-build short-circuit, but none that exercise the core drift decision (missing stamp => drift once the floor is raised; installed < required => drift; installed >= required => no drift). Since MinCompatibleCLIVersion is a const, consider factoring the compare/decision into a helper that can be unit-tested (or making the floor injectable for tests).
A plain `entire enable` on a pre-stamp config previously wrote just the stamp and returned, marking the install as current even though the hook payload on disk was untouched. Once MinCompatibleCLIVersion rises above "0.0.0", that would let drift silently clear without refreshing the actual hooks. Now each agent promotes missing-stamp to force=true, so the hook payload is removed and re-added alongside the stamp write. Gemini's legacy-cleanup test seeds a stamp to keep exercising the cleanup-only path in isolation. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 9901fadee2b6
|
Addressed P1 — missing stamp now promotes to force reinstall across all 7 JSON-config agents (OpenCode's content-diff idempotency already handled it). Gemini's legacy-cleanup-only test now seeds a stamp so it stays focused on its original scenario. New |
- drift.go: hoist "dev" and "v0.0.0" to named consts (devVersion, zeroSemver) so goconst no longer flags the three repetitions - claudecode: maintidx nolint on InstallHooks with the same rationale already used by factoryaidroid - All seven agents' ReadHookMeta: annotate the "file missing → no stamp" return with //nolint:nilerr Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> Entire-Checkpoint: 75d489a446f2
Summary
.claude/settings.json,.cursor/hooks.json,.codex/hooks.json,.github/hooks/entire.json,.gemini/settings.json,.factory/settings.json,.opencode/plugins/entire.ts) with a top-levelentireMeta.cli_versiononentire enable.Why
When a CLI release needs to modify hook wiring, existing users keep running stale hooks silently until something fails cryptically. The only remediation (`entire enable --force`) was undocumented for this purpose, and nothing told users they needed it. This gives us a one-line floor bump + automatic warning path for every future hook contract break.
Design notes
Test plan
🤖 Generated with Claude Code
Note
Medium Risk
Touches hook installation code across multiple agents and adds new filesystem reads/writes plus user-facing warnings, which could affect enable/status behavior if edge cases in config parsing occur. The compatibility floor is currently
0.0.0, so drift warnings are gated off until intentionally raised.Overview
Adds a new hook-config metadata stamp (
entireMeta.cli_version) written duringInstallHooksacross all built-in agents (JSON configs plus OpenCode’s TS plugin), including backfilling the stamp even when hook install would otherwise no-op.Introduces
agent.CheckHookDrift(with semver normalization and a globalMinCompatibleCLIVersionfloor) and wires it intoentire statusand the interactive enable/configure flow to warn when hooks appear stale and recommendentire enable --force.Adds
HookVersionSupport/AsHookVersionSupportand per-agentReadHookMetaimplementations, plus unit tests for meta round-tripping and drift gating (including a dev-build short-circuit).Reviewed by Cursor Bugbot for commit f85e40b. Configure here.