Skip to content

fix: detect our own hooks by ownership instead of "file would change" - #653

Open
css521 wants to merge 2 commits into
Octane0411:mainfrom
css521:fix/hook-ownership-detection
Open

fix: detect our own hooks by ownership instead of "file would change"#653
css521 wants to merge 2 commits into
Octane0411:mainfrom
css521:fix/hook-ownership-detection

Conversation

@css521

@css521 css521 commented Aug 12, 2026

Copy link
Copy Markdown

Summary

  • split "a hook we should replace" from "a hook that is ours" — the same predicate was answering both, so a foreign hook read as a successful install
  • derive managedHooksPresent from actual ownership in both the Claude and Codex installers, instead of from "the removal pass would change the file"
  • add HookHealthReport.Issue.hooksMissing (error severity, auto-repairable) so a config another tool rewrote is visible and self-healing rather than silent
  • gate that issue on the caller's persisted intent, so it never resurrects hooks a user deliberately removed
  • regression coverage for both the installer and the health check

The failure

Open Island can stop receiving hook events while reporting itself as installed and healthy.

Reproduced on a machine that also had the closed-source Vibe Island app installed. ~/.claude/settings.json held zero OpenIslandHooks entries — a leftover vibe-island-bridge hook occupied the slots — and yet:

  • Settings said Claude hooks installed
  • the health panel showed only the informational Other hooks coexist: … notice, no error
  • the startup repair never ran, even though the persisted intent was installed

Net effect: the island showed "no open terminal sessions" with a Claude Code agent running in the next terminal tab. Feeding one event straight into the bridge by hand produced a correct, fully-attached session row, which isolated the break to hook invocation rather than the bridge, AppModel, terminal resolution, or persistence.

Root cause

One predicate answering two questions.

ClaudeHookInstaller.isLegacyOpenIslandHookCommand() matches vibe-island-bridge. That is right for "drop this entry when we write our own hooks" — two islands reacting to every event is not a supported state — but wrong for "are our hooks installed?".

status() derived managedHooksPresent from that removal pass, so a foreign hook counted as ours. That false positive then propagated:

claudeHooksInstalled true → Settings shows the hooks as present
shouldAutoInstall(.claudeCode) .installed && !presentfalse, so the repair written for exactly this case never fires
HookHealthCheck.checkClaude no issue type for "none of our hooks are here", so nothing to report and nothing to repair

CodexHookInstallationManager.status() had the same shape through mutation.changed, which is additionally true when the file merely re-serializes differently — we write sorted, pretty-printed JSON, so any hooks.json not already in that exact form read as installed.

Approach

isManagedHook() stays broad; installing still claims the slots as before. A new isOwnManagedHook() / ownHooksPresent answers the ownership question, matching only the command we recorded at install time or our hook CLI by name — OpenIslandHooks and the pre-rename VibeIslandHooks — so an app bundle that moved since install still reads as ours.

hooksMissing is reported only when the caller passes expectsInstalledHooks. repairHooksIfNeeded() does not consult the intent store, so reporting it unconditionally would reinstall hooks a user turned off on purpose and regress #324.

Behaviour change worth flagging

A user who only ever had the closed-source app's hooks is now recorded as .untouched rather than .installed by migrateIntentStoreIfNeeded(). They get first-run onboarding instead of a silent takeover, which seems like the better default, but it is a visible change for that group.

Not covered here

KimiHookInstaller, GeminiHookInstaller and CursorHookInstaller carry the same broad-predicate shape. They are left alone because HookHealthCheck does not cover those agents, so there is no reporting path to fix alongside them, and I have no reproduction for them. Worth a follow-up.

Verification

Passing locally:

  • scripts/lint-strings.sh — the lint step of the CI harness
  • scripts/check-docs.sh — the docs step
  • OpenIslandCore and OpenIslandCoreTests compile clean, including both new test files. Six of the seven changed files live in OpenIslandCore, which has no external dependencies, so I built it through a throwaway Package.swift containing only that target and its tests.

Not run locally, both for environment reasons rather than anything about the change:

  • test execution — swift-testing needs Testing.framework, which ships with Xcode, and this machine has only CommandLineTools. The bundle builds, then dlopen fails on the framework.
  • swift build of OpenIslandApp — a cold worktree has to re-clone Sparkle and swift-markdown-ui, and this network moved about 2 MB in ten minutes. HookInstallationCoordinator.swift is the one changed file this leaves unbuilt.

Please treat the harness run as the gate. The run is currently sitting at action_required, as outside-contributor runs on this repo do, so it needs a maintainer to approve it.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Improved hook health checks to identify missing Open Island hooks when installation is expected.
    • Prevented legacy bridge hooks from being incorrectly reported as Open Island hooks.
    • Improved malformed configuration handling and avoided duplicate diagnostics.
    • Hook repair and verification now more accurately reflect installation status.
  • Tests

    • Added coverage for missing, installed, malformed, and intentionally absent hook configurations.
    • Added regression coverage for legacy bridge hook cleanup.

Open Island could silently stop receiving hook events while reporting
itself as healthy and installed.

Reproduced on a machine that also had the closed-source Vibe Island app:
`~/.claude/settings.json` contained zero `OpenIslandHooks` entries — the
slots held a leftover `vibe-island-bridge` hook — yet Settings showed
"Claude hooks installed", the health panel showed no error, and the
startup repair never fired. No hook of ours ran, so the island stayed on
"no open terminal sessions" with an agent running in the next tab.

The cause is one predicate answering two different questions:

- ClaudeHookInstaller.isLegacyOpenIslandHookCommand() matches
  `vibe-island-bridge`, which is correct for "drop this when we write our
  own hooks" but wrong for "are our hooks installed?". Because
  `status()` derived managedHooksPresent from that removal pass, a
  foreign hook read as a successful install, and shouldNAutoInstall()'s
  `.installed && !present` repair — written for exactly this case — was
  never reached.
- CodexHookInstallationManager.status() had the same shape via
  `mutation.changed`, which is also true when the file merely
  re-serializes differently (we write sorted, pretty-printed JSON).

Split the two concerns:

- Keep isManagedHook() broad, so installing still claims the slots; two
  islands reacting to every event is not a supported state.
- Add isOwnManagedHook() / ownHooksPresent, matching only the command we
  installed or our hook CLI by name (current and pre-rename), and derive
  managedHooksPresent from that in both installers.
- Add HookHealthReport.Issue.hooksMissing: error severity and
  auto-repairable, so a config another tool rewrote is now visible and
  self-healing rather than silent.

hooksMissing is reported only when the caller passes
expectsInstalledHooks (persisted intent is `.installed`). repairHooksIfNeeded()
does not consult the intent store, so reporting it unconditionally would
reinstall hooks a user deliberately removed and regress Octane0411#324.

Side effect worth noting: a user who only ever had the closed-source
app's hooks is now recorded as `.untouched` rather than `.installed` by
migrateIntentStoreIfNeeded(), so they get onboarding instead of a silent
takeover.

Verified locally: scripts/lint-strings.sh and scripts/check-docs.sh pass.
swift build / swift test could not complete on this machine — a cold
worktree needs to re-clone Sparkle and swift-markdown-ui, and the network
here moves ~2 MB in 10 minutes — so both are left to CI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e5438d9-43ca-4c0a-b988-8c946e31ccb6

📥 Commits

Reviewing files that changed from the base of the PR and between bae9b96 and 018069b.

📒 Files selected for processing (3)
  • Sources/OpenIslandCore/ClaudeHookInstaller.swift
  • Sources/OpenIslandCore/CodexHookInstaller.swift
  • Tests/OpenIslandCoreTests/HookHealthCheckTests.swift
🚧 Files skipped from review as they are similar to previous changes (3)
  • Sources/OpenIslandCore/CodexHookInstaller.swift
  • Sources/OpenIslandCore/ClaudeHookInstaller.swift
  • Tests/OpenIslandCoreTests/HookHealthCheckTests.swift

📝 Walkthrough

Walkthrough

The change makes hook health checks intent-aware. It distinguishes Open Island-owned hooks from legacy managed hooks and reports missing hooks only when installation is expected. Claude and Codex tests cover foreign hooks, malformed configuration, healthy states, and intentional absence.

Changes

Hook health and ownership

Layer / File(s) Summary
Track Open Island hook ownership
Sources/OpenIslandCore/ClaudeHookInstaller.swift, Sources/OpenIslandCore/CodexHookInstaller.swift, Sources/OpenIslandCore/CodexHookInstallationManager.swift, Tests/OpenIslandCoreTests/ClaudeHooksTests.swift
Uninstallation tracks Open Island-owned hooks separately from broader managed-hook cleanup. Legacy Vibe Island hooks remain eligible for removal.
Report missing expected hooks
Sources/OpenIslandCore/HookHealthCheck.swift, Tests/OpenIslandCoreTests/HookHealthCheckTests.swift
Claude and Codex checks accept installation expectations and report repairable hooksMissing issues for parseable configurations without owned hooks.
Propagate persisted installation intent
Sources/OpenIslandApp/HookInstallationCoordinator.swift
Initial, pre-repair, and post-repair health checks use persisted agent intent to determine whether hooks are expected.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant HookInstallationCoordinator
  participant intentStore
  participant HookHealthCheck
  participant HookInstaller
  HookInstallationCoordinator->>intentStore: Read persisted agent intent
  HookInstallationCoordinator->>HookHealthCheck: Pass hook installation expectation
  HookHealthCheck->>HookInstaller: Check Open Island hook ownership
  HookInstaller-->>HookHealthCheck: Return ownership state
  HookHealthCheck-->>HookInstallationCoordinator: Return health report
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: detecting Open Island hooks by ownership instead of configuration changes.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/OpenIslandCore/CodexHookInstaller.swift`:
- Around line 154-160: Update CodexHookInstaller’s ownership detection by adding
containsOwnManagedHook and isOwnManagedHook, matching the ClaudeHookInstaller
pattern, and use the ownership-specific predicate when setting ownHooksPresent
and deciding mutation. Exclude legacyManagedStatusMessage from ownership checks,
while retaining it only for cleanup if needed. Add a regression test covering
entries with “Managed by Vibe Island” and verify
CodexHookInstallationManager.status() does not report Open Island hooks as
installed.
- Around line 38-54: Make CodexHookFileMutation in
Sources/OpenIslandCore/CodexHookInstaller.swift conform to Codable. Also update
HookHealthReport and every nested model type in
Sources/OpenIslandCore/HookHealthCheck.swift to conform to Codable, preserving
their existing Sendable conformances and behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 25149c66-cc58-4832-b848-83a29356f071

📥 Commits

Reviewing files that changed from the base of the PR and between 830f0ef and bae9b96.

📒 Files selected for processing (7)
  • Sources/OpenIslandApp/HookInstallationCoordinator.swift
  • Sources/OpenIslandCore/ClaudeHookInstaller.swift
  • Sources/OpenIslandCore/CodexHookInstallationManager.swift
  • Sources/OpenIslandCore/CodexHookInstaller.swift
  • Sources/OpenIslandCore/HookHealthCheck.swift
  • Tests/OpenIslandCoreTests/ClaudeHooksTests.swift
  • Tests/OpenIslandCoreTests/HookHealthCheckTests.swift

Comment on lines +38 to +54
/// Whether hooks of ours were actually found in the file.
///
/// Distinct from ``changed``, which is also true when the file merely
/// re-serializes differently (we write sorted, pretty-printed JSON). Use
/// this — never ``changed`` — to answer "are our hooks installed?".
public var ownHooksPresent: Bool

public init(contents: Data?, changed: Bool, hasRemainingHooks: Bool) {
public init(
contents: Data?,
changed: Bool,
hasRemainingHooks: Bool,
ownHooksPresent: Bool = false
) {
self.contents = contents
self.changed = changed
self.hasRemainingHooks = hasRemainingHooks
self.ownHooksPresent = ownHooksPresent

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Make the modified models Codable.

  • Sources/OpenIslandCore/CodexHookInstaller.swift#L38-L54: Add Codable to CodexHookFileMutation.
  • Sources/OpenIslandCore/HookHealthCheck.swift#L27-L29: Add Codable to HookHealthReport and its nested model types.

As per coding guidelines, “All models must be Sendable and Codable.”

📍 Affects 2 files
  • Sources/OpenIslandCore/CodexHookInstaller.swift#L38-L54 (this comment)
  • Sources/OpenIslandCore/HookHealthCheck.swift#L27-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/OpenIslandCore/CodexHookInstaller.swift` around lines 38 - 54, Make
CodexHookFileMutation in Sources/OpenIslandCore/CodexHookInstaller.swift conform
to Codable. Also update HookHealthReport and every nested model type in
Sources/OpenIslandCore/HookHealthCheck.swift to conform to Codable, preserving
their existing Sendable conformances and behavior.

Source: Coding guidelines

Comment on lines +154 to 160
let hadOwnHooks = containsManagedHook(in: existingGroups, managedCommand: managedCommand)
if hadOwnHooks {
ownHooksPresent = true
}

if cleanedGroups.count != existingGroups.count || hadOwnHooks {
mutated = true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Use an ownership-specific predicate.

Line 154 calls containsManagedHook. That predicate accepts legacyManagedStatusMessage, so a Vibe Island-only Codex configuration sets ownHooksPresent to true.

CodexHookInstallationManager.status() then reports Open Island hooks as installed. Intent migration can suppress the required repair.

Add containsOwnManagedHook and isOwnManagedHook, as in ClaudeHookInstaller. Do not use legacy status messages to establish ownership. Add a regression test with Managed by Vibe Island entries.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/OpenIslandCore/CodexHookInstaller.swift` around lines 154 - 160,
Update CodexHookInstaller’s ownership detection by adding containsOwnManagedHook
and isOwnManagedHook, matching the ClaudeHookInstaller pattern, and use the
ownership-specific predicate when setting ownHooksPresent and deciding mutation.
Exclude legacyManagedStatusMessage from ownership checks, while retaining it
only for cleanup if needed. Add a regression test covering entries with “Managed
by Vibe Island” and verify CodexHookInstallationManager.status() does not report
Open Island hooks as installed.

CodeRabbit's docstring coverage check flagged 4 of 11 functions in the
previous commit as undocumented. Covers the remaining seven, and records
why each new test exists rather than restating its name.

Co-Authored-By: Claude Opus 5 (1M context) <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.

1 participant