Skip to content

cli: stamp agent hook configs with CLI version for drift detection#982

Draft
gtrrz-victor wants to merge 3 commits intomainfrom
add-versions-agent-hook-configurations
Draft

cli: stamp agent hook configs with CLI version for drift detection#982
gtrrz-victor wants to merge 3 commits intomainfrom
add-versions-agent-hook-configurations

Conversation

@gtrrz-victor
Copy link
Copy Markdown
Contributor

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

Summary

  • Stamp every agent hook config (.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-level entireMeta.cli_version on entire enable.
  • New `agent.CheckHookDrift` compares installed stamp vs a single global `agent.MinCompatibleCLIVersion` floor.
  • `entire status` renders a yellow "Hooks · stale for " line; `entire enable` prints the same warning before its prompts. Remediation is the existing `entire enable --force`.
  • Floor seeded at `"0.0.0"` — no drift warnings fire today. Bump the single constant in `cmd/entire/cli/agent/hook_command.go` when a future release breaks a hook contract; all older installs will see one warning and re-run `entire enable --force`.

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

  • Single `HookVersionSupport` interface method (`ReadHookMeta(ctx)`) — the compatibility floor is a package-level const, not per-agent, keeping the mechanism boring.
  • JSON agents stamp via `agent.WriteJSONHookMeta(map)`; OpenCode's TS plugin embeds a `// entireMeta: {...}` comment via a template placeholder.
  • Stamp is backfilled on any `entire enable` — users on pre-stamp installs acquire an `entireMeta` field the next time they enable (even a no-op enable).
  • Dev builds (`versioninfo.Version == "dev"`) short-circuit drift entirely.
  • Vogon (test agent) skipped by design — `AreHooksInstalled` returns false.

Test plan

  • `go test ./cmd/entire/cli/agent/...` (new round-trip + drift-gate unit tests all pass)
  • `mise run check` (blocked locally: golangci-lint binary is v1 vs mise-pinned v2; unrelated to this PR)
  • Manual: run `entire enable --agent claude-code` in a fixture repo; inspect `.claude/settings.json` for `entireMeta.cli_version`. Edit it to `"0.0.0-stale"`, temporarily bump the floor const, confirm `entire status` warns, run `entire enable --force`, confirm re-stamp.
  • E2E canary (`mise run test:e2e:canary`) — don't run on draft

🤖 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 during InstallHooks across 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 global MinCompatibleCLIVersion floor) and wires it into entire status and the interactive enable/configure flow to warn when hooks appear stale and recommend entire enable --force.

Adds HookVersionSupport/AsHookVersionSupport and per-agent ReadHookMeta implementations, 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.

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
Copilot AI review requested due to automatic review settings April 18, 2026 17:32
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

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_version stamping 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.

Comment thread cmd/entire/cli/setup.go
Comment on lines +298 to +302
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, ", "))
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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.

Suggested change
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))

Copilot uses AI. Check for mistakes.
Comment on lines +45 to +49
continue
}
hs, ok := AsHookSupport(ag)
if !ok || !hs.AreHooksInstalled(ctx) {
continue
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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.

Copilot uses AI. Check for mistakes.
Comment on lines +33 to +36
// 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 {
Copy link

Copilot AI Apr 18, 2026

Choose a reason for hiding this comment

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

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).

Copilot uses AI. Check for mistakes.
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
@gtrrz-victor
Copy link
Copy Markdown
Contributor Author

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 TestInstallHooks_MissingStampForcesReinstall in claudecode pins the invariant: stale managed-prefix command + no stamp + plain entire enable (no --force) must replace the command, not just write the stamp.

- 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
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.

2 participants