Skip to content

fix(doctor): count Brain's pre-push wrapper as installed hooks - #2959

Open
Beehtrix wants to merge 4 commits into
entireio:mainfrom
Beehtrix:fix-doctor-2949-brain-chain
Open

Beehtrix wants to merge 4 commits into
entireio:mainfrom
Beehtrix:fix-doctor-2949-brain-chain

Conversation

@Beehtrix

@Beehtrix Beehtrix commented Oct 29, 2026 •

Copy link
Copy Markdown

Fixes #2949

@peyton-alt @Soph: I asked on the issue to be assigned (#2949 (comment)) and haven't had a reply yet. I'm opening this so it can be reviewed. If you'd rather fix it on the Brain side instead (giving Brain's wrapper a marker doctor recognizes), tell me and I'll close this.

Why

entire brain connect installs its own pre-push hook. It moves the hook it finds (Entire's) to pre-push.pre-entire-brain, and its own hook ends with:

backup="$0.pre-entire-brain"
if [ -x "$backup" ]; then exec "$backup" "$@"; fi

So Entire's hook still runs, with git's stdin and arguments. But the new pre-push doesn't have the # Entire CLI hooks marker, so hookStateIn treats it as foreign and reports the whole set as GitHooksAbsent. I checked what happens next with the real plugin (brain v0.4.0, see Verification):

  • entire doctor shows Git hooks: NOT INSTALLED / "Commits in this repository are not captured as checkpoints" on a setup that works, and suggests --force to fix it.
  • EnsureSetup runs on every agent turn and reinstalls whenever IsGitHookInstalled is false. On the first turn after brain connect, it backs Brain's hook up to pre-push.pre-entire, puts Entire's hook in front, and prints [entire] Your pre-push hook still runs, after Entire's (moved to .git/hooks/pre-push.pre-entire). into the agent's hook output. After that, doctor says OK again.

Pushes keep working after that re-wrap. Entire's new pre-push runs Entire, then Brain's hook from pre-push.pre-entire. Brain's $0-relative chain then looks for pre-push.pre-entire.pre-entire-brain, which doesn't exist, so it stops there (Brain's source says that's deliberate, so the host's hook doesn't run twice). The old Entire copy in pre-push.pre-entire-brain is left behind and nothing runs it. So the bug users see is doctor's false report (and its --force advice) between brain connect and the next agent turn, plus a re-wrap that isn't needed. Commits and pushes were always captured.

The fix

The changes are in cmd/entire/cli/strategy/hooks.go, plus a skip parameter on refreshMovedEntireHooks in hook_backup.go.

hookStateIn: if pre-push doesn't have Entire's marker but is Brain's wrapper, it classifies the copy the wrapper runs (pre-push.pre-entire-brain) the same way it classifies pre-push today:

  • Entire's current hook there → GitHooksCurrent, and doctor says OK.
  • Entire's hook from before the ref list → GitHooksOutdated, and it gets reinstalled (see below).
  • Anything else → GitHooksAbsent, as before. That includes: no copy; a copy that isn't Entire's; a symlink in either place (still read NoFollow); a wrapper git won't run, or a copy Brain's [ -x ] guard skips, because it has no execute bit (not checked on Windows, which has no execute bit); a wrapper whose exec line was removed or commented out; and Brain's wrapper in any hook other than pre-push.

Brain's wrapper only counts when its marker (# entire-brain push hook), the backup="$0.pre-entire-brain" line and the if [ -x "$backup" ]; then exec "$backup" "$@"; fi line each appear as whole lines. Any other wrapper without Entire's marker still reads Absent, so the existing reclaim path for husky, lefthook and other hook managers doesn't change.

installHooks: when pre-push is Brain's wrapper and the copy it runs is Entire's (same check), it writes Entire's hook into that copy and leaves the wrapper alone, instead of backing the wrapper up to pre-push.pre-entire. If Entire had already backed up the user's own pre-push to pre-push.pre-entire before Brain was installed, the copy gets the chained hook, the same content Entire would write at the hook path. Its $(dirname "$0")-relative chain still finds the user's hook from there. Before writing that copy, it refreshes any other moved copies of Entire's hook to the hook that never chains, the same way the normal install path does. Without that, a manager's wrapper in pre-push.pre-entire that runs an older Entire copy could chain back to pre-push.pre-entire forever. This covers an agent turn with a stale copy (Outdated) and a re-run of entire enable. The copy is only rewritten when it reads Outdated or is exactly the pre-push hook Entire writes today (chained or not, for the prefix its own line runs). If the user added lines to it, it's left as it is, on enable and on agent turns, the same rule TestCheckGitHookState_UserAdditionsAreNotDrift and #2878 set for Entire's own hook: it still reads Current, so doctor says OK and nothing re-wraps. Before this change, both re-wrapped Brain's hook. In every Absent case above, install still takes the old re-wrap path, which puts Entire's hook in front where it runs.

I also updated the ReinstallGitHooks doc comment, which said a wrapper without the marker always reads Absent.

The diff is 2 production files (+135/-5) and 2 test files (+289).

Overlap: #2640, #2643 and #2388 (still open) touch the same functions in strategy/hooks.go. None of them covers the Brain layout. I ran git merge-tree against each PR's head: each already conflicts with current main, and this branch adds no new conflicting files or hunks (same files, 3/6/3 hunks with or without this branch).

Tests

  • strategy/hooks_test.go TestCheckGitHookState_BrainPrePushWrapper (table, parallel, filesystem-only):
    • Brain's wrapper plus Entire's current hook reads Current
    • plus Entire's hook from before the ref list reads Outdated
    • plus a hook that isn't Entire's reads Absent
    • with no moved copy reads Absent
    • a non-Brain wrapper next to Entire's moved copy reads Absent
    • Brain's wrapper with its exec line commented out reads Absent
    • Entire's moved copy without an execute bit reads Absent (skipped on Windows)
    • Brain's wrapper without an execute bit reads Absent (skipped on Windows)
  • TestCheckGitHookState_BrainWrapperOnlyCountsForPrePush: Brain's wrapper in post-commit's place reads Absent.
  • TestInstallGitHooks_BrainPrePushWrapperRefreshesOtherMovedCopies: Brain's layout, plus a manager wrapper in pre-push.pre-entire that runs pre-push.old, an Entire hook from before the chain guard that chains straight back. After InstallGitHook, pre-push.old is the hook that never chains, the copy Brain runs is chained, and both wrappers are unchanged.
  • TestInstallGitHooks_KeepsBrainPrePushWrapper: real InstallGitHook, with and without the user's own pre-push there first, then Brain's layout with a stale moved copy. Both ReinstallGitHooks (agent turn) and InstallGitHook (enable) leave the wrapper byte-for-byte. They rewrite the copy to the current hook (chained when the user's hook is in pre-push.pre-entire) and leave the user's hook where it was, or create no pre-push.pre-entire when there wasn't one. Afterwards the set reads Current.
  • TestInstallGitHooks_BrainCopyKeepsUserAdditions: Brain's layout with a line the user added to the copy Brain runs (current hook, chained current hook, hook from before the ref list, current hook missing the chain). InstallGitHook (enable) and ReinstallGitHooks (agent turn) leave the copy byte-for-byte and the set reads Current. A copy that is exactly Entire's hook is still rewritten: from before the ref list or run from a working tree (Outdated, both paths), and today's hook for another prefix or without the chain to the user's hook (enable).
  • TestInstallGitHooks_BrainCopyWithUserLineStillRefreshesOtherMovedCopies: the layout from the test above, but the copy Brain runs has a line the user added. InstallGitHook keeps that line and still rewrites pre-push.old, so it can't chain back.
  • doctor_test.go TestCheckGitHooks_BrainPrePushWrapper: a real InstallGitHook, then the Brain layout. Asserts checkGitHooks prints ✓ Git hooks: OK both without and with --force, and that Brain's wrapper is left byte-for-byte as it was.

The wrapper text in the tests matches what the real brain v0.4.0 writes, except for the plugin path (compared against a real install, see below).

Failing without the fix

The new tests at the branch head, with hooks.go and hook_backup.go reverted to main @ 38ef300:

$ go test -count=1 -run 'BrainPrePushWrapper|BrainWrapperOnlyCountsForPrePush|KeepsBrainPrePushWrapper' ./cmd/entire/cli/strategy/ ./cmd/entire/cli/
--- FAIL: TestInstallGitHooks_BrainPrePushWrapperRefreshesOtherMovedCopies
        expected: "#!/bin/sh\n# entire-brain push hook\n..."   (Brain's wrapper)
        actual  : "#!/bin/sh\n# Entire CLI hooks\n..."          (Entire's hook re-wrapped in front)
--- FAIL: TestInstallGitHooks_KeepsBrainPrePushWrapper
    --- FAIL: .../agent_turn_reinstall/user_hook_false   expected: 2, actual: 0
    --- FAIL: .../agent_turn_reinstall/user_hook_true    expected: 2, actual: 0
    --- FAIL: .../enable/user_hook_false                 expected: 2, actual: 0
    --- FAIL: .../enable/user_hook_true                  expected: 2, actual: 0
--- FAIL: TestCheckGitHookState_BrainPrePushWrapper
    --- FAIL: .../chains_to_Entire's_current_hook                     expected: 1, actual: 0
    --- FAIL: .../chains_to_Entire's_hook_from_before_the_ref_list    expected: 2, actual: 0
FAIL	github.com/entireio/cli/cmd/entire/cli/strategy
--- FAIL: TestCheckGitHooks_BrainPrePushWrapper
        "Git hooks: NOT INSTALLED\n  Commits in this repository are not captured as checkpoints.\n  Fix: reinstall the missing or out-of-date git hooks (any non-Entire hook is backed up).\n  Run `entire doctor --force` to apply it.\n" does not contain "✓ Git hooks: OK"
FAIL	github.com/entireio/cli/cmd/entire/cli

(1 = GitHooksCurrent, 2 = GitHooksOutdated, 0 = GitHooksAbsent.) TestInstallGitHooks_BrainCopyKeepsUserAdditions fails on the first version of this PR (727ff09) in its four enable cases with a user line, and TestInstallGitHooks_BrainCopyWithUserLineStillRefreshesOtherMovedCopies fails there too: the copy comes back as Entire's template with the line gone. Its agent-turn cases already passed there, because a copy with the user's lines reads Current and an agent turn only rewrites stale hooks. Its exact-template cases fail if the "exactly today's hook" check is removed. The Absent cases pass on main too, because main reads every Brain layout as Absent. They're there to keep the new check from accepting a layout that doesn't run Entire's hook. Each one fails with expected: 0, actual: 1 without its check: each with only its own check removed (the moved-copy execute bit, the exec line, the wrapper execute bit, and the hook != prePushHook condition for the post-commit case). Everything passes at the branch head.

Verification

Linux amd64, git 2.56.0, go 1.27.3, mise 2026.10.21, golangci-lint 2.15.0. Clean environment: env -i with only HOME/PATH/Go cache variables, and no agent, plugin or telemetry variables. The real-plugin runs also used a throwaway HOME, config dir and cache.

At the branch head (5e67636, on main @ 38ef300):

  • mise run fmt: no changes. mise run lint: 0 issues, and gofmt, gomod, shellcheck, secretpatterns and mise all passed.

  • mise run test:ci: 1 failure, cmd/entire/cli/api TestClient_Get_NamesUnresolvableHost. It expects a DNS failure for entire-netcause-test.invalid, and my sandbox's resolver returns something else. Clean main @ 38ef300 gives the same result: the same single failure, with the same 92 packages ok. Because that step fails, the task stops before the canary, so I ran mise run test:e2e:canary separately. It passed. mise run test: 16584 tests, the same 1 failure.

  • GOOS=windows go test -c ./cmd/entire/cli/strategy/ compiles. CI on my fork passed (test, test-windows, integration a/b/c, canary, canary-windows, lint, CodeQL, govulncheck). controlplane failed there because the fork doesn't have the E2E secrets.

  • Real Brain, end to end. I installed the real release (entire plugin install brain, v0.4.0) into a temp HOME, then ran entire enable -y --agent claude-code in a fresh repo, then entire brain connect --repo-id … --api-url https://127.0.0.1:9. No account: connect writes the hook before its first sync, and the sync fails with "Not logged in". Brain's hook came out as quoted above, with Entire's hook in pre-push.pre-entire-brain. Then:

    doctor, released 0.12.2:        Git hooks: NOT INSTALLED
    doctor, main @ 38ef300:         Git hooks: NOT INSTALLED
    doctor, this branch:            ✓ Git hooks: OK
    
    • The real per-turn path: I piped a UserPromptSubmit payload into entire hooks claude-code user-prompt-submit (what .claude/settings.json runs; it calls EnsureSetup). On main, Brain's wrapper moved to pre-push.pre-entire and the [entire] Your pre-push hook still runs… line printed. On this branch, pre-push stayed byte-for-byte Brain's wrapper and nothing printed.
    • A line the user appended to pre-push.pre-entire-brain, then entire enable, an agent turn and entire doctor --force: the line is still there after each, doctor says ✓ Git hooks: OK, and a push runs Entire's hooks git pre-push origin once and then the user's line. On 727ff09, entire enable dropped the line.
    • git push to a local bare remote through Brain's wrapper, with a stub entire on PATH that logs its calls: Entire's hooks git pre-push origin ran exactly once, and Brain's memory notify was launched.
    • Stale copy (the hook from before the ref list written into pre-push.pre-entire-brain), then an agent turn: doctor says OUT OF DATE, the turn rewrites the copy to the current hook, and Brain's wrapper stays. Running entire enable again also leaves the wrapper in place.
    • chmod -x pre-push.pre-entire-brain, Brain's exec line commented out, or chmod -x pre-push (git then skips the hook with hint: The '.git/hooks/pre-push' hook was ignored…): doctor says NOT INSTALLED and the turn re-wraps, so Entire's hook runs on the next push, same as main.
    • The user's own pre-push first, then entire enable, brain connect, and entire enable again: a push runs Entire's hook and then the user's, both before and after the second enable. (On main, the second enable re-wraps Brain's hook and rotates the user's hook to pre-push.pre-entire.<timestamp>, which no longer runs, with a warning.)
    • A manager wrapper in pre-push.pre-entire that runs an Entire copy from before the chain guard (pre-push.old), then entire enable: the next push finishes, with entire hooks git pre-push called twice, once from the copy Brain runs and once from the refreshed pre-push.old through the manager's wrapper.

What I didn't run:

  • Windows itself. The CI Windows job only runs tests matching Windows|MSYS, so none of these tests run there. The new code is a Lstat through the existing os.Root plus string matching, and it skips the execute-bit check on Windows.
  • A hosted Brain sync (needs an account). It doesn't touch hooks.
  • The Entire side of a real push with a real login. I used a stub entire to count calls.

Known limits / open questions

  • This takes the CLI-side option from the issue. The other option is for Brain's wrapper to carry a marker doctor understands. I asked which you prefer on the issue and haven't heard back yet.
  • If Brain ever changes its marker, chain or exec line, the CLI goes back to reading Absent and re-wrapping. That fails safe: it's today's behavior, and pushes still run Entire's hook.
  • entire disable --uninstall leaves Entire's hook running behind Brain's wrapper, on this branch and on main after its re-wrap. restoreWrapped matches wrappers by literal file name, and Brain uses $0. That bug was already there before this PR and is filed separately as entire disable --uninstall leaves Entire's pre-push hook running behind entire-brain's wrapper #2957.
  • Two related behaviors came up in automated review on my fork. Both are on main today, and I reproduced both on clean main. (1) When the user's own hook is in pre-push.pre-entire behind Brain's layout, the re-wrap rotates it to pre-push.pre-entire.<timestamp>, where nothing runs it. On main that happens on the first agent turn after brain connect. With this PR it only happens when Brain's wrapper has lost its execute bit, which still takes the existing re-wrap path. (2) Refreshing moved Entire copies (refreshMovedEntireHooks) rewrites them from the template and silently drops lines a user added. On main that includes pre-push.pre-entire-brain. This PR no longer does that to pre-push.pre-entire-brain (see The fix), but other moved copies such as a hook manager's pre-push.old are still refreshed as on main. Separately, if the user's own hook lands in pre-push.pre-entire after they edited the copy Brain runs, Entire doesn't add the chain to that copy; adding the chain would mean rewriting their file. I'd rather handle both as separate issues than grow this PR.

Missing Entire-Checkpoint trailers

None of these commits has an Entire-Checkpoint trailer. CONTRIBUTING asks for them, but this work wasn't done in an Entire-captured session that produced one, and I won't make one up. If you need checkpoint linkage, I can redo the commits inside a captured session.

Copilot AI lite review requested due to automatic review settings October 29, 2026 08:55
@Beehtrix
Beehtrix requested a review from a team as a code owner October 29, 2026 08:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ⚠️ Failed 2026-10-29T09:01:53.279259Z
We couldn’t complete this review. Comment @codex review to try again.
727ff09 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

entire-brain moves Entire's pre-push hook to pre-push.pre-entire-brain and
puts its own hook in front, which execs that copy with git's stdin and
arguments. The hook in pre-push lacks Entire's marker, so the hook state read
Absent: `entire doctor` reported "Git hooks: NOT INSTALLED" on a working
setup and offered `--force`, and EnsureSetup re-wrapped Brain's hook on the
next agent turn.

When pre-push is Brain's wrapper, classify the moved copy it runs instead:
a current copy reads Current, an old one Outdated, and anything that is not
Entire's hook still reads Absent. Brain's wrapper only counts when it is a
runnable regular file in pre-push whose marker, backup= assignment and exec
line each appear as whole lines, and the copy it runs is a regular file with
an execute bit (not checked on Windows).

installHooks leaves Brain's wrapper alone and refreshes the copy it runs in
place, chained to the user's own pre-push backup when there is one, but only
when that copy reads Outdated or is exactly the hook Entire writes today; a
copy the user has added lines to is left as it is. It also refreshes the
other moved copies of Entire's hook first, so a hook manager's wrapper that
runs an older copy cannot chain back into pre-push.pre-entire forever. In
every case the wrapper does not qualify, install takes the existing re-wrap
path.
@Beehtrix
Beehtrix force-pushed the fix-doctor-2949-brain-chain branch from 727ff09 to 5e67636 Compare October 29, 2026 09:24
Copilot AI lite review requested due to automatic review settings October 29, 2026 09:24

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

@Beehtrix

Copy link
Copy Markdown
Author

@peyton-alt @Soph a heads-up on this PR: I pushed one more change (now 5e67636, still a single commit). While checking it against Brain's layout I found my first version rewrote the copy Brain runs (pre-push.pre-entire-brain) on entire enable and dropped any lines a user had added to it. That goes against the "user additions are not drift" rule from #1999 and #2878. Now that copy is only rewritten when it reads Outdated or is exactly the hook Entire writes today. If the user added lines, it's left as it is, on enable and on agent turns. I added tests that fail on the earlier version, and ran it against a real Brain v0.4.0 install. The PR body has the details. Happy to adjust if you'd rather handle edited copies differently.

Copilot AI lite review requested due to automatic review settings October 29, 2026 19:39

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Copilot AI lite review requested due to automatic review settings October 29, 2026 20:23

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

Copilot AI lite review requested due to automatic review settings October 30, 2026 08:01

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has insufficient remaining quota.

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.

Entite doctor conflict with brain setup

2 participants